Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Changelog

All notable changes to the Threat.Zone Python SDK are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 1.1.0

API-sync release: brings the SDK into exact agreement with the current
`public-api` HTTP contract. Includes three breaking response-shape changes
(jobs, CDR, static-scan) alongside additive fields and endpoints.

### Breaking

- **Submission overview `jobs` reshape:** `SubmissionOverviewJobs` now carries
`total`, `succeeded`, `failed`, `skipped`, and `finished`. `completed` is
retained as a **deprecated** alias of `finished` (always equal) for
backward compatibility; new code should use `finished`.
- **CDR response is a per-submission singleton:** `CdrResponse` was rewritten
from an items envelope to a single object
(`submission`, `status`, `sanitized`, `removed`, `sanitizedFileInfo`,
`elapsedTime`, `metafields`). The old `CdrResult` envelope is gone.
- **Static-scan response is a per-format singleton:** `StaticScanResponse` was
rewritten from an items envelope to a single object discriminated by
`reportFormat` (`submission`, `status`, `reportFormat`, `level`, `score`,
`fileInfo`, `strings`, `dieResults`, `analysisTime`, `metafields`).
Format-specific inlined keys are preserved as model extras
(`extra="allow"`). The old `StaticScanResult` envelope is gone.

### Added

- `Submission.score` (`float | None`) and `Submission.family` (`str | None`).
- `ReportStatus.duration` (`float | None`).
- `OverviewSummary.score` (`float | None`) and `OverviewSummary.family`
(`str | None`).
- Config `active` / `accessible` flags on metafield options, metafield option
values, and environment options.
- `get_static_scan_strings(uuid)` — streams the raw extracted-strings JSON
for a static report.
- `list_network_configs()` — lists the workspace's network configurations via
`GET /v1/network-configs`; new `NetworkConfigListItem` /
`NetworkConfigListResponse` models.
- Request parameters: `safe_browsing` on `create_url_submission`;
`sort` / `order` on `list_submissions`; `type` / `process_name` on
`get_behaviours`.

### Changed

- `Indicator.category` is now `list[str]` (was a scalar `str`).
- `User-Agent` header bumped to `threatzone-python-sdk/1.1.0`.
- Fixed the `configurations` docstring key on `create_sandbox_submission` /
`create_open_in_browser_submission` to `networkConfig` (the only key the
API accepts).
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "threatzone"
version = "1.0.3"
version = "1.1.0"
description = "Python SDK for the Threat.Zone malware analysis platform"
readme = "README.md"
license = { text = "MIT" }
Expand Down
21 changes: 16 additions & 5 deletions src/threatzone/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
BehaviourOs,
BehavioursResponse,
CdrResponse,
CdrResult,
CdrSanitizedFileInfo,
CdrSanitizedFileInfoDetail,
Connection,
DnsQuery,
EmlAnalysis,
Expand All @@ -57,6 +58,8 @@
Metafields,
MitreResponse,
ModuleInfo,
NetworkConfigListItem,
NetworkConfigListResponse,
NetworkSummary,
NetworkThreat,
OverviewSummary,
Expand All @@ -68,8 +71,10 @@
ReportStatus,
SignatureCheckResponse,
SignatureCheckResult,
StaticScanDieResult,
StaticScanFileInfo,
StaticScanResponse,
StaticScanResult,
StaticScanStrings,
Submission,
SubmissionCreated,
SubmissionLimits,
Expand All @@ -84,7 +89,7 @@
YaraRulesResponse,
)

__version__ = "1.0.3"
__version__ = "1.1.0"

__all__ = [
# Clients
Expand Down Expand Up @@ -150,6 +155,9 @@
"HttpRequest",
"Connection",
"NetworkThreat",
# Types - Network configs
"NetworkConfigListItem",
"NetworkConfigListResponse",
# Types - Processes
"Process",
"ProcessesResponse",
Expand All @@ -162,9 +170,12 @@
"SyscallsResponse",
# Types - Static scan / CDR / Signature check
"StaticScanResponse",
"StaticScanResult",
"StaticScanFileInfo",
"StaticScanStrings",
"StaticScanDieResult",
"CdrResponse",
"CdrResult",
"CdrSanitizedFileInfo",
"CdrSanitizedFileInfoDetail",
"SignatureCheckResponse",
"SignatureCheckResult",
# Types - URL analysis
Expand Down
63 changes: 57 additions & 6 deletions src/threatzone/_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
MetafieldOption,
Metafields,
MitreResponse,
NetworkConfigListItem,
NetworkSummary,
NetworkThreat,
OverviewSummary,
Expand Down Expand Up @@ -178,6 +179,15 @@ async def get_environments(self) -> list[EnvironmentOption]:
response = await self._http.get("/config/environments")
return [EnvironmentOption.model_validate(item) for item in response.json()]

async def list_network_configs(self) -> list[NetworkConfigListItem]:
"""List the workspace's available network configurations (proxy/openvpn/wireguard).

Secrets (passwords, VPN file contents) are never returned — only `has_config_file`.
"""
response = await self._http.get("/v1/network-configs")
data = response.json()
return [NetworkConfigListItem.model_validate(item) for item in data.get("items", [])]

# =========================================================================
# Submissions - Create
# =========================================================================
Expand Down Expand Up @@ -214,7 +224,7 @@ async def create_sandbox_submission(
configurations: Advanced execution configuration options including:
- preScript: Script to run before analysis
- startArguments: Command line arguments for the sample
- network_config: Network configuration ID (MongoDB ObjectId)
- networkConfig: Network configuration ID (MongoDB ObjectId)

Returns:
SubmissionCreated with the new submission UUID.
Expand Down Expand Up @@ -311,20 +321,24 @@ async def create_url_submission(
url: str,
*,
private: bool = False,
safe_browsing: bool = False,
) -> SubmissionCreated:
"""
Create a new URL analysis submission.

Args:
url: URL to analyze.
private: If True, submission is private to your workspace.
safe_browsing: If True, spawn an isolated safe-browsing session
(a sandboxed Chromium pod accessible over noVNC) alongside
URL analysis. Default: False.

Returns:
SubmissionCreated with the new submission UUID.
"""
response = await self._http.post(
"/submissions/url_analysis",
json={"url": url, "private": private},
json={"url": url, "private": private, "safeBrowsing": safe_browsing},
)
return SubmissionCreated.model_validate(response.json())

Expand Down Expand Up @@ -392,6 +406,8 @@ async def list_submissions(
end_date: datetime | str | None = None,
private: bool | None = None,
tags: list[str] | None = None,
sort: str | None = None,
order: str | None = None,
) -> PaginatedSubmissions:
"""
List submissions with optional filters.
Expand All @@ -407,6 +423,8 @@ async def list_submissions(
end_date: Filter submissions created before this date.
private: Filter by privacy status.
tags: Filter by tags.
sort: Sort field (e.g. "createdAt").
order: Sort order ("asc" or "desc").

Returns:
PaginatedSubmissions with items and pagination info.
Expand All @@ -430,6 +448,10 @@ async def list_submissions(
params["private"] = private
if tags:
params["tags"] = tags
if sort is not None:
params["sort"] = sort
if order is not None:
params["order"] = order

response = await self._http.get("/submissions", params=params)
return PaginatedSubmissions.model_validate(response.json())
Expand Down Expand Up @@ -693,32 +715,53 @@ async def get_mitre_techniques(self, uuid: str) -> MitreResponse:

async def get_static_scan_results(self, uuid: str) -> StaticScanResponse:
"""
Get static scan results (per artifact) for a submission.
Get static analysis results for a submission.

The response is a per-submission static analysis report, discriminated
by ``reportFormat``. Format-specific keys (e.g. ``peExeSections``,
``apkInfo``) are inlined at the top level and preserved on
``.model_extra``.

Args:
uuid: Submission UUID.

Returns:
Static scan envelope with per-artifact analyzer output.
Static analysis report singleton.

Raises:
ReportUnavailableError: When the static report is not yet available.
"""
response = await self._http.get(f"/submissions/{uuid}/static-scan")
return StaticScanResponse.model_validate(response.json())

async def get_static_scan_strings(self, uuid: str) -> AsyncDownloadResponse:
"""Stream the raw extracted-strings JSON (``{uuid}_strings.json``) for a static report.

The response is a streamed application/json file (can be multi-MB). Use
``.read()``, ``.iter_bytes()``, or ``.save(path)``. Raises
ReportUnavailableError (409) when the static report is not complete,
NotFoundError (404) when no strings file exists.

Args:
uuid: Submission UUID.

Returns:
AsyncDownloadResponse for streaming or saving the strings JSON file.
"""
return await self._http.get_stream(f"/submissions/{uuid}/static-scan/strings")

async def get_cdr_results(self, uuid: str) -> CdrResponse:
"""
Get Content Disarm and Reconstruction (CDR) results for a submission.

This returns the structured per-artifact CDR engine output. Use
This returns the per-submission CDR transformation result. Use
``download_cdr_result()`` to download the sanitized file itself.

Args:
uuid: Submission UUID.

Returns:
CDR results envelope.
CDR transformation result.

Raises:
ReportUnavailableError: When the CDR report is not yet available.
Expand Down Expand Up @@ -783,8 +826,10 @@ async def get_behaviours(
uuid: str,
*,
os: BehaviourOs,
type: str | None = None,
pid: int | None = None,
operation: str | None = None,
process_name: str | None = None,
page: int | None = None,
limit: int | None = None,
) -> BehavioursResponse:
Expand All @@ -794,8 +839,10 @@ async def get_behaviours(
Args:
uuid: Submission UUID.
os: REQUIRED. Target operating system (windows/linux/android/macos).
type: Filter by event type (e.g. registry, file, network, process, mutex).
pid: Filter by process ID.
operation: Filter by operation name.
process_name: Filter by process name (exact match).
page: 1-indexed page number.
limit: Maximum items per page (1-500).

Expand All @@ -810,10 +857,14 @@ async def get_behaviours(
raise ValueError("get_behaviours() requires the 'os' keyword argument")

params: dict[str, Any] = {"os": os}
if type is not None:
params["type"] = type
if pid is not None:
params["pid"] = pid
if operation is not None:
params["operation"] = operation
if process_name is not None:
params["processName"] = process_name
if page is not None:
params["page"] = page
if limit is not None:
Expand Down
2 changes: 1 addition & 1 deletion src/threatzone/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
API_KEY_ENV_VAR: Final[str] = "THREATZONE_API_KEY"
API_KEY_HEADER: Final[str] = "Authorization"

USER_AGENT: Final[str] = "threatzone-python-sdk/1.0.0"
USER_AGENT: Final[str] = "threatzone-python-sdk/1.1.0"

CHUNK_SIZE: Final[int] = 8192
Loading
Loading