openapi: 3.1.0 info: title: Harpia API version: "1.0.0" summary: CVE exploitation intelligence - search, CLI inventory matching, per-CVE records, live decision frameworks, and delta sync. description: | Harpia turns raw vulnerability data into **exploitation intelligence**: which CVEs are exploited in the wild, weaponized, or have public PoCs — and, via the selectable SSVC and BOD 26-04 decision frameworks. ## Access surfaces - **Bearer API** (`Authorization: Bearer `) — the programmatic path. Mint keys from a signed-in browser session (`POST /api/v1/keys`), then use them here. - **Cookie session** (`__spa_tok`, HttpOnly) — the web app. Powers `/auth/*`, key management, and usage views. Not for programmatic clients. - **Public** — `GET /taxii2/` (TAXII discovery) and `GET /api/v1/health` only. Direct **anonymous** API calls are not supported: keyless requests must carry the SPA fetch markers (they come from the site). Create a free account for a key. ## Tiers Every route has a minimum tier (`x-min-tier`: `anon` < `free` < `pro` < `enterprise`). Above the plan → `403`. Response **fields** are also tier-stripped (e.g. `ransomware_use`/`ttps` are pro+), and a stripped field loses its facet count too, so summaries never leak what rows hide. The authoritative limits live in CONFIGURATION.md. ## Conventions - Times are UTC. Dates are `YYYY-MM-DD`. - Bulk calls count as **one** request. CVE and decision bodies cap at 32 KB, ordinary calls at 256 KB, and inventory matching at 2 MiB. - Every account carries **two independent budgets**: your API keys share one (`scope: api_key`) and your web session has another (`scope: web_session`). Browsing the site never spends API-key quota. Each is a token bucket plus a daily cap that resets at midnight UTC. Honour `Retry-After` on `429`, and read `scope` in the body to see which budget ran out. - Metered responses carry `X-Tier`, `X-Quota-Scope`, `X-RateLimit-Limit`, `X-Daily-Limit` and `X-Daily-Remaining` so you can pace without tripping the limit. - CVE lookup and search are reachable signed out, stripped to public basics (maturity ladder, CVSS, severity, CWE, EPSS, the exploit-evidence count, fix status). Decisions, references, repositories, bulk and TAXII need an account or a plan and answer `401`/`403` with the tier that unlocks them. contact: name: Harpia support email: support@harpia.ae url: https://www.harpia.ae/contact license: name: Terms of Service url: https://www.harpia.ae/tos servers: - url: https://api.harpia.ae description: Programmatic (Bearer) API host - url: https://www.harpia.ae description: Web-app host (same API; also serves the site + session routes) security: - bearerAuth: [] - cookieAuth: [] tags: - name: Search description: Full boolean query language over the CVE corpus. - name: Lookup description: Per-CVE records and their sub-resources. - name: Decision frameworks description: Live, context-aware "what should we do" decisions. - name: Inventory description: Compact PURL and CPE coordinate matching for the Harpia CLI (pro+). - name: Bulk & sync description: Batch lookups and delta sync for building a local mirror (pro+). - name: Account description: API-key management (cookie session only). - name: Session description: Web-app identity and usage (cookie session only). - name: System description: Health and plan metadata. - name: TAXII description: STIX 2.1 / TAXII 2.1 threat-intel feed. paths: /api/v1/vulnerabilities/search: get: tags: [Search] summary: Search the CVE corpus operationId: search x-min-tier: anon description: | Full boolean [query language](https://www.harpia.ae/api#query) in `term`. **AND is implicit** between adjacent terms; `OR`, `NOT`/`-`, `"phrases"`, and `( )` are supported (max 64 tokens, 16 nesting levels). Ordinary searches return CVEs and malicious-package findings. Advisory restatements are suppressed to avoid duplicate, sparsely enriched rows. Enter an advisory ID such as `RHSA-2026:56097` to resolve it to the CVE or CVEs it covers. Use `type:osv_advisory` only when the advisory records themselves are required. **Qualifiers** (exact structured matches): - Numeric (`>=` default, plus `> >= < <=` and `lo..hi`): `epss:`, `cvss:`, `percentile:`, `exploits:`, `refs:`, `stars:`, `forks:`. - Flags (`true`/`false`): `exploited:`, `kev:`, `ransomware:`, `weaponized:`, `automatable:`, `fixable:`, `malicious:`, `trending:`, `rising:`. - `has:` shorthands: `has:exploit|poc|kev|ransomware|weaponized|malicious|refs|repo|fix`. - Software (siblings match the same affected row): `vendor:`, `product:`, `ecosystem:`, `version:<3.0`, `purl:`, `cpe:`. - Weakness: `cwe:79` · `cwe:CWE-416` · class names (`cwe:rce,xss`). - **Time**: `age:<30` (days by publication) · `published:2024` · `modified:>2026-07-13` (record's own last-modified) · `added:>2025-01-01` (CISA KEV date-added). `published/modified/added` take a **date or year** (`2024`, `2024-06-01`, `>2024-06-01`, `lo..hi`) — a non-date like `modified:<7` is ignored. `modified:` is narrower than `/vulnerabilities/changes` (which also counts repo/ref activity). - `status:`, `maturity:none|poc|weaponized|active`, `alias:GHSA-…`. - **Attribution (pro+)**: `actor:` (word match — `actor:qilin`, `actor:apt29`), `origin:` (actor origin) and `target:` (victim geography) each accept an ISO-2 code, the full country name, or a partial word - `origin:RU` = `origin:russia`, `target:AE` = `target:"united arab emirates"`, `target:saudi` -> SA, `origin:korea` -> KP+KR, all matched against the stored code(s), `sector:`, `motivation:`, and the ATT&CK views `ttp:`/`technique:`, `tactic:`, `capec:` (ID-shaped values match the id exactly, anything else matches the display name). - **Observed probing** (honeypot telemetry, not victimology): `observed_in:AE`, `observed_from:CN`, `observed:>100` (unique probing hosts), `observed_days:>10`, `scanning:up|down|flat` (7d vs prior 7d). - **EPSS movement**: `epss_change:`/`epss_7d:`, `epss_30d:`, `epss_trend:up|down|flat`. - **Trending**: `trend:trending|rising|active|stagnant`, `trend_score:>30`, `risk:>1.5` (the risk scale is 0-2). - **Evidence**: `ref_type:exploited|poc|weapon|reference`, `ref_source:exploitdb`, `ref_since:>2026-07-01`. - **Repositories**: `repo_stars:>500`, `repo_role:exploit|weapon|scanner|detection|writeup|patch|lab`, `repo_lang:python`, `repo_since:>2026-07-01` (newly created exploit code), `suspicious:true`. - **Vendor advisories**: `advisory:DSA-5020-1`, `issuer:debian`. - **SSVC (CISA)**: `ssvc:active|poc|none`, `ssvc_automatable:`. - **CVSS provenance**: `cvss_version:4.0`, `attack_vector:network|adjacent|local|physical` (alias `av:`), `nvd_verified:`. - **Automation / TTE**: `initial_access:`, `post_exploit:`, `tte:<7` (days to the first public exploit artifact: weaponized code or a KEV listing), `zeroday:`. - **Arsenal (pro+)**: `malware:plugx`, `tool:psexec` — matched via the attributed actors. `malware:true` keeps its published meaning, "is a malicious-package advisory". - **Record shape**: `type:cve|osv_advisory|malicious_package`, `assigner:cve@mitre.org`, `cluster:CVE-2021-44228` (near-duplicates). - Aliases: `pkg:`/`package:`→`purl:`, `sev:`→`severity:`, `eco:`→`ecosystem:`, `ver:`→`version:`, `av:`→`attack_vector:`, `epss_delta:`→`epss_change:`, `0day:`→`zeroday:`. - Negation is `NOT`, `-` or `!`, and binds to a qualifier, a phrase or a parenthesised group: `kev:true -ssvc:active`, `-(kev:true OR trend:trending)`. - An unregistered qualifier, or a wrong-typed value, is a **400** naming the problem — never a silent `200 total=0`. - Input is normalised before parsing (NFKC, invalid UTF-8 dropped, zero-width and bidi characters removed, Unicode spaces collapsed, an en/em dash INSIDE a word folded to a hyphen so a typeset `CVE-2026–59310` still resolves; a space-flanked dash is left alone because `-` alone negates). Empty `term` matches nothing (no full-table dump). **Attribution qualifiers are tier-gated.** Filtering on a field your plan strips leaks it by inference, so a query whose compiled plan joins an attribution table is a `403`, not a silently dropped term. The gate reads the compiled plan, not the raw text: a quoted phrase like `"threat actor: qilin"` joins nothing and stays open to every tier. parameters: - $ref: '#/components/parameters/Term' - $ref: '#/components/parameters/Page' - $ref: '#/components/parameters/Limit' - name: sort in: query schema: { type: string, enum: [epss, cvss, exploits, published, trending], default: epss } description: Result ordering (not text relevance). Invalid → `epss`. - $ref: '#/components/parameters/Severity' - $ref: '#/components/parameters/Exploited' - $ref: '#/components/parameters/Weaponized' - $ref: '#/components/parameters/Ransomware' - $ref: '#/components/parameters/CisaKev' - $ref: '#/components/parameters/Malicious' - name: decision in: query schema: { type: string, enum: [immediate, out_of_cycle, scheduled, defer, 3_days, 14_days, 60_days, fix_on_system_upgrade] } description: Server-side outcome filter. SSVC accepts priority values; BOD 26-04 accepts timeline values. `total` and pages scope to it. - name: framework in: query schema: { type: string, enum: [harpia_ssvc, bod_26_04], default: harpia_ssvc } description: Framework used by `decision`. Required for BOD timeline filters. - name: exposure in: query schema: { type: string, enum: [exposed, controlled, not_exposed], default: exposed } - name: mission_consequence in: query schema: { type: string, enum: [critical, high, moderate, low], default: critical } responses: '200': description: Matches with facets and an exploitation-posture summary. content: application/json: schema: { $ref: '#/components/schemas/SearchResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/AttributionForbidden' } '429': { $ref: '#/components/responses/RateLimited' } /api/v1/decisions/summary: get: tags: [Search, Decision frameworks] summary: Aggregate SSVC priorities or BOD 26-04 timelines for a query operationId: searchDecisions x-min-tier: pro description: | Counts each selected-framework outcome across the **entire** match set of a query, not just one page. `harpia_ssvc` returns priority counts. `bod_26_04` returns remediation timeline counts. Same query params as `/vulnerabilities/search`; it reads the identical input and compiles the identical query language, including the panel filters. Use it to size a remediation backlog before pulling records. The same attribution gate as `/vulnerabilities/search` applies: this endpoint accepts the same qualifiers, so an attribution filter would leak the stripped fields by inference through the counts. parameters: - $ref: '#/components/parameters/Term' - $ref: '#/components/parameters/Severity' - $ref: '#/components/parameters/Exploited' - $ref: '#/components/parameters/Weaponized' - $ref: '#/components/parameters/Ransomware' - $ref: '#/components/parameters/CisaKev' - $ref: '#/components/parameters/Malicious' - name: framework in: query schema: { type: string, enum: [harpia_ssvc, bod_26_04], default: harpia_ssvc } description: Selects the outcome vocabulary returned in `counts`. - name: exposure in: query schema: { type: string, enum: [exposed, controlled, not_exposed], default: exposed } - name: mission_consequence in: query schema: { type: string, enum: [critical, high, moderate, low], default: critical } responses: '200': description: Decision counts over the whole result set. content: application/json: schema: type: object properties: total: { type: integer, examples: [1240] } framework: { type: string, enum: [harpia_ssvc, bod_26_04] } inputs: { type: object, additionalProperties: true } counts: type: object additionalProperties: { type: integer } description: SSVC keys are immediate, out_of_cycle, scheduled, defer. BOD keys are 3_days, 14_days, 60_days, fix_on_system_upgrade. '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/AttributionForbidden' } '429': { $ref: '#/components/responses/RateLimited' } /api/v1/vulnerabilities/{id}: get: tags: [Lookup] summary: Full record for one CVE or advisory operationId: getCVE x-min-tier: anon description: | The complete CVE 5.1-shaped record plus Harpia's `x_intel_priority` intel block. Reference and repository arrays are summarised to **counts** here — fetch them via the sub-resources. `id` accepts CVE and advisory IDs (`MAL-`, `GHSA-`, `RUSTSEC-`, `PYSEC-`, `GO-`, `RHSA-`, `USN-`, `DSA-`, …). parameters: - $ref: '#/components/parameters/CVEId' responses: '200': description: The CVE record. content: application/json: schema: { $ref: '#/components/schemas/CVERecord' } '404': { $ref: '#/components/responses/NotFound' } /api/v1/vulnerabilities/{id}/software: get: tags: [Lookup] summary: Affected software for a CVE operationId: getCVESoftware x-min-tier: free description: Vendor/product/ecosystem rows with version ranges and fixed versions. Paginated. parameters: - $ref: '#/components/parameters/CVEId' - $ref: '#/components/parameters/SubPage' - $ref: '#/components/parameters/SubLimit' responses: '200': description: Paginated affected-software rows. content: application/json: schema: { $ref: '#/components/schemas/PaginatedRows' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /api/v1/vulnerabilities/{id}/references: get: tags: [Lookup] summary: Reference URLs for a CVE (pro+) operationId: getCVEReferences x-min-tier: pro description: > Advisories, write-ups, exploit posts and patch links, each with the source's own `published` date. Ordered oldest first, so a page reads as the evidence timeline for the CVE. Paginated. parameters: - $ref: '#/components/parameters/CVEId' - $ref: '#/components/parameters/SubPage' - $ref: '#/components/parameters/SubLimit' responses: '200': description: Paginated reference rows, with the whole array's composition. content: application/json: schema: { $ref: '#/components/schemas/PaginatedReferences' } '403': { $ref: '#/components/responses/Forbidden' } /api/v1/vulnerabilities/{id}/repositories: get: tags: [Lookup] summary: Public exploit repositories for a CVE (pro+) operationId: getCVERepositories x-min-tier: pro description: Public PoC/exploit repositories with stars, forks, and confidence metadata. Paginated. parameters: - $ref: '#/components/parameters/CVEId' - $ref: '#/components/parameters/SubPage' - $ref: '#/components/parameters/SubLimit' responses: '200': description: Paginated repository rows, with the whole array's composition. content: application/json: schema: { $ref: '#/components/schemas/PaginatedRepositories' } '403': { $ref: '#/components/responses/Forbidden' } /api/v1/vulnerabilities/{id}/decision: get: tags: [Decision frameworks] summary: Live Harpia SSVC or CISA BOD 26-04 decision operationId: getCVEDecision x-min-tier: free description: | Select one complete framework. `harpia_ssvc` returns an SSVC priority and its matching timeline. `bod_26_04` returns the CISA timeline and forensic-triage requirement. Computed live on every request. parameters: - $ref: '#/components/parameters/CVEId' - name: framework in: query schema: { type: string, enum: [harpia_ssvc, bod_26_04], default: harpia_ssvc } - name: exposure in: query schema: { type: string, enum: [exposed, controlled, not_exposed], default: exposed } - name: mission_consequence in: query schema: { type: string, enum: [critical, high, moderate, low], default: critical } - name: discovered in: query schema: { type: string, format: date } description: Optional; anchors the remediation clock. responses: '200': description: The live decision. content: application/json: schema: { $ref: '#/components/schemas/DecisionResponse' } '404': { $ref: '#/components/responses/NotFound' } /api/v1/vulnerabilities/lookup: post: tags: [Bulk & sync] summary: Batch CVE lookup (pro+) operationId: bulkGetCVE x-min-tier: pro description: EPSS, exploit maturity, and KEV status for up to **500** IDs, one request. requestBody: { $ref: '#/components/requestBodies/IdList' } responses: '200': description: Map keyed by CVE ID. content: application/json: schema: { type: object, additionalProperties: { type: object } } '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '413': { $ref: '#/components/responses/TooLarge' } /api/v1/weaknesses/resolve: post: tags: [Bulk & sync] summary: Batch CWE → CAPEC → ATT&CK chains (pro+) operationId: bulkGetCWE x-min-tier: pro description: CWE list plus the resolved CWE→CAPEC→ATT&CK chain and tactic IDs for up to **500** IDs. requestBody: { $ref: '#/components/requestBodies/IdList' } responses: '200': description: Map keyed by CVE ID with `{cwes, chains, tactic_ids, source}`. content: application/json: schema: { type: object, additionalProperties: { type: object } } '403': { $ref: '#/components/responses/Forbidden' } /api/v1/decisions/evaluate: post: tags: [Bulk & sync, Decision frameworks] summary: Batch Harpia SSVC or CISA BOD 26-04 decisions (pro+) operationId: bulkGetCVEDecision x-min-tier: pro description: Decisions for up to **500** IDs under one selected framework and context. Duplicates deduped. requestBody: required: true content: application/json: schema: type: object required: [ids] properties: ids: { type: array, items: { type: string }, maxItems: 500 } framework: { type: string, enum: [harpia_ssvc, bod_26_04], default: harpia_ssvc } exposure: { type: string, enum: [exposed, controlled, not_exposed], default: exposed } mission_consequence: { type: string, enum: [critical, high, moderate, low], default: critical } discovered: { type: string, format: date } additionalProperties: false responses: '200': description: Decisions plus any unresolved IDs. content: application/json: schema: type: object properties: inputs: { type: object, additionalProperties: true } results: { type: object, additionalProperties: { $ref: '#/components/schemas/DecisionResponse' } } not_found: { type: array, items: { type: string } } '403': { $ref: '#/components/responses/Forbidden' } /api/v1/inventory/match: post: tags: [Inventory, Bulk & sync] summary: Match compact package coordinates (pro+) operationId: matchComponentsBulk x-min-tier: pro description: | Accepts at most **500** package coordinates prepared locally by the Harpia CLI. The source SBOM, manifest, or lockfile is not uploaded. PURL is preferred and CPE is the fallback under the same matching rules as the corpus matcher. This endpoint returns corpus matches only; evaluate the deduplicated vulnerability IDs with `/api/v1/decisions/evaluate`. requestBody: required: true content: application/json: schema: type: object required: [components] additionalProperties: false properties: components: type: array minItems: 1 maxItems: 500 items: type: object additionalProperties: false description: | One compact coordinate. `purl` is preferred and `cpe` is the fallback. `name` is display metadata. `version` may be supplied separately or carried by the PURL/CPE. Invalid or versionless coordinates are reported as skipped when the batch also contains at least one usable coordinate. properties: ref: { type: string } name: { type: string, description: Optional display name. } version: { type: string, description: Installed version when it is not already carried by the identity. } purl: { type: string, description: Preferred Package URL identity, optionally including the installed version. } cpe: { type: string, description: CPE 2.3 fallback identity, optionally including the installed version. } source_name: { type: string } source_version: { type: string } os_namespace: { type: string, description: Normalized distribution namespace derived from the inventory OS descriptor. } os_product: { type: string, description: OS product identity derived from the inventory OS descriptor. } os_version: { type: string } os_major: { type: string, pattern: '^[0-9]{1,16}$' } responses: '200': description: Corpus match report without policy decisions. headers: Cache-Control: schema: { type: string, const: "private, no-cache" } content: application/json: schema: { $ref: '#/components/schemas/InventoryMatchReport' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '413': { $ref: '#/components/responses/TooLarge' } '415': description: The request is not JSON. content: application/json: schema: { $ref: '#/components/schemas/Error' } '429': { $ref: '#/components/responses/RateLimited' } '503': description: Matcher admission or execution timed out while the service was at capacity. content: application/json: schema: { $ref: '#/components/schemas/Error' } /api/v1/vulnerabilities/changes: get: tags: [Bulk & sync] summary: CVE IDs changed since a date (pro+) operationId: modifiedSince x-min-tier: pro description: | Distinct CVE IDs changed on or after `since` — the building block for a local mirror (fetch only what changed). Counts the record's own last-modified **plus** repository and reference activity (broader than the search `modified:` qualifier). Returns up to 50,000 IDs. parameters: - name: since in: query required: true schema: { type: string, format: date, examples: ["2026-07-01"] } responses: '200': description: Changed IDs. content: application/json: schema: type: object properties: since: { type: string, format: date } count: { type: integer } ids: { type: array, items: { type: string } } '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } /api/v1/exploits/repositories: get: tags: [Bulk & sync] summary: PoC repository records (pro+) operationId: listRepos x-min-tier: pro description: Paginated public exploit-repository records with an optional keyword/CVE filter. parameters: - name: term in: query schema: { type: string, maxLength: 500 } description: | Keyword or CVE filter. Same 500-rune bound and control-character strip as `term` on `/vulnerabilities/search`; over it is `400 search term too long`. - name: sort in: query schema: { type: string, enum: [epss, stars, repos, published], default: epss } - $ref: '#/components/parameters/Page' - name: limit in: query schema: { type: integer, minimum: 1, maximum: 200, default: 25 } responses: '200': description: Paginated repositories. content: application/json: schema: { $ref: '#/components/schemas/PaginatedRows' } '403': { $ref: '#/components/responses/Forbidden' } /api/v1/observed/trending: get: tags: [Search] summary: Regionally trending observed probing operationId: observedTrending x-min-tier: free description: Authenticated regional sensor telemetry. Probing is exposure to attempts, not confirmed compromise. parameters: - name: country in: query schema: { type: string } description: ISO-3166-1 alpha-2 code or full country name; defaults to caller-IP geolocation. - name: limit in: query schema: { type: integer, minimum: 1, maximum: 50, default: 10 } responses: '200': description: Regional trending records. content: application/json: schema: { type: object, additionalProperties: true } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/RateLimited' } /api/v1/risk: get: tags: [System] summary: Homepage momentum feed (web-app only) operationId: webRisk security: [] description: Quota-free first-party web-app feed. Bearer and non-SPA callers receive 404. responses: '200': description: Bounded momentum records used by the homepage. content: application/json: schema: { type: object, additionalProperties: true } '404': { $ref: '#/components/responses/NotFound' } /api/v1/keys: get: tags: [Account] summary: List your API keys operationId: listKeys x-min-tier: free security: [{ cookieAuth: [] }] description: Cookie session only — a Bearer key is rejected here. responses: '200': description: Your keys. content: application/json: schema: type: object properties: keys: type: array items: type: object properties: id: { type: string } name: { type: string } expires_at: { type: [string, "null"], format: date-time } '403': { $ref: '#/components/responses/Forbidden' } post: tags: [Account] summary: Mint an API key operationId: createKey x-min-tier: free security: [{ cookieAuth: [] }] description: Cookie session only. Choose a lifetime up to 365 days. The full key is shown once. requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: { type: string } days: { type: integer, minimum: 1, maximum: 365, default: 365 } responses: '201': description: The new key (full value returned once). content: application/json: schema: type: object properties: id: { type: string } name: { type: string } key: { type: string, description: The secret — store it now; not retrievable later. } '403': description: Key limit reached for your plan, or Bearer used. content: application/json: schema: { $ref: '#/components/schemas/Error' } /api/v1/keys/{id}: delete: tags: [Account] summary: Revoke an API key operationId: revokeKey x-min-tier: free security: [{ cookieAuth: [] }] description: Cookie session only. Requests using the key stop working immediately. parameters: - name: id in: path required: true schema: { type: string } responses: '204': { description: Revoked. } '403': { $ref: '#/components/responses/Forbidden' } /auth/me: get: tags: [Session] summary: Current identity and tier operationId: authMe security: [{ cookieAuth: [] }] description: Web-app only. Returns the signed-in user, tier, key allowance, and saved default decision framework. responses: '200': description: Identity. content: application/json: schema: type: object properties: username: { type: string } tier: { type: string, enum: [free, pro, enterprise] } max_api_keys: { type: integer } default_decision_framework: { type: string, enum: [harpia_ssvc, bod_26_04], default: harpia_ssvc } '401': { $ref: '#/components/responses/Unauthorized' } /auth/preferences: put: tags: [Session] summary: Save account preferences operationId: updateAuthPreferences security: [{ cookieAuth: [] }] description: Web-app only. Saves the decision framework that CVE and search views open first. API decision requests remain explicit. requestBody: required: true content: application/json: schema: type: object required: [default_decision_framework] properties: default_decision_framework: { type: string, enum: [harpia_ssvc, bod_26_04] } responses: '200': description: Preference saved. content: application/json: schema: type: object properties: default_decision_framework: { type: string, enum: [harpia_ssvc, bod_26_04] } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /auth/usage: get: tags: [Session] summary: Your consumption vs. plan operationId: authUsage security: [{ cookieAuth: [] }] description: Web-app only. Daily usage, rate, key count, and search depth for the signed-in user. responses: '200': description: Usage snapshot. content: application/json: schema: type: object properties: tier: { type: string } daily: type: object properties: used: { type: integer } limit: { type: integer } resets_at: { type: string, format: date-time } resets_in_seconds: { type: integer } rate_per_minute: { type: integer } api_keys: type: object properties: { used: { type: integer }, max: { type: integer } } search: type: object properties: { max_page: { type: integer }, max_limit: { type: integer } } '401': { $ref: '#/components/responses/Unauthorized' } /auth/methods: get: tags: [Session] summary: Enabled sign-in methods operationId: authMethods security: [] description: Web-app only. Advertises enabled providers for the sign-in page. responses: '200': description: Enabled methods. content: application/json: schema: type: object properties: local: { type: boolean } oauth: { type: array, items: { type: string } } /auth/login: post: tags: [Session] summary: Sign in with local credentials operationId: localLogin security: [] description: Conditional web-app route; available when local authentication is enabled. requestBody: required: true content: application/json: schema: type: object required: [username, password] properties: username: { type: string } password: { type: string, format: password } responses: '200': { description: Signed in; the HttpOnly session cookie is set. } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/RateLimited' } /auth/logout: post: tags: [Session] summary: End the browser session operationId: logout security: [{ cookieAuth: [] }] description: Web-app-only. Also clears a cookie whose token no longer verifies. requestBody: { $ref: '#/components/requestBodies/EmptyJSON' } responses: '204': { description: Session ended and cookie cleared. } '429': { $ref: '#/components/responses/RateLimited' } /auth/login/{provider}: get: tags: [Session] summary: Begin OAuth sign-in operationId: oauthBegin security: [] parameters: [{ name: provider, in: path, required: true, schema: { type: string, enum: [google, microsoft, github] } }] responses: '302': { description: Redirect to the selected identity provider. } '404': { $ref: '#/components/responses/NotFound' } /auth/callback/{provider}: get: tags: [Session] summary: Complete OAuth sign-in operationId: oauthCallback security: [] parameters: - { name: provider, in: path, required: true, schema: { type: string, enum: [google, microsoft, github] } } - { name: code, in: query, schema: { type: string } } - { name: state, in: query, schema: { type: string } } responses: '302': { description: Session established and browser redirected. } '400': { $ref: '#/components/responses/BadRequest' } /auth/register: post: tags: [Session] summary: Register a local account operationId: register security: [] description: Conditional route; available only when self-signup is configured. requestBody: { $ref: '#/components/requestBodies/EmailPassword' } responses: '202': { description: Registration accepted; verification may be required. } '400': { $ref: '#/components/responses/BadRequest' } '429': { $ref: '#/components/responses/RateLimited' } /auth/resend: post: tags: [Session] summary: Resend account verification operationId: resendVerification security: [] requestBody: { $ref: '#/components/requestBodies/EmailOnly' } responses: '202': { description: Accepted without revealing account existence. } '429': { $ref: '#/components/responses/RateLimited' } /auth/verify: get: tags: [Session] summary: Verify a local account operationId: verifyAccount security: [] parameters: [{ name: token, in: query, required: true, schema: { type: string } }] responses: '302': { description: Verification applied and browser redirected. } '400': { $ref: '#/components/responses/BadRequest' } /auth/forgot: post: tags: [Session] summary: Request password recovery operationId: forgotPassword security: [] requestBody: { $ref: '#/components/requestBodies/EmailOnly' } responses: '202': { description: Always accepted without revealing account existence. } '429': { $ref: '#/components/responses/RateLimited' } /auth/reset: post: tags: [Session] summary: Reset a local password operationId: resetPassword security: [] requestBody: required: true content: application/json: schema: type: object required: [token, password] properties: token: { type: string } password: { type: string, format: password } responses: '200': { description: Password reset; existing sessions and keys are invalidated. } '400': { $ref: '#/components/responses/BadRequest' } '429': { $ref: '#/components/responses/RateLimited' } /auth/change-password: post: tags: [Session] summary: Change the signed-in user's password operationId: changePassword security: [{ cookieAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [current_password, new_password] properties: current_password: { type: string, format: password } new_password: { type: string, format: password } responses: '200': { description: Password changed; every session and API key is invalidated. } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /auth/trial/accept: post: tags: [Session] summary: Accept the active trial operationId: acceptTrial security: [{ cookieAuth: [] }] requestBody: { $ref: '#/components/requestBodies/EmptyJSON' } responses: '204': { description: Trial acknowledgement recorded. } '401': { $ref: '#/components/responses/Unauthorized' } /auth/trial/decline: post: tags: [Session] summary: Decline the active trial operationId: declineTrial security: [{ cookieAuth: [] }] requestBody: { $ref: '#/components/requestBodies/EmptyJSON' } responses: '200': { description: Trial ended and the base tier returned. } '401': { $ref: '#/components/responses/Unauthorized' } /auth/request-access: get: tags: [Session] summary: Read access-request status operationId: accessRequestStatus security: [{ cookieAuth: [] }] responses: '200': { description: Current request status. } '401': { $ref: '#/components/responses/Unauthorized' } post: tags: [Session] summary: Request a tier upgrade operationId: requestAccess security: [{ cookieAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [use_case, reason] properties: tier: { type: string } use_case: { type: string } reason: { type: string } responses: '202': { description: Access request accepted. } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /auth/access-decision: get: tags: [Session] summary: Review a signed access decision operationId: accessDecisionPage security: [] parameters: - { name: rid, in: query, required: true, schema: { type: integer, minimum: 1 } } - { name: action, in: query, required: true, schema: { type: string, enum: [approve, reject] } } - { name: sig, in: query, required: true, schema: { type: string, minLength: 64, maxLength: 64, pattern: '^[0-9a-f]{64}$' } } responses: '200': { description: HTML confirmation page for the signed decision link. } '400': { $ref: '#/components/responses/BadRequest' } post: tags: [Session] summary: Apply a signed access decision operationId: applyAccessDecision security: [] requestBody: required: true content: application/x-www-form-urlencoded: schema: type: object required: [rid, action, sig, code] properties: rid: { type: string } action: { type: string, enum: [approve, reject] } sig: { type: string } code: { type: string } grant: { type: string, enum: [pro, enterprise] } trial_days: { type: integer, minimum: 0, maximum: 3650 } responses: '200': { description: Signed decision applied. } '400': { $ref: '#/components/responses/BadRequest' } /api/v1/tiers: get: tags: [System] summary: Plan-limit table (web-app view) operationId: getTiers security: [] description: The plan limits shown on the docs page. Web-app-only view (Bearer clients get 404). responses: '200': description: Tier limits. content: application/json: schema: { type: object, additionalProperties: true } /api/v1/health: get: tags: [System] summary: Health check operationId: health security: [] description: Open to bare callers (container healthchecks). responses: '200': description: Service status. content: application/json: schema: type: object properties: status: { type: string, enum: [ok, degraded] } /taxii2/: get: tags: [TAXII] summary: TAXII 2.1 discovery operationId: taxiiDiscovery security: [] description: | Unauthenticated TAXII 2.1 discovery per spec. Negotiate the media type with `Accept: application/taxii+json;version=2.1`. The API root and collection walks under `/threatintel/` require an **enterprise** key. responses: '200': description: TAXII discovery document. content: application/taxii+json;version=2.1: schema: { type: object, additionalProperties: true } /threatintel/: get: tags: [TAXII] summary: TAXII API root operationId: taxiiApiRoot x-min-tier: enterprise responses: '200': { description: TAXII API-root document. } '403': { $ref: '#/components/responses/Forbidden' } /threatintel/collections/: get: tags: [TAXII] summary: List TAXII collections operationId: taxiiCollections x-min-tier: enterprise responses: '200': { description: Collections visible to the caller. } '403': { $ref: '#/components/responses/Forbidden' } /threatintel/collections/{id}/: get: tags: [TAXII] summary: Read a TAXII collection operationId: taxiiCollection x-min-tier: enterprise parameters: [{ $ref: '#/components/parameters/CollectionId' }] responses: '200': { description: Collection metadata. } '404': { $ref: '#/components/responses/NotFound' } /threatintel/collections/{id}/objects/: get: tags: [TAXII] summary: Read objects from a TAXII collection operationId: taxiiObjects x-min-tier: enterprise parameters: - { $ref: '#/components/parameters/CollectionId' } - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 1000 } } - { name: added_after, in: query, schema: { type: string, format: date-time } } - { name: next, in: query, schema: { type: string, maxLength: 2048 } } responses: '200': { description: TAXII envelope containing STIX objects. } '404': { $ref: '#/components/responses/NotFound' } /threatintel/collections/{id}/objects/{obj_id}/: get: tags: [TAXII] summary: Read one STIX object operationId: taxiiObject x-min-tier: enterprise parameters: - { $ref: '#/components/parameters/CollectionId' } - { $ref: '#/components/parameters/ObjectId' } - { name: added_after, in: query, schema: { type: string, format: date-time } } responses: '200': { description: TAXII envelope containing the requested object. } '404': { $ref: '#/components/responses/NotFound' } /threatintel/collections/{id}/objects/{obj_id}/versions/: get: tags: [TAXII] summary: List versions of one STIX object operationId: taxiiObjectVersions x-min-tier: enterprise parameters: - { $ref: '#/components/parameters/CollectionId' } - { $ref: '#/components/parameters/ObjectId' } - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 1000 } } - { name: added_after, in: query, schema: { type: string, format: date-time } } - { name: next, in: query, schema: { type: string, maxLength: 2048 } } responses: '200': { description: Available object versions. } '404': { $ref: '#/components/responses/NotFound' } /threatintel/collections/{id}/manifest/: get: tags: [TAXII] summary: Read a TAXII collection manifest operationId: taxiiManifest x-min-tier: enterprise parameters: - { $ref: '#/components/parameters/CollectionId' } - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 1000 } } - { name: added_after, in: query, schema: { type: string, format: date-time } } - { name: next, in: query, schema: { type: string, maxLength: 2048 } } responses: '200': { description: Collection manifest. } '404': { $ref: '#/components/responses/NotFound' } /threatintel/search/: get: tags: [TAXII] summary: Search as a STIX 2.1 bundle (pro+) operationId: taxiiSearch x-min-tier: pro description: | Non-standard convenience endpoint (it is not part of TAXII 2.1) that runs the **same query language and the same engine as `/api/v1/vulnerabilities/search`** and returns the matches as a STIX 2.1 bundle instead of JSON rows. It backs the STIX Explorer. Pro+ — the raw TAXII collection walk under `/threatintel/` stays enterprise-only. It is bound to your tier the same way `/vulnerabilities/search` is: identical field strips, and never more rows than your `search_max_limit` allows (20 by default). **What comes back.** The matched `vulnerability` SDOs plus their enrichment — `attack-pattern`, `tool` (exploit modules and PoC repos), `malware`, `software`, `threat-actor`, `location`, `identity` (targeted sectors), `note` (exploitation signals), `course-of-action` — and the `relationship` SROs tying them together. Three objects anchor every response: the producer `identity` that every SDO points at through `created_by_ref`, the TLP `marking-definition` that identity carries, and one `grouping` (`context: unspecified`, `name: "Search: "`, id deterministic per query) whose `object_refs` list every result. The grouping states the shared context without asserting a relationship between results, and keeps an unenriched result attached to the graph rather than floating as an island. No results, no grouping. **Size is bounded at 5,000 objects.** The row cap limits how many CVEs match; it says nothing about response size, since each CVE expands into everything related to it. Trimming therefore drops **whole vulnerabilities**, least relevant first, never truncating the object list — cutting a list would leave SROs and `_ref`/`_refs` properties aimed at ids no longer present, which a viewer draws as broken edges. Enrichment shared with a surviving CVE is kept. If anything is dropped, the bundle contains a standard STIX `note` SDO with abstract `Harpia export truncation notice`; Bundle itself remains a closed, schema-compliant STIX container. parameters: - name: q in: query required: true schema: { type: string, maxLength: 500, examples: ["actor:qilin exploited:true"] } description: | The same query language as `term` on `/api/v1/vulnerabilities/search`, under the same 500-rune bound and control-character strip. Empty is `400 q parameter required`. - name: enrich in: query schema: { type: string, enum: ["false"], default: "true" } description: | `enrich=false` returns the `vulnerability` SDOs alone, without the related objects. Any other value enriches. - { name: download, in: query, schema: { type: string, enum: ["1"] } } - { $ref: '#/components/parameters/Severity' } - { $ref: '#/components/parameters/Exploited' } - { $ref: '#/components/parameters/Weaponized' } - { $ref: '#/components/parameters/Ransomware' } - { $ref: '#/components/parameters/CisaKev' } - { $ref: '#/components/parameters/Malicious' } - name: sort in: query schema: { type: string, enum: [epss, cvss, exploits, published, trending], default: epss } responses: '200': description: A STIX 2.1 bundle. content: application/json: schema: { $ref: '#/components/schemas/StixBundle' } application/taxii+json;version=2.1: schema: { $ref: '#/components/schemas/StixBundle' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/RateLimited' } components: securitySchemes: bearerAuth: type: http scheme: bearer description: "`Authorization: Bearer `. Mint keys from a signed-in browser session." cookieAuth: type: apiKey in: cookie name: __spa_tok description: HttpOnly web-app session cookie. Used only by the site (not programmatic clients). parameters: CollectionId: name: id in: path required: true schema: { type: string, minLength: 1 } ObjectId: name: obj_id in: path required: true schema: { type: string, minLength: 1 } Term: name: term in: query required: false schema: { type: string, maxLength: 500, examples: ["product:openssl version:<3.0 exploited:true"] } description: | The query language. Empty matches nothing. Bounded at **500 runes** — counted in characters, not bytes, so a non-Latin query is not rejected sooner than the same query in ASCII. Over that is `400 search term too long`. Control characters are stripped before the query compiles. The same bound applies to `q` on `/threatintel/search/`. Page: name: page in: query schema: { type: integer, minimum: 1, default: 1 } description: Clamped to your tier's max page. Limit: name: limit in: query schema: { type: integer, minimum: 1, maximum: 200, default: 20 } description: Clamped to your tier's max limit. CVEId: name: id in: path required: true schema: { type: string, examples: [CVE-2024-3400] } description: A CVE or advisory ID. SubPage: name: page in: query schema: { type: integer, minimum: 1, maximum: 10000, default: 1 } SubLimit: name: limit in: query schema: { type: integer, minimum: 1, maximum: 100, default: 100 } Severity: name: severity in: query schema: { type: string, examples: ["CRITICAL,HIGH"] } description: | Panel filter — comma list of CRITICAL, HIGH, MEDIUM, LOW (unknown values are discarded, and only the first four entries are read). Distinct from the `severity:` qualifier inside `term`. Exploited: name: exploited in: query schema: { type: string, enum: ["1"] } description: | Panel flag — the value is literally `1`; anything else is ignored. The five threat flags are OR-combined with each other and AND-ed with `severity`. Weaponized: name: weaponized in: query schema: { type: string, enum: ["1"] } description: Panel flag (`exploit_maturity = weaponized`). OR-ed with the other flags. Ransomware: name: ransomware in: query schema: { type: string, enum: ["1"] } description: Panel flag (known ransomware use). OR-ed with the other flags. CisaKev: name: cisa_kev in: query schema: { type: string, enum: ["1"] } description: Panel flag (listed in the CISA KEV catalogue). OR-ed with the other flags. Malicious: name: malicious in: query schema: { type: string, enum: ["1"] } description: Panel flag (malicious package). OR-ed with the other flags. requestBodies: EmptyJSON: required: true content: application/json: schema: { type: object, maxProperties: 0 } EmailOnly: required: true content: application/json: schema: type: object required: [email] properties: { email: { type: string, format: email } } EmailPassword: required: true content: application/json: schema: type: object required: [email, password] properties: email: { type: string, format: email } password: { type: string, format: password } IdList: required: true content: application/json: schema: type: object required: [ids] properties: ids: { type: array, items: { type: string }, maxItems: 500, examples: [["CVE-2024-3400", "CVE-2021-44228"]] } responses: BadRequest: description: Invalid query or parameter. content: application/json: schema: { $ref: '#/components/schemas/Error' } Unauthorized: description: Missing/invalid credentials. Mint a key; direct anonymous API calls are unsupported. content: application/json: schema: { $ref: '#/components/schemas/Error' } Forbidden: description: | Above your plan, wrong access surface, or a generic block (WAF / IP ban / geo filter — intentionally identical so probes can't fingerprint defenses). content: application/json: schema: { $ref: '#/components/schemas/Error' } AttributionForbidden: description: | Either the endpoint is above your plan, or the query filtered on threat attribution your plan does not include — e.g. `the actor: qualifier needs a plan that includes threat attribution`. The refusal is explicit rather than a silently dropped term, so a zero-match answer is never ambiguous. content: application/json: schema: { $ref: '#/components/schemas/Error' } NotFound: description: Unknown ID, or a route not on your surface. content: application/json: schema: { $ref: '#/components/schemas/Error' } TooLarge: description: Body over the limit (256 KB ordinary, 32 KB CVE and decision bulk, 2 MiB inventory match) or too many coordinates. Split the input. RateLimited: description: > Rate or daily limit hit. Honour `Retry-After` (seconds). The body's `scope` says which of your two budgets ran out — `api_key` or `web_session` — so a throttled integration is never confused with heavy browsing. headers: Retry-After: schema: { type: integer } description: Seconds to wait. X-Quota-Scope: schema: { type: string, enum: [api_key, web_session] } description: Which budget this request drew from. content: application/json: schema: { $ref: '#/components/schemas/Error' } schemas: Error: type: object description: > Errors use either `error` (short) or `message` (detailed). Tier walls carry extra fields so a client can act on them without parsing prose: `scope`, `tier`, `required_tier`, `unlocks`, `daily_limit`, `resets_at`, `retry_after` and `upgrade`. properties: error: { type: string } message: { type: string } scope: type: string enum: [api_key, web_session] description: Which budget was exhausted (429 only). tier: { type: string, description: The tier this request was served under. } required_tier: { type: string, description: The tier that lifts this limit. } unlocks: type: array items: { type: string } description: What the required tier opens, in product terms. daily_limit: { type: integer } resets_at: { type: string, format: date-time } retry_after: { type: integer } upgrade: { type: string, format: uri } examples: - error: "this endpoint is not included in your plan" tier: free required_tier: pro unlocks: ["public exploit and PoC repositories"] upgrade: "https://www.harpia.ae/contact" - error: "daily API request limit reached (500 requests) -- resets at midnight UTC" scope: api_key tier: free daily_limit: 500 resets_at: "2026-07-24T00:00:00Z" - { message: "query syntax error: check parentheses and operators" } CVEResult: type: object description: One search row. Some fields are tier-stripped (noted). properties: id: { type: string, examples: [CVE-2024-3400] } description: { type: string, description: Truncated to 300 chars. } published: { type: string, format: date } severity: { type: string, enum: [CRITICAL, HIGH, MEDIUM, LOW] } cvss: { type: number, examples: [10.0] } epss_score: { type: number, examples: [0.99] } exploit_maturity: { type: string, enum: [none, poc, weaponized, active] } exploited_in_wild: { type: boolean } exploit_count: type: integer description: > Exploit CODE only: poc/weapon references plus the repositories the classifier kept with an exploit or weapon role. A reference reporting real in-the-wild attacks adds nothing to it. Backs `has_exploit:true` and `sort=exploits`. ref_count: type: integer description: Every reference held for this vulnerability. repo_count: type: integer description: > Every public repository found for it, whatever grade the classifier gave it. `ref_count + repo_count` is the Evidence figure the web app shows. weaponized: { type: boolean } cisa_kev: { type: boolean } is_fixable: { type: boolean } is_malicious: { type: boolean } malicious_packages: { type: [object, "null"] } cwe: { type: array, items: { type: string } } aliases: { type: array, items: { type: string } } is_automatable: { type: boolean, description: "free+ (stripped for anonymous)" } trend_level: { type: string, enum: [STAGNANT, ACTIVE, RISING, TRENDING], description: "free+" } trending_score: { type: number, description: "free+" } ransomware_use: { type: boolean, description: "pro+ (attribution)" } ttps: $ref: '#/components/schemas/TTPBlock' actor_ttps: $ref: '#/components/schemas/TTPBlock' threat_actors: { type: array, items: { type: string }, description: "pro+ (attribution) — actors attributed to this CVE", examples: [["Qilin", "APT29"]] } threat_countries: { type: array, items: { type: string }, description: "pro+ (attribution) — actor origin, ISO-3166-1 alpha-2 codes (normalized at ingest; expand code→name client-side)", examples: [["RU", "CN"]] } threat_motivations: { type: array, items: { type: string }, description: "pro+ (attribution) — why those actors operate", examples: [["espionage", "financial"]] } threat_sectors: { type: array, items: { type: string }, description: "pro+ (attribution) — industries the attributed actors target" } target_countries: { type: array, items: { type: string }, description: "pro+ (attribution) — victim geography, ISO-3166-1 alpha-2 codes (normalized at ingest; expand code→name client-side)", examples: [["UA", "US"]] } actor_count: { type: integer, description: "pro+ (attribution) — attributed actors on this CVE" } TTPBlock: type: object description: >- MITRE ATT&CK techniques (pro+). `ttps` is the CVE's OWN exploitation chain (its CWEs → CAPEC → ATT&CK); `actor_ttps` is the behavioural playbook of the actors attributed to it. `tactic_ids` and `stages` are parallel and ordered by KILL-CHAIN position — never by id, since ATT&CK tactic ids are assigned by creation date (TA0043 Reconnaissance is first, TA0040 Impact last). Absent when no formal mapping exists. properties: techniques: type: array items: type: object properties: id: { type: string, examples: ["T1190"] } name: { type: string, examples: ["Exploit Public-Facing Application"] } tactics: { type: array, items: { type: string }, examples: [["TA0001"]] } tactic_names: { type: array, items: { type: string }, examples: [["Initial Access"]] } tactic_ids: { type: array, items: { type: string } } stages: { type: array, items: { type: string } } source: { type: string, examples: ["attack_dim"] } formal: { type: boolean } Exploitation: type: object description: Posture of the WHOLE match set (public, every tier). properties: max_level: { type: string, enum: [active, weaponized, poc, none] } in_the_wild: { type: integer } weaponized: { type: integer } poc: { type: integer } kev: { type: integer } SearchResponse: type: object properties: total: { type: integer, description: Honest match count — never capped. } accessible: { type: integer, description: How many your tier can paginate to (= total for pro/enterprise). } returned: { type: integer } page: { type: integer } limit: { type: integer } results: { type: array, items: { $ref: '#/components/schemas/CVEResult' } } facets: { type: object, additionalProperties: { type: integer } } exploitation: { $ref: '#/components/schemas/Exploitation' } StixBundle: type: object description: A STIX 2.1 bundle. Objects are modelled loosely — the STIX schema is large. properties: type: { type: string, const: bundle } id: { type: string, examples: ["bundle--0f1e…"] } objects: type: array items: { type: object, additionalProperties: true } required: [type, id, objects] additionalProperties: false PaginatedRows: type: object properties: total: { type: integer } page: { type: integer } limit: { type: integer } results: { type: array, items: { type: object, additionalProperties: true } } # `counts` describes the WHOLE array, not the page, and is omitted when the # caller's tier left them nothing to describe. It exists because # `exploit_count` on the CVE record is not a total of everything below it: # it counts poc/weapon references plus the repositories the classifier kept # with an exploit or weapon role. A reference reporting real in-the-wild # attacks contributes nothing to it, and neither does a rejected repository, # so the composition cannot be recovered by subtracting these totals. PaginatedReferences: allOf: - $ref: '#/components/schemas/PaginatedRows' - type: object properties: counts: type: object description: > Every reference for this CVE, bucketed by `source_type`. `poc` + `weapon` is the reference half of `exploit_count`; `exploited` is the count of reports of real attacks and is carried by no other field. properties: total: { type: integer } poc: { type: integer, description: Proof-of-concept code. } weapon: { type: integer, description: Packaged, reliable exploit code. } exploited: { type: integer, description: Reports of exploitation in the wild. } observed: { type: integer, description: Honeypot or scanner telemetry. } reference: { type: integer, description: Context only - advisory, writeup, patch. } PaginatedRepositories: allOf: - $ref: '#/components/schemas/PaginatedRows' - type: object properties: counts: type: object properties: total: type: integer description: > Every public repository found for this CVE, whatever the classifier made of it. A published repository is a signal in its own right, so this is not filtered by confidence. exploit_code: type: integer description: > Those the classifier kept with an exploit or weapon role - the repository half of `exploit_count`. DecisionResponse: type: object properties: cve_id: { type: string } framework: { type: string, enum: [harpia_ssvc, bod_26_04] } inputs: type: object properties: framework: { $ref: '#/components/schemas/DecisionInput' } exposure: { $ref: '#/components/schemas/DecisionInput' } mission_consequence: { $ref: '#/components/schemas/DecisionInput' } x_intel_priority: type: object description: Tier-stripped like other intel fields. properties: decision: { type: string, enum: [immediate, out-of-cycle, scheduled, defer] } outcome: { type: string } exploit_maturity: { type: string } exploited_in_wild: { type: boolean } is_automatable: { type: boolean } epss_score: { type: number } attack_vector: { type: string } policy_evaluated: { type: boolean } engine_version: { type: string, examples: [harpia-decision-v3] } facts: type: object description: Versioned Harpia-derived facts with provenance; external SSVC labels are reported separately from Harpia derivation. properties: exploitation: { type: object, additionalProperties: true } automatable: { type: object, additionalProperties: true } technical_impact: { type: object, additionalProperties: true } guidance: type: object description: Remediation timeline owned by the selected framework. properties: id: { type: string, enum: [harpia_ssvc, bod_26_04] } framework: { type: string, enum: [Harpia SSVC, CISA BOD 26-04] } source: { type: string } reference: { type: string, format: uri } row: { type: integer, minimum: 1, maximum: 16 } timeline: { type: string, enum: [3_days, 14_days, 60_days, fix_on_system_upgrade] } label: { type: string } window_days: { type: [integer, 'null'], enum: [3, 14, 60, null] } requires_forensic_triage: { type: boolean } discovered_at: { type: string, format: date } due_date: { type: [string, 'null'], format: date } overdue: { type: boolean } criteria: type: object properties: priority: { type: string, enum: [immediate, out-of-cycle, scheduled, defer] } exploitation_basis: { type: string, enum: [cisa_kev] } publicly_exposed: { type: boolean } on_kev: { type: boolean } automatable: { type: boolean } technical_impact: { type: string, enum: [total_control, partial_control] } DecisionInput: type: object required: [value, source, defaulted] properties: value: {} source: { type: string, enum: [customer, harpia_default] } defaulted: { type: boolean } InventoryMatchReport: type: object required: [format, spec_version, summary, findings, truncated] properties: format: { type: string, const: coordinates } spec_version: { type: string, const: "1" } summary: { $ref: '#/components/schemas/InventoryMatchSummary' } findings: type: array maxItems: 5000 items: { $ref: '#/components/schemas/InventoryFinding' } skipped: type: array description: Coordinates with an invalid identity or without an installed version. items: { $ref: '#/components/schemas/InventoryComponentIssue' } truncated: type: boolean description: True when the 5,000-finding response bound was reached. InventoryMatchSummary: type: object properties: components: { type: integer, description: Identifiable installed package records after source, file, OS, container, and other descriptor records are excluded, before deduplication. } unique_components: { type: integer } components_scanned: { type: integer, description: Unique component identities resolved in the Harpia corpus and checked against affected ranges. } vulnerable_components: { type: integer } clean_components: { type: integer, description: Corpus-resolved components with no affected range for the installed version. Unresolved identities are not counted as clean. } skipped_components: { type: integer } findings: { type: integer } exploited_findings: { type: integer } malicious_findings: { type: integer } InventoryFinding: type: object properties: component: type: object properties: ref: { type: string } name: { type: string } version: { type: string } purl: { type: string } cpe: { type: string } match_method: { type: string, enum: [purl, purl_source, purl_inferred, cpe], description: purl_source uses declared OS source-package and distribution context; purl_inferred is used only for high-confidence SPDX OS-package identities. } occurrences: { type: integer, minimum: 1, description: Number of duplicate identity and version entries represented by this component. } vulnerability: type: object properties: id: { type: string, examples: [CVE-2021-44228, MAL-2026-1828] } kind: { type: string, examples: [cve, malicious_package] } severity: { type: string, enum: [CRITICAL, HIGH, MEDIUM, LOW] } cvss: { type: number } epss: { type: number } maturity: { type: string, enum: [none, poc, weaponized, active] } kev: { type: boolean } automatable: { type: boolean } fix_available: { type: boolean } fixed_version: { type: string } vendor_advisory_id: { type: string } InventoryComponentIssue: type: object properties: ref: { type: string } name: { type: string } version: { type: string } purl: { type: string } cpe: { type: string } reason: type: string enum: [invalid_identity, missing_version, invalid_version] CVERecord: type: object description: | CVE 5.1-shaped envelope plus Harpia's `x_intel_priority` intel block. Reference/repository arrays are summarised to counts — fetch full arrays via the sub-resources. Modelled loosely here (the CVE 5.1 schema is large). properties: dataType: { type: string, examples: [CVE_RECORD] } dataVersion: { type: string, examples: ["5.1"] } cveMetadata: { type: object, additionalProperties: true } containers: { type: object, additionalProperties: true } x_intel_priority: { type: object, additionalProperties: true } additionalProperties: true