endoflife.ai
SBOM API Stack Scanner purl-map.json Does an SBOM show EOL?

SBOM Enrichment Walkthrough: End-of-Life Data in a CycloneDX SBOM

By Scott Bissett  ·  Published: September 22, 2026  ·  Worked example  ·  Every response below is the API's real output from the day of publication, trimmed only of fields that change daily.

An SBOM says what is in your software. It does not say whether any of it is still supported. This page shows the whole round trip that fills that gap: a small CycloneDX document goes to one API endpoint and comes back with an end-of-life verdict written onto every component it recognises, as ordinary CycloneDX properties that Dependency-Track, GUAC or your own tooling can read. Nothing here is a mock: the request was sent on the day this page was published and the response is reproduced as returned. The background, and what the two SBOM formats can and cannot carry natively, is in Does an SBOM Show End-of-Life Software?.

The one-line version. curl -X POST "https://api.endoflife.ai/v1/sbom?enrich=1" -H "Content-Type: application/json" --data-binary @sbom.json. No account needed. Components are matched on their package URL first (pkg:maven/…, pkg:npm/…, pkg:pypi/…, pkg:docker/…), then by name.

1. The input: a seven-component SBOM

A deliberately mixed bag, the way a real service's SBOM is: two Maven libraries, two npm packages, a Python framework, a database container image and one npm package nobody tracks. Every component carries a package URL, which is what any SBOM generator (Syft, Trivy, cdxgen, the CycloneDX build plugins, snyk sbom, Mend's SBOM export) writes for you.

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "version": 1,
  "metadata": {
    "component": {
      "type": "application",
      "name": "orders-service",
      "version": "3.4.0"
    }
  },
  "components": [
    {
      "type": "library",
      "name": "log4j-core",
      "version": "2.14.1",
      "purl": "pkg:maven/org.apache.logging.log4j/[email protected]"
    },
    {
      "type": "library",
      "name": "jackson-databind",
      "version": "2.13.4",
      "purl": "pkg:maven/com.fasterxml.jackson.core/[email protected]"
    },
    {
      "type": "library",
      "name": "lodash",
      "version": "4.17.21",
      "purl": "pkg:npm/[email protected]"
    },
    {
      "type": "library",
      "name": "moment",
      "version": "2.29.4",
      "purl": "pkg:npm/[email protected]"
    },
    {
      "type": "library",
      "name": "django",
      "version": "3.2.25",
      "purl": "pkg:pypi/[email protected]"
    },
    {
      "type": "container",
      "name": "postgres",
      "version": "13",
      "purl": "pkg:docker/library/postgres@13"
    },
    {
      "type": "library",
      "name": "left-pad",
      "version": "1.3.0",
      "purl": "pkg:npm/[email protected]"
    }
  ]
}
Facing an end-of-life deadline?
Tell us which product and we’ll reply with vetted extended-support options and pricing guidance — free, no obligation. Vendors don’t pay for placement.

Free · No obligation · Independent · dates verified against vendor sources · Not urgent? Follow the EOL radar or see the 2026 EOL calendar →

2. The output: the same SBOM, annotated

The response is the document you sent, with an endoflife: property block on each component and a summary block under metadata.properties. Nothing else in the document is touched, so it stays a valid SBOM for whatever consumes it next. Fields that change every day (days_past_eol, the enrichment timestamp, the map version) are omitted here; everything else is as returned.

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "version": 1,
  "metadata": {
    "component": {
      "type": "application",
      "name": "orders-service",
      "version": "3.4.0",
      "properties": [
        {
          "name": "endoflife:status",
          "value": "not-tracked"
        },
        {
          "name": "endoflife:reason",
          "value": "name not recognised"
        }
      ]
    },
    "properties": [
      {
        "name": "endoflife:components",
        "value": "8"
      },
      {
        "name": "endoflife:matched",
        "value": "6"
      },
      {
        "name": "endoflife:past_eol",
        "value": "2"
      },
      {
        "name": "endoflife:not_tracked",
        "value": "2"
      },
      {
        "name": "endoflife:unique_products",
        "value": "5"
      },
      {
        "name": "endoflife:unique_lines",
        "value": "6"
      },
      {
        "name": "endoflife:gate",
        "value": "fail"
      },
      {
        "name": "endoflife:gate_policy",
        "value": "fail_on=eol; warn_days=90"
      },
      {
        "name": "endoflife:strict",
        "value": "false"
      },
      {
        "name": "endoflife:docs",
        "value": "https://endoflife.ai/api#sbom"
      }
    ],
    "tools": {
      "components": [
        {
          "type": "service",
          "name": "endoflife.ai API",
          "version": "1.0.0",
          "supplier": {
            "name": "endoflife.ai",
            "url": [
              "https://endoflife.ai"
            ]
          }
        }
      ]
    }
  },
  "components": [
    {
      "type": "library",
      "name": "log4j-core",
      "version": "2.14.1",
      "purl": "pkg:maven/org.apache.logging.log4j/[email protected]",
      "properties": [
        {
          "name": "endoflife:status",
          "value": "active"
        },
        {
          "name": "endoflife:product",
          "value": "log4j"
        },
        {
          "name": "endoflife:product_url",
          "value": "https://endoflife.ai/log4j"
        },
        {
          "name": "endoflife:cycle",
          "value": "2"
        },
        {
          "name": "endoflife:matched_by",
          "value": "purl"
        },
        {
          "name": "endoflife:purl_matched",
          "value": "pkg:maven/org.apache.logging.log4j/log4j-core"
        },
        {
          "name": "endoflife:purl_type",
          "value": "maven"
        },
        {
          "name": "endoflife:score_url",
          "value": "https://endoflife.ai/score/log4j/2"
        },
        {
          "name": "endoflife:extended_support_available",
          "value": "true"
        },
        {
          "name": "endoflife:risk_score",
          "value": "10"
        },
        {
          "name": "endoflife:risk_grade",
          "value": "A"
        },
        {
          "name": "endoflife:risk_band",
          "value": "Low"
        },
        {
          "name": "endoflife:kev_exposed",
          "value": "false"
        }
      ]
    },
    {
      "type": "library",
      "name": "jackson-databind",
      "version": "2.13.4",
      "purl": "pkg:maven/com.fasterxml.jackson.core/[email protected]",
      "properties": [
        {
          "name": "endoflife:status",
          "value": "version-not-tracked"
        },
        {
          "name": "endoflife:product",
          "value": "jackson"
        },
        {
          "name": "endoflife:product_url",
          "value": "https://endoflife.ai/jackson"
        },
        {
          "name": "endoflife:matched_by",
          "value": "purl"
        },
        {
          "name": "endoflife:purl_matched",
          "value": "pkg:maven/com.fasterxml.jackson.core/jackson-databind"
        },
        {
          "name": "endoflife:purl_type",
          "value": "maven"
        },
        {
          "name": "endoflife:reason",
          "value": "Version not tracked"
        }
      ]
    },
    {
      "type": "library",
      "name": "lodash",
      "version": "4.17.21",
      "purl": "pkg:npm/[email protected]",
      "properties": [
        {
          "name": "endoflife:status",
          "value": "active"
        },
        {
          "name": "endoflife:product",
          "value": "lodash"
        },
        {
          "name": "endoflife:product_url",
          "value": "https://endoflife.ai/lodash"
        },
        {
          "name": "endoflife:cycle",
          "value": "4"
        },
        {
          "name": "endoflife:matched_by",
          "value": "purl"
        },
        {
          "name": "endoflife:purl_matched",
          "value": "pkg:npm/lodash"
        },
        {
          "name": "endoflife:purl_type",
          "value": "npm"
        },
        {
          "name": "endoflife:score_url",
          "value": "https://endoflife.ai/score/lodash/4"
        },
        {
          "name": "endoflife:extended_support_available",
          "value": "true"
        },
        {
          "name": "endoflife:risk_score",
          "value": "10"
        },
        {
          "name": "endoflife:risk_grade",
          "value": "A"
        },
        {
          "name": "endoflife:risk_band",
          "value": "Low"
        },
        {
          "name": "endoflife:kev_exposed",
          "value": "false"
        }
      ]
    },
    {
      "type": "library",
      "name": "moment",
      "version": "2.29.4",
      "purl": "pkg:npm/[email protected]",
      "properties": [
        {
          "name": "endoflife:status",
          "value": "active"
        },
        {
          "name": "endoflife:product",
          "value": "moment"
        },
        {
          "name": "endoflife:product_url",
          "value": "https://endoflife.ai/moment"
        },
        {
          "name": "endoflife:cycle",
          "value": "2"
        },
        {
          "name": "endoflife:matched_by",
          "value": "purl"
        },
        {
          "name": "endoflife:purl_matched",
          "value": "pkg:npm/moment"
        },
        {
          "name": "endoflife:purl_type",
          "value": "npm"
        },
        {
          "name": "endoflife:score_url",
          "value": "https://endoflife.ai/score/moment/2"
        },
        {
          "name": "endoflife:extended_support_available",
          "value": "true"
        },
        {
          "name": "endoflife:risk_score",
          "value": "40"
        },
        {
          "name": "endoflife:risk_grade",
          "value": "B"
        },
        {
          "name": "endoflife:risk_band",
          "value": "Medium"
        },
        {
          "name": "endoflife:kev_exposed",
          "value": "false"
        }
      ]
    },
    {
      "type": "library",
      "name": "django",
      "version": "3.2.25",
      "purl": "pkg:pypi/[email protected]",
      "properties": [
        {
          "name": "endoflife:status",
          "value": "eol"
        },
        {
          "name": "endoflife:product",
          "value": "django"
        },
        {
          "name": "endoflife:product_url",
          "value": "https://endoflife.ai/django"
        },
        {
          "name": "endoflife:cycle",
          "value": "3.2"
        },
        {
          "name": "endoflife:eol_date",
          "value": "2024-04-01"
        },
        {
          "name": "endoflife:matched_by",
          "value": "purl"
        },
        {
          "name": "endoflife:purl_matched",
          "value": "pkg:pypi/django"
        },
        {
          "name": "endoflife:purl_type",
          "value": "pypi"
        },
        {
          "name": "endoflife:score_url",
          "value": "https://endoflife.ai/score/django/3.2"
        },
        {
          "name": "endoflife:eol_date_source",
          "value": "upstream"
        },
        {
          "name": "endoflife:eol_date_source_url",
          "value": "https://endoflife.date/django"
        },
        {
          "name": "endoflife:eol_date_confidence",
          "value": "medium"
        },
        {
          "name": "endoflife:extended_support_available",
          "value": "true"
        },
        {
          "name": "endoflife:risk_score",
          "value": "60"
        },
        {
          "name": "endoflife:risk_grade",
          "value": "C"
        },
        {
          "name": "endoflife:risk_band",
          "value": "High"
        },
        {
          "name": "endoflife:kev_exposed",
          "value": "false"
        }
      ]
    },
    {
      "type": "container",
      "name": "postgres",
      "version": "13",
      "purl": "pkg:docker/library/postgres@13",
      "properties": [
        {
          "name": "endoflife:status",
          "value": "eol"
        },
        {
          "name": "endoflife:product",
          "value": "postgresql"
        },
        {
          "name": "endoflife:product_url",
          "value": "https://endoflife.ai/postgresql"
        },
        {
          "name": "endoflife:cycle",
          "value": "13"
        },
        {
          "name": "endoflife:eol_date",
          "value": "2025-11-13"
        },
        {
          "name": "endoflife:matched_by",
          "value": "purl"
        },
        {
          "name": "endoflife:purl_matched",
          "value": "pkg:docker/library/postgres"
        },
        {
          "name": "endoflife:purl_type",
          "value": "docker"
        },
        {
          "name": "endoflife:score_url",
          "value": "https://endoflife.ai/score/postgresql/13"
        },
        {
          "name": "endoflife:eol_date_source",
          "value": "vendor-fetched"
        },
        {
          "name": "endoflife:eol_date_source_url",
          "value": "https://www.postgresql.org/support/versioning/"
        },
        {
          "name": "endoflife:eol_date_verified_at",
          "value": "2026-09-17"
        },
        {
          "name": "endoflife:eol_date_confidence",
          "value": "high"
        },
        {
          "name": "endoflife:extended_support_available",
          "value": "true"
        },
        {
          "name": "endoflife:scored",
          "value": "false"
        }
      ]
    },
    {
      "type": "library",
      "name": "left-pad",
      "version": "1.3.0",
      "purl": "pkg:npm/[email protected]",
      "properties": [
        {
          "name": "endoflife:status",
          "value": "not-tracked"
        },
        {
          "name": "endoflife:reason",
          "value": "purl not tracked"
        }
      ]
    }
  ]
}

3. Reading the seven answers

ComponentWhat came backWhat it means
log4j-core 2.14.1Line active, risk 10 (A), matched by purlThe build is the Log4Shell build and any vulnerability scanner will say so. The lifecycle answer is different and just as true: the Log4j 2 line is still maintained, so a fix exists and upgrading inside the line is the whole remediation. The two signals are complementary, not redundant.
jackson-databind 2.13.4version-not-trackedThe product is tracked and matched by purl; the 2.13 line is not a cycle we carry. The response says so instead of guessing a date from a neighbouring line.
lodash 4.17.21Line active, risk 10 (A)Supported line, extended support available if it ever stops being.
moment 2.29.4Line active, risk 40 (B)Moment is in maintenance mode by its own maintainers' statement; still on a supported line, but the risk score reflects a project that has stopped moving.
django 3.2.25eol, date April 1, 2024, risk 60 (C), source upstream, confidence mediumPast end of life. The date came from the community dataset rather than a page we read at the vendor, and the response says so: a consumer can decide whether medium confidence is enough for its purpose.
postgres:13 (container)eol, date November 13, 2025, source vendor-fetched from postgresql.org, confidence high, scored: falsePast end of life, with the date read from PostgreSQL's own versioning page and the day it was last verified. It carries no risk score only because this request was anonymous: the free tiers score the first five matched lines of a document and return lifecycle facts for all of them; a key raises the cap.
left-pad 1.3.0not-tracked, reason purl not trackedNobody publishes a lifecycle for it, so the response has nothing to say and says nothing. The summary block counts it under not_tracked.
The summary block is the part a pipeline reads. endoflife:gate is fail here because the default policy is fail_on=eol and two components are past end of life. warn_days (default 90) sets how far ahead a line counts as approaching end of life and turns the gate to warn; strict=1 matches on package URL only, so a component without one is reported as not tracked instead of being matched by name. The components count is eight, not seven, because the application itself (metadata.component) is a component too and is checked like the rest.

4. SPDX, and where the data lands

Send an SPDX 2.2 or 2.3 JSON document instead and the same facts come back as annotations on each package from Tool: endoflife.ai API, plus one document-level annotation for the summary. The property names are identical, so a consumer written for one format reads the other with a one-line change. The full list of properties and the SPDX shape are in the API reference.

PropertyPresent whenValue
endoflife:statusalwaysactive, warn, eol, version-not-tracked or not-tracked
endoflife:eol_datethe line has a published dateISO date
endoflife:eol_date_source, _source_url, _verified_at, _confidencea date is presentwhere the date was read (vendor page or community dataset), the page, the day it was last checked, and a confidence grade
endoflife:risk_score, _grade, _band, kev_exposedthe component was scoredthe EOL Risk Score and whether the product has entries in CISA's Known Exploited Vulnerabilities catalog
endoflife:matched_by, purl_matched, purl_typethe component resolvedhow it resolved: the exact package URL from purl-map.json, or the name
endoflife:reasonnothing resolvedwhy: purl not tracked, Version not tracked, an ambiguous version line

5. Running it against your own SBOM

  1. Generate the SBOM with package URLs. Every mainstream generator does; if yours writes names only, the endpoint falls back to name matching, which is honest but coarser.
  2. Post it once. One request per document, not per component. Anonymous calls are limited per day and score five lines; a free key raises both.
  3. Read the summary first. past_eol, not_tracked and gate tell you whether to look further before you open the component list.
  4. Keep the enriched document. It carries its own evidence: the page each date came from and when it was verified, which is what an auditor asks for.
  5. Check the not-tracked list. If a component you care about comes back untracked, write to [email protected] with the package name; that list is where the catalog grows from.

Frequently Asked Questions

How do I add end-of-life data to an SBOM?

Send the SBOM to POST https://api.endoflife.ai/v1/sbom?enrich=1 as CycloneDX or SPDX JSON. The same document comes back with endoflife: properties on every CycloneDX component (or annotations on every SPDX package): status, end-of-life date, the page the date was read from and when it was verified, EOL Risk Score, and CISA KEV exposure. Components are matched on their package URL first, then by name.

Does SBOM enrichment replace a vulnerability scanner?

No. A vulnerability scanner says whether the exact build you have carries known CVEs. Lifecycle enrichment says whether the release line it belongs to still receives fixes at all. A vulnerable build on a supported line has a patch waiting; a clean build on an unsupported line will never get one. You need both signals.

What happens to components endoflife.ai does not track?

They come back marked not-tracked with a reason, and a component whose product is tracked but whose version is not comes back as version-not-tracked. Nothing is guessed: a component with no answer gets no date.

Which SBOM tools produce a document the endpoint accepts?

Any tool that writes CycloneDX 1.4 to 1.6 or SPDX 2.2 or 2.3 JSON with package URLs, including Syft, Trivy, cdxgen, the CycloneDX Maven, Gradle and npm plugins, snyk sbom, and the SBOM export in Mend SCA, Dependency-Track and GitHub.

Related

© 2026 endoflife.ai · How we verify our dates · API · About