{
  "_about": "endoflife.ai SBOM enrichment - the lifecycle facts POST https://api.endoflife.ai/v1/sbom?enrich=1 writes back onto a CycloneDX or SPDX document. CycloneDX: one `properties` entry per field on each component, in the `endoflife:` namespace; document-level fields under metadata.properties; a metadata.tools entry names the API and its version. SPDX: the same fields as `key=value; ...` in one annotation per package (annotator 'Tool: endoflife.ai API') and one document-level annotation. Everything else in the document - serialNumber, bom-refs, hashes, licenses, existing properties - is left exactly as received. Re-enriching replaces earlier endoflife entries instead of stacking them.",
  "namespace": "endoflife:",
  "version": "4",
  "resolution_rule": "A component carrying a package URL resolves ONLY by that purl (normalised: type + namespace/name, version and qualifiers stripped, lowercased, %40 -> @, PyPI _ -> -) against https://endoflife.ai/purl-map.json. A component without a purl resolves by exact name unless strict=1. A component we cannot resolve is stamped status=not-tracked with a reason - never a guess. Versions resolve to the tracked release line (exact cycle, else major.minor, else major), never a patch build. AI-BOM components resolve by exact model id: a CycloneDX machine-learning-model component (name = the model API id, optional version), a pkg:generic or pkg:huggingface purl whose last path segment is a model id, or a plain component named exactly by a model id (gpt-5, claude-sonnet-4-5-20250929, gemini-2.5-pro, llama-3.3-70b, mistral-medium-2508). The id, id-version and id@version forms are tried; nothing fuzzier. A resolved model carries its line's retirement date (eol_date), status and recommended replacement where the vendor publishes one; matched_by reads model. Model ids come from the AI-model products (openai-api, google-gemini, meta-llama, mistral-ai) and the claude product, the same data the product pages serve.",
  "component_properties": {
    "endoflife:status": "active | warn | eol | not-tracked | version-not-tracked. 'warn' = end of life within the API's warning horizon; 'eol' = past end of life (or declared end-of-life with no date - see eol_date_published); 'version-not-tracked' = product known, this version not placeable (ranges such as ^18.3.1 land here).",
    "endoflife:product": "Resolved product slug (https://endoflife.ai/<product>).",
    "endoflife:product_url": "Canonical product page.",
    "endoflife:cycle": "The tracked release line the component resolved to (postgres@14.13 -> 14).",
    "endoflife:eol_date": "ISO date the line reaches end of life. Absent when the vendor publishes no date.",
    "endoflife:eol_date_published": "'false' when the line is declared end-of-life without a calendar date. Absent otherwise.",
    "endoflife:days_until_eol": "Whole days until eol_date (present while supported).",
    "endoflife:days_past_eol": "Whole days since eol_date (present once past).",
    "endoflife:matched_by": "'purl' or 'name'. A name match only ever happens for a component that carries no purl, and never in strict mode.",
    "endoflife:purl_matched": "The normalised purl that resolved (e.g. pkg:docker/library/postgres) - the exact key from purl-map.json.",
    "endoflife:purl_type": "The purl type (docker, generic, npm, maven, pypi, deb, rpm, apk ...). A factual stand-in for a component class: docker/oci/generic rows are base images and runtimes, the rows that matter most for lifecycle.",
    "endoflife:score_url": "The EOL Risk Score card for this line.",
    "endoflife:eol_date_source": "Where eol_date comes from, the same label the API serves: vendor-override, vendor-fetched, vendor-verified, custom, upstream or discrepancy (definitions in the OpenAPI document at https://api.endoflife.ai/openapi.json). API only.",
    "endoflife:eol_date_source_url": "The page or record the date was read from. API only.",
    "endoflife:eol_date_verified_at": "ISO date the value was last checked against that source. API only.",
    "endoflife:eol_date_confidence": "high = two or more sources agree; medium = one source; low = the vendor feed disagrees with the served value, or a stale feed is the only source. API only.",
    "endoflife:extended_support_available": "'true' when a commercial extended-support path is known for this product (post-EOL patches from a vendor or third party); routing runs through endoflife.ai/match.",
    "endoflife:extended_support_date": "ISO date the vendor's own extended or paid support phase ends for this line, where one is published (Microsoft ESU, Oracle Extended Support, Red Hat ELS, Ubuntu ESM). A component past eol_date may still be covered until this date under that program. Absent where none is recorded. The eol_date provenance fields describe eol_date only and do not cover this date; confirm it with the vendor before relying on it for a contract. API only.",
    "endoflife:risk_score": "EOL Risk Score 0-100 (scored subset only - see scoring cap).",
    "endoflife:risk_grade": "Letter grade A-F for the score (scored subset only).",
    "endoflife:risk_band": "Low | Medium | High | Critical (scored subset only).",
    "endoflife:kev_exposed": "'true' when the product has entries in CISA's Known Exploited Vulnerabilities catalog (scored subset only).",
    "endoflife:scored": "'false' on matched lines beyond the tier's scoring cap: lifecycle facts are present, the risk score is not.",
    "endoflife:reason": "Why a component is not-tracked / version-not-tracked: 'purl not tracked', 'name not recognised', 'no purl (strict mode)', 'Version not tracked', 'No version in SBOM', 'Version line is split into several cycles' (a build that cannot be tied to one service-pack cycle is never given a date), 'Version could be the standard release or the ESR line' (Firefox N, N.0 or N.0.x on an ESR number, from a component that does not say ESR: the two editions end years apart, so no date is given; name the component firefox-esr if it is the ESR)."
  },
  "document_properties": {
    "endoflife:enriched_at": "ISO timestamp of this enrichment.",
    "endoflife:components": "Components read from the document (CycloneDX: including nested components and metadata.component; SPDX: packages).",
    "endoflife:matched": "Distinct product@version lines resolved.",
    "endoflife:unique_products": "Distinct products among the matched lines.",
    "endoflife:unique_lines": "Distinct product@cycle lines (duplicates of the same line are annotated individually but counted once here).",
    "endoflife:past_eol": "Matched lines past end of life.",
    "endoflife:not_tracked": "Components with no verdict.",
    "endoflife:gate": "fail (any line past end of life) | warn (any line reaches end of life within warn_days) | pass.",
    "endoflife:gate_policy": "The policy the gate was computed with, e.g. 'fail_on=eol; warn_days=90'.",
    "endoflife:strict": "'true' when name matching was disabled for this run.",
    "endoflife:purl_map_generated": "Generation date of the purl map this enrichment resolved against.",
    "endoflife:purl_map": "https://endoflife.ai/purl-map.json",
    "endoflife:docs": "https://endoflife.ai/api#sbom"
  },
  "metadata_tools": "CycloneDX 1.5+: metadata.tools.components gains { type: 'service', name: 'endoflife.ai API', version, supplier: { name: 'endoflife.ai' } }. CycloneDX 1.4 and earlier: metadata.tools (array) gains { vendor: 'endoflife.ai', name: 'endoflife.ai API', version }.",
  "request": {
    "endpoint": "POST https://api.endoflife.ai/v1/sbom?enrich=1",
    "body": "The CycloneDX or SPDX JSON document, or { \"sbom\": <document>, \"enrich\": true, \"strict\": false, \"warn_days\": 90 }",
    "query": {
      "enrich": "1 to return the enriched document instead of the JSON report",
      "strict": "1 for purl-only resolution",
      "warn_days": "warning window for the gate (default 90)"
    },
    "response_content_type": "application/vnd.cyclonedx+json or application/spdx+json; the X-Endoflife-Summary header carries the counts and the gate",
    "scoring_cap": "Lifecycle facts cover every matched line. Risk scores are added for the first N matched lines per request: Free 5, Starter 25, Pro 50 (https://endoflife.ai/api)."
  },
  "samples": {
    "input": "https://endoflife.ai/sbom-sample.cdx.json",
    "enriched": "https://endoflife.ai/sbom-sample.enriched.cdx.json"
  },
  "other_surfaces": {
    "github_action": "https://github.com/endoflife-ai/eol-check - the `sbom` input resolves the same way, locally, with zero API calls; `sbom-output` writes this schema (minus risk scores) to a file",
    "stack_scanner": "https://endoflife.ai/scanner - paste or upload an SBOM, then 'Download enriched SBOM' writes this schema (minus risk scores) in the browser; nothing is uploaded",
    "mcp": "check_sbom on https://mcp.endoflife.ai",
    "watch": "POST https://api.endoflife.ai/v1/sbom/watch { email, sbom, strict? } - subscribes the document's resolved slug:cycle lines to EOL and KEV alerts (double opt-in); stores the lines, never the document; replaces the subscriber's previous list; 50 lines maximum"
  },
  "consistency": "Three writers produce this schema: the API (api-worker/src/sbom.js enrichSbom), the eol-check Action (index.js enrichDoc) and the Stack Scanner (scanner.js buildEnrichedSbom). Only the API adds risk_score / risk_grade / risk_band / kev_exposed and the four eol_date provenance fields (version 3, 2026-09-18); the other two stamp scored=false on every matched line. If two writers ever disagree on a property name or a status value, that is a bug - report it to partners@endoflife.ai.",
  "ai_bom": {
    "since": "2026-09-10",
    "component_types": [
      "machine-learning-model"
    ],
    "purl_types": [
      "generic",
      "huggingface",
      "mlflow",
      "oci",
      "model"
    ],
    "matched_by_value": "model",
    "model_products": [
      "claude",
      "google-gemini",
      "meta-llama",
      "mistral-ai",
      "openai-api"
    ],
    "note": "Frameworks and runtimes (PyTorch, TensorFlow, CUDA) resolve like any software component once tracked; model weights and datasets have no vendor lifecycle and are not resolved."
  }
}
