Metadata-Version: 2.4
Name: threatzone
Version: 1.1.0
Summary: Python SDK for the Threat.Zone malware analysis platform
Project-URL: Homepage, https://threat.zone
Project-URL: Documentation, https://github.com/Malwation/threatzone-python-sdk
Project-URL: Repository, https://github.com/Malwation/threatzone-python-sdk
Project-URL: Issues, https://github.com/Malwation/threatzone-python-sdk/issues
Author-email: Malwation <info@malwation.com>
License: MIT
License-File: LICENSE
Keywords: analysis,malware,sandbox,sdk,security,threatzone
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: dev
Requires-Dist: mypy>=1.10.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Description-Content-Type: text/markdown

# Threat.Zone Python SDK

Official Python SDK for the [Threat.Zone](https://threat.zone) malware analysis platform.
Targets the Threat.Zone Public API, ships fully typed Pydantic v2
models for every endpoint, and exposes both synchronous (`ThreatZone`) and asynchronous
(`AsyncThreatZone`) clients with identical method surfaces. Requires **Python 3.10+**.

> [!WARNING]
> ## ⚠️ Version Compatibility Notice
>
> **This SDK requires Threat.Zone v3.2.0 or later.**
>
> This SDK targets the Public API shipped with **Threat.Zone v3.2.0**. It will **not** function correctly against earlier versions of the platform.
>
>
> Running an older Threat.Zone version? Pin to a pre-v3.2.0 release of this SDK, or coordinate with your admin to upgrade the platform first.

## Install

```bash
pip install threatzone
# or
uv add threatzone
```

## Configure

```python
from threatzone import ThreatZone

client = ThreatZone(api_key="<your-api-key>")
```

The client reads `THREATZONE_API_KEY` from the environment, so `ThreatZone()` is enough
if you export it. The default `base_url` is `https://app.threat.zone/public-api`.

For on-prem or local development, pass `base_url` explicitly:

```python
client = ThreatZone(
    api_key="<your-api-key>",
    base_url="https://threatzone.your-company.internal/public-api",
)
```

The `base_url` **must** include the `/public-api` suffix. For self-signed certs, leave
`verify_ssl=False` (the default).

## Quick usage

```python
from pathlib import Path

account = client.get_user_info()
print(f"Workspace: {account.workspace_name}")

submission = client.create_sandbox_submission(Path("./sample.exe"))
completed = client.wait_for_completion(submission.uuid, timeout=600)
print(f"Verdict: {completed.level}")
print(f"MITRE techniques: {completed.mitre_techniques}")
```

- Pass `auto_select_environment=True` to let the server route the sample to a Windows / Linux / macOS / Android environment based on its detected type. See [recipe 14](./docs/RECIPES.md#14-auto-select-sandbox-environment).

### Filename canonicalisation (`dynamic_mimetype_check`)

By default the server appends the real (Magika-detected) extension to the
persisted filename when it disagrees with what the file was named. Opt out per
submission &mdash; sandbox / open-in-browser take it as a `metafields` key, static
/ CDR take it as a top-level kwarg:

```python
# sandbox / open-in-browser → inside metafields
client.create_sandbox_submission(
    "./malware.txt",
    environment="w10_x64",
    metafields={"dynamic_mimetype_check": False, "timeout": 120},
)

# static / CDR → top-level kwarg
client.create_static_submission("./malware.txt", dynamic_mimetype_check=False)
client.create_cdr_submission("./report.doc", dynamic_mimetype_check=False)
```

See [Recipe 13](docs/RECIPES.md#13-control-dynamic_mimetype_check) for the full
breakdown including reading back `FileInfo.is_mimetype_checked` after the
submission completes.

## Core concepts

- **Submissions** — one analysis target (file or URL) with a stable `uuid`, a workspace
  owner, a `private` flag, and up to four reports.
- **Reports** — `dynamic`, `static`, `cdr`, `url_analysis`. Independent; poll each via
  `submission.is_complete()` / `submission.has_errors()`. Every report carries its own
  `status`, `level`, `score`.
- **Observation streams** — the dynamic report produces `indicators` (rule hits),
  `iocs` (concrete IOC values), `behaviours` (OS-level operations — the `os` query param
  is required), and `syscalls` (raw log lines).
- **Artifacts** — files produced or extracted during analysis; keyed by `hashes.sha256`.
- **Access control** — public submissions are visible to every API key; private
  submissions are workspace-scoped. Cross-workspace reads of private submissions raise
  `PermissionDeniedError` (403).

## Async

`AsyncThreatZone` mirrors `ThreatZone` method-for-method:

```python
import asyncio
from threatzone import AsyncThreatZone

async def main() -> None:
    async with AsyncThreatZone(api_key="<your-api-key>") as client:
        account = await client.get_user_info()
        print(account.email)

asyncio.run(main())
```

Use `asyncio.gather()` for concurrent fan-out. See [recipe 10](./docs/RECIPES.md#10-use-the-async-client).

## Errors

Every error inherits from `ThreatZoneError`. HTTP errors additionally inherit from
`APIError` and carry the parsed error envelope on `.body`, with the structured code at
`.body["code"]`. Branch on `.code`, never on `.message`.

| Status | Exception | When |
|---|---|---|
| — | `ThreatZoneError` | Base class. Catch for a single safety net. |
| — | `APIError` | HTTP error base class. `.status_code`, `.body`, `.response`. |
| 400 | `BadRequestError` | `INVALID_UUID`, `INVALID_QUERY_PARAM`. |
| 401 | `AuthenticationError` | Missing/invalid API key. |
| 402 | `PaymentRequiredError` | Workspace subscription inactive. |
| 403 | `PermissionDeniedError` | Cross-workspace private read, or module not enabled. |
| 404 | `NotFoundError` | Submission, artifact, or media missing. |
| 409 | `ReportUnavailableError` | Required report not yet available. Carries `.code`, `.submission_uuid`, `.required_report`, `.current_status`, `.available_reports`. |
| 429 | `RateLimitError` | API quota exceeded. Has `.retry_after`. |
| 5xx | `InternalServerError` | Server-side failure. |
| — | `TimeoutError` | HTTP request timed out. |
| — | `ConnectionError` | Network-level failure. |
| — | `AnalysisTimeoutError` | `wait_for_completion()` exceeded its timeout. `.uuid`, `.elapsed`. |
| 202 | `YaraRulePendingError` | YARA rule generation still in progress. `.retry_after`. |

```python
from threatzone import ReportUnavailableError, ThreatZone

try:
    indicators = client.get_indicators("00000000-0000-0000-0000-000000000000")
except ReportUnavailableError as exc:
    if exc.code == "DYNAMIC_REPORT_UNAVAILABLE":
        print(f"Dynamic report not ready (status: {exc.current_status}).")
```

See [recipe 9](./docs/RECIPES.md#9-discriminate-errors-by-their-code-field) for the full
discrimination pattern and [recipe 7](./docs/RECIPES.md#7-handle-the-reportunavailableerror-exception)
for the recommended retry loop.

## Documentation

| Document | Purpose |
|---|---|
| [Recipes](./docs/RECIPES.md) | Runnable examples covering common tasks. |
| [Type Reference](./docs/TYPES.md) | Pydantic model overview grouped by feature area. |

## Testing utilities

The SDK ships a stateful in-process fake Threat.Zone API for consumer-side tests:

```python
from threatzone import ThreatZone
from threatzone.testing import FakeThreatZoneAPI, scenarios

fake = FakeThreatZoneAPI()
scenarios.seed_malicious_pe(fake, sha256="a" * 64)

client = ThreatZone(
    api_key="test-key",
    base_url="https://fake.threat.zone/public-api",
    http_client=fake.as_httpx_client(),
)
```

Available scenarios: `seed_malicious_pe`, `seed_benign_document`, `seed_cdr_document`,
`seed_phishing_url`, `seed_static_only_submission`, `seed_private_cross_workspace`.

## Development

```bash
uv sync --all-extras --dev
uv run pytest tests/
uv run ruff check src/ tests/
uv run mypy src/threatzone
uv build
```

All tests run in-process against `FakeThreatZoneAPI` — no network, no API token. Live
smoke-testing uses the scripts in `examples/` with `PUBLIC_API_TOKEN` exported; see
`examples/README.md`.

GitHub Actions workflows can be exercised locally via `act` — see
[`.github/workflows/README.md`](./.github/workflows/README.md).

## Links

- [Threat.Zone platform](https://threat.zone)
- [Public API reference](https://app.threat.zone/public-api/docs)
- [PyPI package](https://pypi.org/project/threatzone/)
- [GitHub repository](https://github.com/Malwation/threatzone-python-sdk)

## License

MIT — see [LICENSE](LICENSE).
