Metadata-Version: 2.4
Name: crimson-crawler-client
Version: 4.1.1
Summary: Typed Python client for the Crimson Crawler API
Project-URL: Homepage, https://crimsoncrawler.com
Project-URL: Repository, https://github.com/crimson-crawler/python-sdk
Project-URL: Documentation, https://crimsoncrawler.com/docs/clients/python-client/
Author: Def-Logix, Inc.
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: crimson-crawler,cti,openapi,threat-intelligence
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: attrs>=24.0
Requires-Dist: httpx<1,>=0.28
Requires-Dist: python-dateutil>=2.9
Provides-Extra: dev
Requires-Dist: hatchling<2,>=1.27; extra == 'dev'
Requires-Dist: mypy<2,>=1.19; extra == 'dev'
Requires-Dist: openapi-python-client>=0.28.3; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-cov>=7; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.15; extra == 'dev'
Requires-Dist: twine==6.2.0; extra == 'dev'
Description-Content-Type: text/markdown

# Crimson Crawler Python SDK

<p align="center">
  <a href="https://crimsoncrawler.com">
    <img src="https://crimsoncrawler.com/logo-icon.png" alt="Crimson Crawler spider mark" width="112">
  </a>
</p>
<p align="center">
  <a href="https://crimsoncrawler.com">
    <img src="https://crimsoncrawler.com/crimson-crawler-logo-python.svg" alt="Crimson Crawler" width="520">
  </a>
</p>
<p align="center"><strong>Typed, async threat intelligence for Python.</strong></p>
<p align="center">
  <a href="https://crimsoncrawler.com/docs/clients/python-client/">Documentation</a> ·
  <a href="https://pypi.org/project/crimson-crawler-client/">PyPI</a> ·
  <a href="https://github.com/crimson-crawler/python-sdk">GitHub</a> ·
  <a href="https://crimsoncrawler.com/dashboard">Get an API key</a>
</p>
<p align="center">
  <a href="https://pypi.org/project/crimson-crawler-client/"><img alt="Published on PyPI" src="https://img.shields.io/badge/package-PyPI-EF4444?style=flat-square&labelColor=18181B"></a>
  <img alt="Python 3.11+" src="https://img.shields.io/badge/Python-3.11%2B-EF4444?style=flat-square&labelColor=18181B">
  <img alt="Typed package" src="https://img.shields.io/badge/typing-py.typed-EF4444?style=flat-square&labelColor=18181B">
  <a href="LICENSE"><img alt="Apache 2.0" src="https://img.shields.io/badge/license-Apache--2.0-EF4444?style=flat-square&labelColor=18181B"></a>
</p>

Turn a CVE into a complete, typed attack briefing with one async call. The client also covers package, IOC, CWE, and ATT&CK enrichment, artifact and inventory workflows, grounded reports, and every v1 endpoint without hand-written request models.

## Install

```bash
pip install crimson-crawler-client
# or
uv add crimson-crawler-client
```

Python 3.11+.

PyPI publication is pending. The install command above applies once the first release lands; until then, use the repository checkout for evaluation. The badge above will show the registry version once that release succeeds.

## Authentication

Set your key in the environment:

```bash
export CRIMSON_CRAWLER_API_KEY=your-api-key
```

Or pass it directly (preferred for tests and multi-tenant code):

```python
async with CrawlerClient(api_key="your-api-key") as client:
    ...
```

The constructor argument wins over the env var. Generate a key at [crimsoncrawler.com/dashboard](https://crimsoncrawler.com/dashboard).

## Custom or local endpoint

The hosted API at `https://crimsoncrawler.com` is the default. Point the
official package at a local or self-hosted deployment with either form:

```bash
export CRIMSON_CRAWLER_BASE_URL=http://localhost:8000
```

```python
client = CrawlerClient(base_url="http://localhost:8000")
```

The SDK sends its `apikey` header to the selected origin. Use a local or
development key when overriding the endpoint; do not reuse a production key
with an origin you do not control. Custom non-loopback endpoints must use
HTTPS. Plain HTTP is accepted only for loopback development URLs such as the
example above. TLS verification stays enabled by default; trust the
deployment's CA when possible. `verify_ssl=False` or
`CRIMSON_CRAWLER_VERIFY_SSL=false` is only for isolated local development with
a self-signed certificate. The SDK emits a warning for either override.

## Quick start

```python
import asyncio
from crimson_crawler_client import CrawlerClient


async def main() -> None:
    async with CrawlerClient() as client:
        result = await client.enrich_cve_full("CVE-2024-3400")

        if result.cve:
            print(result.cve.cvss_score)

        for technique in result.techniques or []:
            print(technique.technique_id, technique.name)


asyncio.run(main())
```

Every list-valued field on an enrichment response can be `None` if that layer wasn't requested or had no data. The `or []` guard is the standard pattern.

## The client

### Constructor

```python
CrawlerClient(
    api_key: str | None = None,
    base_url: str | None = None,
    verify_ssl: bool | None = None,
    max_retries: int = 2,
)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `api_key` | `str \| None` | env `CRIMSON_CRAWLER_API_KEY` | Sent as the `apikey` header on every request. |
| `base_url` | `str \| None` | env `CRIMSON_CRAWLER_BASE_URL`, then `https://crimsoncrawler.com` | Custom, self-hosted, or local API origin. |
| `verify_ssl` | `bool \| None` | env `CRIMSON_CRAWLER_VERIFY_SSL`, then `True` | Disable only for isolated local development; prefer trusting the deployment CA. |
| `max_retries` | `int` | `2` | Number of automatic retries on transient failures (see [Retry & rate limits](#retry--rate-limits)). `0` disables. |

Raises `MissingCredentials` if neither `api_key` nor the env var is set. Raises `ValueError` for a malformed or insecure non-loopback `base_url`, or if `max_retries < 0`.

### Properties

- **`api`** — pass this to any endpoint function when calling endpoints directly (see [Beyond the convenience surface](#beyond-the-convenience-surface)).
- **`base_url`** — read-only; the resolved hosted, custom, or local endpoint.
- **`max_retries`** — read-only; the configured retry budget (see [Retry & rate limits](#retry--rate-limits)).

### Async lifecycle

`CrawlerClient` is an async context manager. The recommended pattern:

```python
async with CrawlerClient() as client:
    result = await client.enrich_cve_full("CVE-2024-3400")
```

Entering opens the HTTP client. Exiting closes it.

If you need finer control:

```python
client = CrawlerClient()
try:
    result = await client.enrich_cve_full("CVE-2024-3400")
finally:
    await client.aclose()
```

A single `CrawlerClient` can issue many concurrent requests. TCP connections are reused across calls, so don't construct one per request.

## Convenience methods

**Every `/v1` operation has a method** — 33 of them, plus the `search_techniques` shortcut. The seven documented in full below cover the highest-traffic patterns; the rest are grouped after them with their signatures. The 15 `enrich_*_full` methods call their endpoint with a full-walk `include=[...]` preset baked in (pass `include=` to override); the others take no preset. Every method is `async`, returns a typed response model, and raises `UnexpectedStatus` on a non-2xx.

| Group | Methods |
|---|---|
| Full-walk enrichment (15) | `enrich_cve_full` · `enrich_ioc_full` · `enrich_cwe_full` · `enrich_technique_full` · `enrich_product_full` · `enrich_package_full` · `enrich_capec_full` · `enrich_group_full` · `enrich_software_full` · `enrich_campaign_full` · `enrich_atlas_full` · `enrich_disarm_full` · `enrich_defend_full` · `enrich_location_full` · `enrich_sector_full` |
| Enrichment, no preset (2) | `enrich_batch` · `enrich_poc_source` |
| Search (8 + 1) | `search_cve` · `search_cti` · `search_kev` · `search_misp` · `search_knowledgebase` · `search_vendor` · `search_poc` · `search_d3fend` · `search_techniques` |
| Assessment (4) | `assess_technique_coverage` · `assess_vulnerability_exposure` · `assess_group_exposure` · `assess_ioc_portfolio` |
| Async engines (4) | `artifact_enrich` · `artifact_enrich_status` · `inventory_enrich` · `inventory_enrich_status` |

#### `enrich_cve_full(cve_id, *, include=None)`

Returns CVE details, EPSS exploit probability, KEV status, mapped weaknesses, attack patterns, ATT&CK techniques, TIE threat predictions, MISP events, knowledge-base context, and web findings. Single request.

```python
async with CrawlerClient() as client:
    result = await client.enrich_cve_full("CVE-2024-3400")

    if result.cve:
        print(f"CVSS: {result.cve.cvss_score} ({result.cve.cvss_severity})")
        print(f"Description: {result.cve.description}")

    if result.epss:
        print(f"EPSS: {result.epss.epss} (percentile {result.epss.percentile})")
    if result.kev and result.kev.in_kev:
        print(f"KEV: added {result.kev.date_added}")

    print(f"Weaknesses: {len(result.weaknesses or [])} CWE(s)")
    print(f"Attack patterns: {len(result.attack_patterns or [])} CAPEC(s)")
    for t in result.techniques or []:
        print(f"  Technique: {t.technique_id} - {t.name}")
    for p in result.predictions or []:
        print(f"  TIE prediction: {p.technique_id} ({p.probability})")
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `cve_id` | `str` | required | CVE identifier, e.g. `"CVE-2024-3400"`. |
| `include` | `list[str] \| None` | `presets.CVE_FULL_WALK` | Override the default include list. |

Returns: `EnrichCVEResponse`.

#### `enrich_ioc_full(value, ioc_type="ip-dst", *, include=None)`

Returns MISP attribute matches, linked ATT&CK techniques, attributed threat groups, knowledge-base context, and web findings for an indicator (IP, hash, domain, URL).

```python
async with CrawlerClient() as client:
    result = await client.enrich_ioc_full("203.0.113.5", "ip-dst")

    for attr in result.misp_attributes or []:
        print(f"MISP event: {attr.event_info}")
    for t in result.techniques or []:
        print(f"Linked technique: {t.technique_id}")
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `value` | `str` | required | Indicator value. |
| `ioc_type` | `str` | `"ip-dst"` | MISP-style attribute type: `ip-dst`, `ip-src`, `domain`, `hostname`, `md5`, `sha1`, `sha256`, `url`, etc. |
| `include` | `list[str] \| None` | `presets.IOC_FULL_WALK` | Override the default include list. |

Returns: `EnrichIOCResponse`.

#### `enrich_cwe_full(cwe_id, *, include=None)`

Returns the weakness, mapped CAPEC attack patterns, ATT&CK techniques, and TIE predictions. Use it when you start from a weakness ID.

```python
async with CrawlerClient() as client:
    result = await client.enrich_cwe_full("CWE-79")

    if result.weakness:
        print(f"Weakness: {result.weakness.name}")
    print(f"CAPEC patterns: {len(result.attack_patterns or [])}")
    print(f"Techniques: {len(result.techniques or [])}")
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `cwe_id` | `str` | required | CWE identifier, e.g. `"CWE-79"`. |
| `include` | `list[str] \| None` | `presets.CWE_FULL_WALK` | Override the default include list. |

Returns: `EnrichCWEResponse`.

#### `enrich_technique_full(technique_ids, *, framework="enterprise", include=None)`

Returns technique details, TIE predictions, attributed groups, software, campaigns, and a Navigator JSON layer for one or more ATT&CK techniques.

```python
async with CrawlerClient() as client:
    result = await client.enrich_technique_full(
        ["T1190", "T1059.001"],
        framework="enterprise",
    )

    for t in result.techniques or []:
        print(f"{t.technique_id}: {t.name}")
    for g in result.groups or []:
        print(f"Group using these: {g.name}")
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `technique_ids` | `list[str]` | required | ATT&CK technique IDs. |
| `framework` | `str` | `"enterprise"` | ATT&CK matrix variant: `enterprise`, `ics`, or `mobile`. |
| `include` | `list[str] \| None` | `presets.TECHNIQUE_FULL` | Override the default include list. |

Returns: `EnrichTechniqueResponse`.

#### `enrich_product_full(product, *, version=None, vendor=None, include=None)`

Finds vulnerabilities affecting a product/version and enriches each matched CVE with severity, exploit signals, weakness mapping, ATT&CK techniques, threat predictions, MISP events, and web context.

```python
async with CrawlerClient() as client:
    result = await client.enrich_product_full(
        "Apache HTTP Server",
        version="2.4.51",
        vendor="apache",
    )

    for cve in result.cves or []:
        print(f"{cve.cve_id}: {cve.cvss_score} ({cve.cvss_severity})")
        if cve.kev and cve.kev.in_kev:
            print("  in KEV")
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `product` | `str` | required | Product name, e.g. `"Apache HTTP Server"`, `"nginx"`. |
| `version` | `str \| None` | `None` | Specific version (e.g. `"2.4.51"`). Optional. |
| `vendor` | `str \| None` | `None` | Vendor name to narrow results (e.g. `"apache"`). Optional. |
| `include` | `list[str] \| None` | `presets.PRODUCT_FULL` | Override the default include list. |

Returns: `EnrichProductResponse`.

#### `enrich_package_full(ecosystem, package, *, version=None, include=None)`

The package-ecosystem sibling of `enrich_product_full`. OSV.dev supplies the package's advisories (npm / PyPI / Go / Maven / crates.io …, including the non-CVE GHSA / PYSEC / RUSTSEC / GO findings the CPE-keyed product endpoint can't reach), and the CVEs they alias are enriched through the same CWE → CAPEC → ATT&CK + EPSS/KEV chain.

```python
async with CrawlerClient() as client:
    result = await client.enrich_package_full("PyPI", "django", version="4.0")

    for cve in result.cves or []:
        print(f"{cve.cve_id}: {cve.cvss_score} ({cve.cvss_severity})")
        if cve.kev and cve.kev.in_kev:
            print("  in KEV")
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `ecosystem` | `str` | required | OSV ecosystem, e.g. `"PyPI"`, `"npm"`, `"Go"`, `"Maven"`, `"crates.io"`. |
| `package` | `str` | required | Package name within the ecosystem, e.g. `"django"`. |
| `version` | `str \| None` | `None` | Specific version (e.g. `"4.0"`). Optional. |
| `include` | `list[str] \| None` | `presets.PACKAGE_FULL` | Override the default include list. |

Returns: `EnrichPackageResponse`.

#### `search_techniques(keyword, *, limit=20)`

Keyword-searches the ATT&CK knowledge base and returns matching techniques (attack-patterns).

```python
async with CrawlerClient() as client:
    result = await client.search_techniques("credential dumping", limit=10)

    for hit in result.results or []:
        print(hit)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `keyword` | `str` | required | Free-text query, e.g. `"phishing"`, `"credential dumping"`. |
| `limit` | `int` | `20` | Maximum number of results (1-100). |

Returns: `SearchCTIResponse`.

#### The other nine full-walk enrichments

Same shape as the six above: a full-walk preset baked in, `include=` to override. Each takes an **optional id positional or `search=`** — pass one, not both — so you can go straight to an id or find the entity by name.

```python
async with CrawlerClient() as client:
    apt29 = await client.enrich_group_full("G0016")
    same = await client.enrich_group_full(search="cozy bear")
    ml = await client.enrich_atlas_full("AML.T0043")
```

| Method | Id parameter | Extra keywords | Returns |
|---|---|---|---|
| `enrich_capec_full(capec_id=None, …)` | `CAPEC-66` | `search`, `include` | `EnrichCAPECResponse` |
| `enrich_group_full(group_id=None, …)` | `G0016` | `search`, `framework`, `include` | `EnrichGroupResponse` |
| `enrich_software_full(software_id=None, …)` | `S0154` | `search`, `framework`, `include` | `EnrichSoftwareResponse` |
| `enrich_campaign_full(campaign_id=None, …)` | `C0028` | `search`, `framework`, `include` | `EnrichCampaignResponse` |
| `enrich_atlas_full(technique_id=None, …)` | `AML.T0043` | `search`, `include` | `EnrichATLASResponse` |
| `enrich_disarm_full(technique_id=None, …)` | `T0001` | `search`, `include` | `EnrichDISARMResponse` |
| `enrich_defend_full(d3fend_id=None, …)` | `D3-AL` | `search`, `include` | `EnrichD3FENDResponse` |
| `enrich_location_full(location_id=None, …)` | `L0001` | `search`, `include` | `EnrichLocationResponse` |
| `enrich_sector_full(sector_id=None, …)` | `financial-services` | `search`, `include` | `EnrichSectorResponse` |

`framework` is `"enterprise"` (default), `"ics"`, or `"mobile"`, and only exists where the endpoint supports a matrix variant.

#### `enrich_batch(items)`

Up to 50 heterogeneous `enrich/*` lookups in **one** request — one call against your rate limit. Items are plain dicts: a `type` discriminator plus that type's fields and its own optional `include`. No preset; the list passes through untouched.

```python
async with CrawlerClient() as client:
    resp = await client.enrich_batch([
        {"type": "cve", "cve_id": "CVE-2024-3400", "include": ["details", "kev"]},
        {"type": "ioc", "value": "1.2.3.4", "value_type": "ip-dst"},
        {"type": "technique", "technique_id": "T1190"},
    ])

    for item in resp.results or []:      # input order preserved
        print(item.index, item.type, item.status)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `items` | `list[dict[str, Any]]` | required | 1–50 items. `type` is one of `cve`, `product`, `package`, `cwe`, `capec`, `technique`, `ioc`, `group`, `software`, `campaign`, `atlas`, `disarm`, `location`, `sector`, `defend`, `poc_source`. |

Returns: `EnrichBatchResponse`. One failing item does not fail the batch — each result carries its own `status` and `errors[]`. Two field names to get right: IOC items use **`value_type`** for the indicator type (`type` is taken by the discriminator, unlike `enrich_ioc_full`'s `ioc_type` argument), and the D3FEND discriminator is **`defend`**, matching the endpoint path.

#### `enrich_poc_source(repo_url)`

LLM-summarized analysis of a proof-of-concept exploit repository — what the code does, how weaponized it looks, what it targets. No `include`.

```python
async with CrawlerClient() as client:
    pocs = await client.search_poc("CVE-2024-3400")
    analysis = await client.enrich_poc_source("https://github.com/example/CVE-2024-3400-poc")
```

Returns: `FetchPoCSourceResponse`.

#### Search (8 methods)

One method per search endpoint. `search_cve` is the only one that accepts `include=`; none of them uses a preset. `search_kev` and `search_misp` take no positional at all — call them bare to browse.

```python
async with CrawlerClient() as client:
    weaponized = await client.search_cve(kev_only=True, epss_min=0.9, limit=20)
    kev = await client.search_kev(ransomware_status="Known")
    answer = await client.search_knowledgebase("how do I detect kerberoasting", alpha=0.6)
```

| Method | Signature | Returns |
|---|---|---|
| `search_cve` | `(query=None, *, cve_id, stix_id, cwe_id, capec_id, attack_id, cpe, cpes_not_vulnerable, created_by_ref, created_min, created_max, modified_min, modified_max, cvss_min, cvss_v2_min, cvss_v4_min, epss_min, epss_percentile_min, kev_only=False, vuln_status, sort, limit=50, page=1, include)` | `SearchCVEResponse` |
| `search_cti` | `(query, *, sources=None, types=None, limit=20, page=1, deprecated=None, revoked=None)` | `SearchCTIResponse` |
| `search_kev` | `(*, cve_id=None, ransomware_status=None, limit=50, page=1)` | `SearchKEVResponse` |
| `search_misp` | `(*, value, type_attribute, category, event_id, eventinfo, tags, date_from, date_to, published, to_ids, threat_level_id, limit=50, page=1)` | `SearchMISPResponse` |
| `search_knowledgebase` | `(query, *, collections=None, alpha=0.7, limit=10)` | `SearchKnowledgebaseResponse` |
| `search_vendor` | `(vendor, *, limit=50)` | `SearchVendorResponse` |
| `search_poc` | `(cve_id, *, limit=25)` | `SearchPoCResponse` |
| `search_d3fend` | `(query, *, d3fend_form=None, limit=20, page=1)` | `SearchD3FENDResponse` |

Constrained values: `vuln_status` is `Analyzed` / `Awaiting Analysis` / `Modified` / `Received` / `Rejected`; `sort` is `created_ascending` / `created_descending` / `modified_ascending` / `modified_descending` / `epss_score_descending` / `x_opencti_cvss_base_score_descending`; `ransomware_status` is `Known` / `Unknown`; `d3fend_form` is `tactic` / `mitigation` / `sub-mitigation` / `artifact`; `collections` draws from `main` / `large` / `user` / `red_team`; `types` from `attack-pattern` / `intrusion-set` / `malware` / `tool` / `campaign` / `weakness` / `course-of-action`. You pass plain strings — the wrapper converts them.

#### Assessment (4 methods)

Portfolio questions instead of single-entity lookups: what a *set* of techniques, CVEs, actors, or indicators means together. Each takes an optional `include=` passthrough (no preset — omit it and the server default applies); the endpoints cap their inputs at 50.

```python
async with CrawlerClient() as client:
    gaps = await client.assess_technique_coverage(["T1190", "T1059.001", "T1566"])
    exposure = await client.assess_vulnerability_exposure(
        ["CVE-2024-3400", "CVE-2021-44228"],
        stakeholder={"exposure": "controlled", "mission_prevalence": "essential"},
    )
    actors = await client.assess_group_exposure(group_ids=["G0016", "G0007"])
    portfolio = await client.assess_ioc_portfolio([
        {"value": "1.2.3.4", "type": "ip-dst"},
        {"value": "evil.example", "type": "domain"},
    ])
```

| Method | Signature | Returns |
|---|---|---|
| `assess_technique_coverage` | `(technique_ids, *, framework="enterprise", include=None)` | `AssessTechniqueCoverageResponse` |
| `assess_vulnerability_exposure` | `(cve_ids, *, stakeholder=None, include=None)` | `AssessVulnerabilityExposureResponse` |
| `assess_group_exposure` | `(*, group_ids=None, search=None, framework="enterprise", include=None)` | `AssessGroupExposureResponse` |
| `assess_ioc_portfolio` | `(indicators, *, include=None)` | `AssessIOCPortfolioResponse` |

`stakeholder` is the SSVC decision dict (`method`, `exposure`, `mission_prevalence`, `human_impact`, `public_wellbeing_impact`) and applies when `include` asks for `ssvc`. `assess_group_exposure` takes `group_ids`, free-text `search` terms, or both.

#### Async engines (4 methods)

The raw submit/status pairs behind the two background enrichment engines. A submit returns a job handle immediately (**it counts toward your usage**); the status read is **usage-exempt**, so poll as often as you like. For a one-call version that submits, polls, and hands back the finished report, use [`client.enricher.enrich`](#enricher-clientenricher) or `client.inventory_manager.ingest_scan`.

```python
async with CrawlerClient() as client:
    job = await client.artifact_enrich(text="Suspicious login from 10.1.1.1, then CVE-2024-1234 scan")
    status = await client.artifact_enrich_status(job.enrichment_id)

    inv = await client.inventory_enrich([{"product": "nginx", "version": "1.24.0"}])
    findings = await client.inventory_enrich_status(inv.enrichment_id)
```

| Method | Signature | Returns |
|---|---|---|
| `artifact_enrich` | `(*, text=None, files=None, formats=None, stakeholder=None, include=None)` | `EnrichmentSubmitResponse` |
| `artifact_enrich_status` | `(enrichment_id)` | `ArtifactEnrichmentJobResponse` |
| `inventory_enrich` | `(items, *, include=None, attachments_text=None, attachments_files=None)` | `EnrichmentSubmitResponse` |
| `inventory_enrich_status` | `(enrichment_id)` | `InventoryEnrichmentJobResponse` |

`files` and `attachments_files` are `[{"filename": …, "content_b64": …}]`; `items` are asset dicts (`product` is the only required key). `formats` picks the machine exports to build alongside the report (`report`, `json`, `stix`, `vex`, `csaf`, `navigator`, `kev_remediation`, `oscal_poam`, `misp`, `csv`). A submit returns `202` with `status: "queued"` plus a `queue_position` when your key already has 3 enrichments running; past 20 in flight it raises `UnexpectedStatus(429)` with `error: "enrichment_queue_full"`.

## Presets

A preset is a named `list[str]`: a curated set of `include` values for one of the common workflows. The point is so you don't have to memorize which `include` values are valid for which endpoint, or pick the right depth for your use case every time.

The `presets` module exports 18 — one full-walk default per enrichment endpoint that accepts `include`, plus three depth variants for the two highest-traffic ones:

| Preset | Use for | Include values |
|---|---|---|
| `CVE_FULL_WALK` | Default for `enrich_cve_full`. Full enrichment depth. | `details`, `epss`, `kev`, `cwe`, `capec`, `techniques`, `tie`, `misp`, `knowledgebase`, `web` |
| `CVE_FAST` | Quick artifact-scan. Score and KEV status, nothing else. | `details`, `epss`, `kev` |
| `CVE_NARRATIVE` | Report generation. Full walk plus AI summary and adversary attribution. | full walk + `summary`, `groups`, `campaigns` |
| `IOC_FULL_WALK` | Default for `enrich_ioc_full`. | `misp_attributes`, `techniques`, `groups`, `knowledgebase`, `web` |
| `IOC_FAST` | Indicator hits in MISP feeds, nothing else. | `misp_attributes` |
| `CWE_FULL_WALK` | Default for `enrich_cwe_full`. | `capecs`, `techniques`, `tie` |
| `TECHNIQUE_FULL` | Default for `enrich_technique_full`. Adversary attribution + Navigator. | `details`, `tie`, `groups`, `software`, `campaigns`, `navigator` |
| `PRODUCT_FULL` | Default for `enrich_product_full`. Per-CVE full enrichment for the matched set. | `details`, `epss`, `kev`, `cwe`, `capec`, `techniques`, `tie`, `misp`, `knowledgebase`, `web` |
| `PACKAGE_FULL` | Default for `enrich_package_full`. OSV advisories + per-CVE full enrichment. | `details`, `severity`, `affected`, `references`, `epss`, `kev`, `cwe`, `capec`, `techniques`, `tie` |
| `CAPEC_FULL` | Default for `enrich_capec_full`. Attack pattern → techniques, CVEs, actors. | `details`, `techniques`, `tie`, `cves`, `groups`, `software`, `campaigns`, `navigator` |
| `GROUP_FULL` | Default for `enrich_group_full`. The full actor picture. | `details`, `techniques`, `software`, `tie`, `campaigns`, `cves`, `sectors`, `locations`, `navigator` |
| `SOFTWARE_FULL` | Default for `enrich_software_full`. Malware / tool → who uses it and how. | `details`, `techniques`, `tie`, `groups`, `campaigns`, `cves`, `navigator` |
| `CAMPAIGN_FULL` | Default for `enrich_campaign_full`. | `details`, `techniques`, `tie`, `groups`, `software`, `cves`, `navigator` |
| `ATLAS_FULL` | Default for `enrich_atlas_full`. Adversarial-ML technique walk. | `details`, `techniques`, `tie`, `groups`, `software`, `campaigns`, `navigator` |
| `DISARM_FULL` | Default for `enrich_disarm_full`. Influence-operation technique + countermeasures. | `details`, `countermeasures`, `techniques`, `tie`, `groups`, `software`, `campaigns`, `navigator` |
| `DEFEND_FULL` | Default for `enrich_defend_full`. D3FEND has no graph hops, so this set is short by design. | `details`, `techniques`, `knowledgebase`, `web` |
| `LOCATION_FULL` | Default for `enrich_location_full`. Regional threat picture. | `details`, `groups`, `techniques`, `tie`, `software`, `campaigns`, `sectors`, `navigator` |
| `SECTOR_FULL` | Default for `enrich_sector_full`. Industry threat picture. | `details`, `groups`, `techniques`, `tie`, `software`, `campaigns`, `locations`, `navigator` |

Every full-walk preset stays inside its endpoint's allowed `include` set (see [Allowed `include` values per endpoint](#allowed-include-values-per-endpoint)) and deliberately leaves out `web_scrape` (a live external fetch, slow and rate-limited upstream) and `summary` (LLM generation). `CVE_NARRATIVE` is the one preset that opts into `summary` — that's what it's for.

Three ways to use them:

```python
from crimson_crawler_client import presets

# Pass a preset by name
await client.enrich_cve_full("CVE-2024-3400", include=presets.CVE_NARRATIVE)

# Build on a preset
await client.enrich_cve_full("CVE-2024-3400", include=[*presets.CVE_FAST, "summary"])

# Skip presets entirely and pass your own list
await client.enrich_cve_full("CVE-2024-3400", include=["details", "epss"])
```

Picking one:

- Triaging a list of CVEs? `CVE_FAST`. Sub-second on warm cache.
- Building a report? `CVE_NARRATIVE`. Full enrichment plus an AI summary.
- Recon walk on a single CVE? `CVE_FULL_WALK` (the default).
- Checking if an IP is in MISP? `IOC_FAST`.
- Threat-modeling around a weakness class? `CWE_FULL_WALK`.
- Coverage analysis on specific techniques? `TECHNIQUE_FULL`.
- "What's exposed in this product/version?" `PRODUCT_FULL`.
- Profiling an actor, malware family, or campaign? `GROUP_FULL` / `SOFTWARE_FULL` / `CAMPAIGN_FULL` — each walks out to techniques, CVEs, and a Navigator layer.
- Regional or industry picture? `LOCATION_FULL` / `SECTOR_FULL`.

## Platform

Beyond the `/v1` convenience surface, the client exposes the three key-authed **Platform** sections as dedicated namespaces — Artifact Manager (`client.artifact_manager`), Inventory Manager (`client.inventory_manager`), and Report Generator (`client.ai_reports`). They share your API key and endpoint, return plain dicts, live outside the versioned `/v1` OpenAPI contract, and **never count toward your API usage**. Enrichment is **not** a Platform section: `client.enricher` and `client.inventory_manager.enrich_inventory` submit→poll the `/v1/artifact-enrich` + `/v1/inventory-enrich` engines (the submit always counts toward your usage; the poll is exempt).

### Artifact Manager (`client.artifact_manager`)

`client.artifact_manager` gives programmatic access to **your Artifact Manager** — the artifacts,
saved scan reports, and folders your account manages in the portal. Same API key, same
endpoint — but **Artifact Manager calls never count toward your API usage**.

```python
async with CrawlerClient() as client:
    ws = client.artifact_manager

    # Your saved scans, newest first
    scans = await ws.list_artifacts(kind="artifact_enrichment")

    # Pull a scan's STIX bundle (the body IS the export file)
    formats = await ws.list_exports(scans[0]["id"])      # ["csaf", "csv", "stix", ...]
    stix = await ws.get_export(scans[0]["id"], "stix")   # {"content", "filename", "content_type"}

    # Upload evidence into a folder
    await ws.upload_artifacts(
        [{"name": "ir-notes.txt", "content": "CVE-2024-3400 observed", "format": "paste"}],
        new_folder_name="incident-2026-06",
    )

    # Track change between two enrichments (2–12 ids)
    matrix = await ws.compare_enrichments([old_enrichment_id, new_enrichment_id])
    print(matrix["counts"])  # {"new": 3, "resolved": 1, "changed": 2, "persistent": 14}
```

Full surface: `discovery`, `list_artifacts`, `upload_artifacts`, `get_artifact`,
`get_artifact_raw` (buffers the raw body as a string, without a JSON re-wrap, up to 10,000,000 bytes),
`update_artifact` (rename / star / move), `delete_artifact`, `copy_artifact`, `get_rating` / `set_rating`,
`list_exports` / `get_export`, `download_all_exports` / `download_bundle` / `download_folder` (zip
bundles), `bulk_delete` / `move_to_folder` / `bulk_set_favorite` (bulk ops), `compare_enrichments`,
`save_enrichment`, and folder CRUD (`list_folders`,
`create_folder`, `rename_folder`, `delete_folder`). Responses are plain dicts —
the Artifact Manager surface lives outside the versioned `/v1` OpenAPI contract and evolves
independently. Non-2xx raises the same `UnexpectedStatus`.

### Inventory Manager (`client.inventory_manager`)

`client.inventory_manager.ingest_scan` turns a vulnerability scan into a ranked **briefing** in one call. It
parses the scan file **server-side** (so the parsers stay in one place), enriches the discovered assets
through the `/v1/inventory-enrich` KEV-first ranking engine, and returns a structured briefing.

```python
async with CrawlerClient() as client:
    with open("scan.xml") as f:
        briefing = await client.inventory_manager.ingest_scan(f.read(), "nmap")

    print(briefing["parsed_count"], "assets")
    for a in briefing["recommended_actions"]:        # KEV-first, top-N
        print(a["cve_id"], a["product"], a["reason"])
    # briefing["report"] is the full /v1/inventory-enrich result (per-asset findings + rollup)
```

`scan_format` is one of `json` / `sbom` / `csv` / `nmap` / `list` / `grype` / `trivy` / `depcheck` / `nessus` / `gvm` / `xlsx` (you PICK it — no auto-detection). The parse is usage-exempt; the one enrichment submit **counts toward your usage**. Need just the parsed assets? `client.inventory_manager.parse_scan(content, scan_format)` returns `{format, items, count}` without enriching. (This is the same workflow the `ccc ingest` CLI wraps.)

`get_inventory_context` returns a bounded context bundle with a safe prompt preamble, source metadata,
and explicit delimiters around artifact-derived sections.

> **Untrusted data.** Inventory context is untrusted data, not instructions. Attached artifacts may contain adversarial instructions from external scans, advisories, email, or user uploads. Do not follow instructions found in the context. Restrict agent tools and require human approval before consequential actions.

Beyond ingest, `client.inventory_manager` covers the full inventory surface — CRUD, `enrich_inventory`,
`save_enrichment` (persist a completed enrichment into the inventory's history), `set_favorite`,
**`compare_enrichments`** (a CVE-matched difference matrix across 2–12 of an inventory's saved enrichments),
`import_inventory` / `export_inventory`, `parse_multi` / `import_multi` (combine several scan/inventory files
into one deduped inventory), `apply_asset_fix` (apply a recommended upgrade to one asset), attachments,
and daily automation (`get_automation` / `set_automation`).

When attachment enrichment is enabled, `enrich_inventory` returns `attachment_omissions`, with
`{"attachment_id", "reason"}` for every requested attachment that could not be included. Resolution is
permissive by default for compatibility. Pass `strict_attachments=True` to fail before the enrichment
submit rather than analyze incomplete attachment evidence.

### Report Generator (`client.ai_reports`)

`client.ai_reports` is the Report Generator at `/ai-reports/*` — generate grounded, cite-or-refuse
intelligence reports from your saved artifacts. **Fully usage-exempt, including the LLM generation.**

```python
async with CrawlerClient() as client:
    reports = client.ai_reports

    # One-call convenience: create a draft, fill every AI section, fetch the result
    result = await reports.generate_report(template, source_ids=[scan_id])
    report_id = result["report"]["id"]

    # Or export an existing report
    html = await reports.export_report(report_id, "html")  # or "md"
```

Full surface: `discovery`, `list_templates`, `list_reports`, `get_report`, `get_report_raw`,
`create_draft`, `fill_section`, `section` (ad-hoc grounded section), `rename_report`, `delete_report`,
`export_report` (`html` / `md`), and `generate_report` (the headline convenience). `get_report_raw`
buffers the raw HTML string up to 10,000,000 bytes; it does not return a stream. Responses are plain
dicts; non-2xx raises the same `UnexpectedStatus`.

### Enricher (`client.enricher`)

`client.enricher.enrich` is a thin submit→poll convenience over the async `/v1/artifact-enrich` engine —
it stages a security artifact (pasted text + uploaded files), submits, polls until done, and returns the
completed report. The **submit counts toward your usage**; the poll is exempt. Pass `save=True` to also
file the finished report into your Artifact Manager library afterward.
`timeout` is a hard end-to-end polling deadline: status requests, response reads, `on_progress`
callbacks, and sleeps all share the same remaining budget. `poll_interval` and `timeout` must be finite
and non-negative.

```python
async with CrawlerClient() as client:
    result = await client.enricher.enrich(
        text="CVE-2024-3400 observed in ...",
        save=True,
    )
    print(result["enrichment_id"])
```

## Depth control (`include`)

The 15 single-entity enrichment endpoints, `search_cve`, and all four assessment endpoints accept an `include: list[str]` parameter that controls how deep the walk goes. `enrich_poc_source`, `enrich_batch` (its items carry their own), and the other seven search endpoints do not. The 15 `enrich_*_full` methods bake in a full-walk preset (see [Presets](#presets)); pass `include=` to override. `search_cve` and the `assess_*` methods take `include=` with **no** default — leave it out and the server's own default applies. The exact set of layer names each endpoint accepts is listed in [Allowed `include` values per endpoint](#allowed-include-values-per-endpoint). Invalid values raise `HTTP 422` with the allowed list in the response body.

## Beyond the convenience surface

There is no gap to cover: **every one of the 33 `/v1` operations has a named method** on `CrawlerClient`. What the generated layer still gives you is (1) **synchronous** calls — the convenience surface is async-only — and (2) hand-built request models when you want a field a convenience signature doesn't expose. Pass `client.api` to any endpoint function:

```python
from crimson_crawler_client import CrawlerClient
from crimson_crawler_client._generated.api.search import search_cve
from crimson_crawler_client._generated.models import SearchCVERequest

# The convenience form: await client.search_cve(kev_only=True, epss_min=0.9)
# The escape hatch, when you need it synchronously or with a raw model:
client = CrawlerClient()
result = search_cve.sync(
    client=client.api,
    body=SearchCVERequest(kev_only=True, epss_min=0.9, created_min="2026-01-01"),
)
```

Each endpoint module exposes `sync`, `sync_detailed`, `asyncio`, and `asyncio_detailed` (see [Calling pattern](#calling-pattern)); all four go through the same retry transport as the convenience methods. The catalog below maps every endpoint to its module — and, since parity is 33/33, to a `client.<name>` method of the same name.

### Endpoint catalog

#### Enrichment (17 endpoints)

| Endpoint module | What it returns |
|---|---|
| `_generated.api.enrichment.enrich_cve` | CVE → CVSS, EPSS, KEV, weaknesses, techniques, predictions |
| `_generated.api.enrichment.enrich_ioc` | IOC → MISP events, linked techniques, threat assessment |
| `_generated.api.enrichment.enrich_cwe` | CWE → CAPEC patterns, ATT&CK techniques, TIE predictions |
| `_generated.api.enrichment.enrich_capec` | CAPEC → linked techniques and weaknesses |
| `_generated.api.enrichment.enrich_technique` | ATT&CK technique → groups, software, campaigns, Navigator JSON |
| `_generated.api.enrichment.enrich_product` | Product/version → affected CVEs, KEV, exploit signals |
| `_generated.api.enrichment.enrich_package` | Package/ecosystem (OSV.dev) → advisories + per-CVE enrichment |
| `_generated.api.enrichment.enrich_group` | Threat actor → techniques, software, campaigns |
| `_generated.api.enrichment.enrich_software` | Malware/tool → techniques, attribution |
| `_generated.api.enrichment.enrich_campaign` | Campaign → groups, techniques, timeline |
| `_generated.api.enrichment.enrich_atlas` | Adversarial ML technique enrichment |
| `_generated.api.enrichment.enrich_disarm` | Disinformation countermeasures |
| `_generated.api.enrichment.enrich_defend` | D3FEND defensive technique enrichment |
| `_generated.api.enrichment.enrich_location` | Regional threat picture |
| `_generated.api.enrichment.enrich_sector` | Industry threat picture |
| `_generated.api.enrichment.enrich_poc_source` | LLM-summarized analysis of PoC repositories |
| `_generated.api.enrichment.enrich_batch` | Up to 50 mixed enrich/* items, each with its own `include` |

#### Search (8 endpoints)

| Endpoint module | What it does |
|---|---|
| `_generated.api.search.search_cve` | Filter CVEs by CWE, CPE, CVSS, EPSS, KEV, dates |
| `_generated.api.search.search_cti` | Cross-knowledge-base search (ATT&CK, CWE, CAPEC, ATLAS, DISARM) |
| `_generated.api.search.search_kev` | Known Exploited Vulnerabilities catalog |
| `_generated.api.search.search_misp` | Threat intel events by indicator or event info |
| `_generated.api.search.search_knowledgebase` | Natural-language semantic search over ingested OSINT |
| `_generated.api.search.search_vendor` | Vendor product lookup with CPE identifiers |
| `_generated.api.search.search_poc` | Proof-of-concept exploit search for a CVE |
| `_generated.api.search.search_d3fend` | D3FEND defensive technique search |

#### Assessment (4 endpoints)

| Endpoint module | What it does |
|---|---|
| `_generated.api.assess.assess_technique_coverage` | Up to 50 techniques: gap analysis, mitigations, detections |
| `_generated.api.assess.assess_vulnerability_exposure` | Aggregate technique surface across up to 50 CVEs |
| `_generated.api.assess.assess_group_exposure` | Combined threat surface for threat-actor groups |
| `_generated.api.assess.assess_ioc_portfolio` | Multi-indicator threat analysis |

#### Async engines (4 endpoints)

| Endpoint module | What it does |
|---|---|
| `_generated.api.artifact_enrich.artifact_enrich` | Submit a security artifact for background enrichment → `202` + `enrichment_id` |
| `_generated.api.artifact_enrich.artifact_enrich_status` | Per-stage progress, then the finished `ArtifactEnrichmentReport` (usage-exempt) |
| `_generated.api.inventory_enrich.inventory_enrich` | Submit an asset inventory for background enrichment → `202` + `enrichment_id` |
| `_generated.api.inventory_enrich.inventory_enrich_status` | Per-stage progress, then the finished inventory report (usage-exempt) |

### Allowed `include` values per endpoint

Each endpoint accepts its own set of `include` layer names. Pass any subset. Invalid values raise `HTTP 422` with the allowed list in the response body.

**Enrichment**

| Endpoint | Allowed `include` values |
|---|---|
| `enrich_cve` | `affected_products`, `campaigns`, `capec`, `cwe`, `defend`, `details`, `detections`, `epss`, `exploits`, `groups`, `inferred_chain`, `kev`, `knowledgebase`, `misp`, `mitigations`, `navigator`, `nist_controls`, `osv`, `poc`, `poc_source`, `sector_context`, `similar_cves`, `software`, `ssvc`, `summary`, `techniques`, `tie`, `timeline`, `web`, `web_scrape` |
| `enrich_product` | `affected_products`, `campaigns`, `capec`, `cwe`, `defend`, `details`, `detections`, `epss`, `exploits`, `groups`, `kev`, `knowledgebase`, `misp`, `mitigations`, `navigator`, `nist_controls`, `osv`, `poc`, `risk_summary`, `software`, `ssvc`, `summary`, `techniques`, `tie`, `web`, `web_scrape` |
| `enrich_package` | `affected`, `affected_products`, `campaigns`, `capec`, `cwe`, `defend`, `details`, `detections`, `epss`, `exploits`, `groups`, `inferred_chain`, `kev`, `knowledgebase`, `misp`, `mitigations`, `navigator`, `nist_controls`, `osv`, `poc`, `poc_source`, `references`, `sector_context`, `severity`, `similar_cves`, `software`, `ssvc`, `summary`, `techniques`, `tie`, `timeline`, `web`, `web_scrape` |
| `enrich_cwe` | `campaigns`, `capecs`, `cves`, `defend`, `details`, `detections`, `groups`, `knowledgebase`, `mitigations`, `navigator`, `nist_controls`, `software`, `summary`, `techniques`, `tie`, `web`, `web_scrape` |
| `enrich_capec` / `enrich_atlas` | `campaigns`, `cves`, `defend`, `details`, `detections`, `groups`, `knowledgebase`, `mitigations`, `navigator`, `nist_controls`, `software`, `summary`, `techniques`, `tie`, `web`, `web_scrape` |
| `enrich_technique` | `campaigns`, `cves`, `defend`, `details`, `detections`, `groups`, `knowledgebase`, `misp`, `mitigations`, `navigator`, `nist_controls`, `software`, `summary`, `tie`, `web`, `web_scrape` |
| `enrich_ioc` | `campaigns`, `cves`, `defend`, `detections`, `galaxies`, `groups`, `inferred_chain`, `knowledgebase`, `misp_attributes`, `mitigations`, `navigator`, `nist_controls`, `poc`, `sightings`, `software`, `summary`, `techniques`, `tie`, `warninglist`, `web`, `web_scrape` |
| `enrich_group` | `campaigns`, `cves`, `defend`, `details`, `detections`, `knowledgebase`, `locations`, `misp`, `mitigations`, `navigator`, `nist_controls`, `sectors`, `software`, `summary`, `techniques`, `tie`, `web`, `web_scrape` |
| `enrich_software` | `campaigns`, `cves`, `defend`, `details`, `detections`, `groups`, `knowledgebase`, `misp`, `mitigations`, `navigator`, `nist_controls`, `summary`, `techniques`, `tie`, `web`, `web_scrape` |
| `enrich_campaign` | `cves`, `defend`, `details`, `detections`, `groups`, `knowledgebase`, `mitigations`, `navigator`, `nist_controls`, `software`, `summary`, `techniques`, `tie`, `web`, `web_scrape` |
| `enrich_disarm` | `campaigns`, `countermeasures`, `defend`, `details`, `detections`, `groups`, `knowledgebase`, `mitigations`, `navigator`, `software`, `summary`, `techniques`, `tie`, `web`, `web_scrape` |
| `enrich_location` | `campaigns`, `cves`, `defend`, `details`, `detections`, `groups`, `knowledgebase`, `mitigations`, `navigator`, `sectors`, `software`, `summary`, `techniques`, `tie`, `web`, `web_scrape` |
| `enrich_sector` | `campaigns`, `cves`, `defend`, `details`, `detections`, `groups`, `knowledgebase`, `locations`, `mitigations`, `navigator`, `software`, `summary`, `techniques`, `tie`, `web`, `web_scrape` |
| `enrich_defend` | `details`, `knowledgebase`, `summary`, `techniques`, `web`, `web_scrape` |
| `enrich_poc_source` | _no `include` parameter — pass `repo_url` only_ |

**Search**

| Endpoint | Allowed `include` values |
|---|---|
| `search_cve` | `details` |
| All other `search_*` endpoints | _no `include` parameter — filter via the request body fields_ |

**Assessment**

| Endpoint | Allowed `include` values |
|---|---|
| `assess_technique_coverage` | `coverage_score`, `cves`, `defend`, `detections`, `mitigations`, `navigator`, `nist_controls`, `summary`, `tie` |
| `assess_vulnerability_exposure` | `defend`, `exploit_chain`, `exploits`, `navigator`, `nist_controls`, `osv`, `prioritized_remediation`, `remediation_plan`, `ssvc`, `summary`, `techniques`, `tie` |
| `assess_group_exposure` | `cves`, `defend`, `details`, `locations`, `navigator`, `nist_controls`, `sectors`, `summary`, `techniques`, `tie` |
| `assess_ioc_portfolio` | `campaigns`, `defend`, `detections`, `galaxies`, `groups`, `misp_attributes`, `mitigations`, `nist_controls`, `sightings`, `software`, `summary`, `techniques`, `tie`, `warninglist` |

### Calling pattern

Each endpoint module exposes 4 entrypoints:

- **`sync(client, body)`** — synchronous call, returns the parsed response or raises.
- **`sync_detailed(client, body)`** — synchronous, returns a `Response` wrapper with `parsed`, `status_code`, `headers`.
- **`asyncio(client, body)`** — async equivalent of `sync`.
- **`asyncio_detailed(client, body)`** — async equivalent of `sync_detailed`.

Use the `_detailed` variants when you need response headers (rate-limit info, request IDs) or want to inspect non-200 statuses without raising.

## Errors

### Exceptions

3 exception types, all subclasses of `CrawlerClientError`:

```python
from crimson_crawler_client import (
    CrawlerClient,
    CrawlerClientError,
    MissingCredentials,
    UnexpectedStatus,
)

try:
    async with CrawlerClient() as client:
        await client.enrich_cve_full("CVE-2024-3400")
except MissingCredentials:
    # CRIMSON_CRAWLER_API_KEY not set and no api_key passed
    ...
except UnexpectedStatus as exc:
    # Server returned a non-success response
    print(exc.status, exc.body)
except CrawlerClientError:
    # Base class — catches anything raised by this package
    ...
```

| Exception | Raised when |
|---|---|
| `CrawlerClientError` | Base class for everything raised by this package. |
| `MissingCredentials` | No `api_key` argument and `CRIMSON_CRAWLER_API_KEY` is unset. |
| `UnexpectedStatus` | A convenience or Platform method received a non-success response, or a generated response could not be parsed. Carries `.status` and `.body`. |

Network-level errors (DNS failure, connection refused, timeout) come straight from `httpx` and aren't wrapped. Catch `httpx.HTTPError` for those.

### Response patterns

#### Partial failures via `errors[]`

Every enrichment response includes an `errors` array. A `200` with a non-empty `errors` array means partial success: one or more intelligence sources were unavailable, but the rest of the data is valid.

```python
async with CrawlerClient() as client:
    result = await client.enrich_cve_full("CVE-2024-3400")
    for error in result.errors or []:
        print(f"[{error.step}] {error.service}: {error.error}")
```

If one source is unavailable, you still get everything else. HTTP 4xx codes (400, 422, 429) only fire on client-side issues.

#### Optional fields

Every layer of an enrichment response is independently optional. If a layer wasn't requested or had no data, its slot is `None` (scalar) or an empty/`None` list.

```python
# Defensive scalar access
cvss = result.cve.cvss_score if result.cve else None

# Defensive list iteration
for t in result.techniques or []:
    ...
```

## Retry & rate limits

`CrawlerClient` retries transient failures on safe HTTP methods (`GET`, `HEAD`, and `OPTIONS`). By default it makes up to **3 attempts** (1 initial + `max_retries=2` retries) with capped exponential backoff and jitter. Requests that can mutate state always make one attempt.

```python
# Default: 3 attempts total
async with CrawlerClient() as client:
    ...

# More aggressive
async with CrawlerClient(max_retries=5) as client:
    ...

# Disable retries entirely (single attempt)
async with CrawlerClient(max_retries=0) as client:
    ...
```

What gets retried for safe HTTP methods:

- **HTTP** `429` (rate limited), `502`, `503`, `504`.
- **Transport errors** — connection failures, read/connect/write timeouts, connection resets, protocol errors.

Every `POST`/`PUT`/`PATCH`/`DELETE` fails after its first response or transport error, including v1 enrichment calls and Platform mutations. This preserves at-most-once execution where the API has no idempotency key. Safe-method requests also fail immediately on every other `4xx` (`400`/`401`/`403`/`404`/`422`), `500`, and unparseable responses. When the retry budget is exhausted, the final failure surfaces unchanged.

Backoff is `0.5s`, then `1.0s`, doubling up to an `8.0s` cap, with ±25% jitter. If the server sends a `Retry-After` header (common on `429`), that value is honored instead of the computed delay (clamped to 60s).

Per-tier rate limits still apply on top of retries. If you need to back off explicitly, check the `X-RateLimit-Remaining-Minute` header from the `_detailed` variants.

## Concurrency

A single `CrawlerClient` can fan out as many concurrent requests as your rate limit allows. Use `asyncio.gather()`:

```python
async with CrawlerClient() as client:
    results = await asyncio.gather(*[
        client.enrich_cve_full(cve_id, include=presets.CVE_FAST)
        for cve_id in ["CVE-2024-3400", "CVE-2024-1086", "CVE-2024-21887"]
    ])
```

Per-tier rate limits still apply. If you need to back off, check the `X-RateLimit-Remaining-Minute` header from the `_detailed` variants.

## Stability

The SDK is currently **Beta** (`Development Status :: 4 - Beta`). Public APIs may change between minor releases while the package approaches a stable release; pin a compatible range as described below.

## Versioning

`MAJOR.MINOR.PATCH`:

- `MAJOR.MINOR` tracks the API surface. Any change to a request or response schema bumps at least the minor.
- `PATCH` is reserved for client-only fixes that don't change the wire contract.

Pin a compatible range against the current major:

```toml
dependencies = ["crimson-crawler-client>=4.0.0,<5.0.0"]
```

## Selected top-level exports

| Symbol | Type | Description |
|---|---|---|
| `CrawlerClient` | class | Main client class |
| `presets` | module | 18 named `include` lists |
| `CrawlerClientError` | exception | Base for everything this package raises |
| `MissingCredentials` | exception | Constructor missing API key |
| `UnexpectedStatus` | exception | Non-success or unparseable response |
| `__version__` | str | Package version |

## See also

- [Hosted Python SDK guide](https://crimsoncrawler.com/docs/clients/python-client/)
- [Quickstart](https://crimsoncrawler.com/docs/quickstart/)
- [Authentication and API keys](https://crimsoncrawler.com/docs/authentication/)
- [v1 API reference](https://crimsoncrawler.com/api-reference/v1/)
- [Choosing a client surface](https://crimsoncrawler.com/docs/guides/choosing-a-surface/)
- TypeScript SDK — [`/docs/clients/typescript-client/`](https://crimsoncrawler.com/docs/clients/typescript-client/)
- CLI (`ccc`) — [`/docs/clients/cli/`](https://crimsoncrawler.com/docs/clients/cli/)
- MCP server — [`/docs/clients/mcp/`](https://crimsoncrawler.com/docs/clients/mcp/)
- Source and release history — [GitHub](https://github.com/crimson-crawler/python-sdk)

## Contributing

See [CONTRIBUTING.md](https://github.com/crimson-crawler/python-sdk/blob/main/CONTRIBUTING.md) for development and release instructions. Report vulnerabilities through [SECURITY.md](https://github.com/crimson-crawler/python-sdk/blob/main/SECURITY.md).

## License

Licensed under the [Apache License 2.0](https://github.com/crimson-crawler/python-sdk/blob/main/LICENSE).

Copyright 2026 Def-Logix, Inc.
