Safeguard
Vulnerability Management•

CSAF 2.0 for Software Vendors: Publishing Security Advisories Machines Can Read

What CSAF 2.0 requires, a minimal security advisory that passes the official schema, how customers discover your advisories through provider-metadata.json, and two small scripts to check your own work.

Safeguard Security Team
10 min read

When you fix a vulnerability, your customers need to answer one question fast: does this affect the versions we run? If your advisory is a web page, a person has to read it and compare version numbers by hand. CSAF, the Common Security Advisory Framework, is an OASIS standard for publishing the same information as JSON, so that question can be answered by software.

This guide is for product security and engineering teams at software vendors. It covers what CSAF 2.0 requires, a minimal advisory that passes the official schema, how customers discover your advisories automatically, and two small scripts to check your own work.

The short answer

CSAF 2.0 is a JSON format, standardised by OASIS in November 2022, for security advisories and VEX statements, together with rules for publishing them so tools can find them. A document declares one of five profiles, lists products in a product_tree, and states each product's status for each vulnerability (for example known_affected or fixed). Publishing well means a provider-metadata.json file that tools can discover, ideally at /.well-known/csaf/provider-metadata.json and through a CSAF: line in your security.txt, plus a predictable folder layout for the documents themselves.

Why bother now

Three practical reasons.

First, the people who consume advisories are increasingly machines. Vulnerability scanners and SBOM tools can match a CSAF document's product tree against what a customer runs and close out findings that don't apply. A PDF or HTML advisory needs a human in the loop.

Second, it scales your own work. One JSON document can serve your website, your customer portal and every downstream tool, instead of maintaining each separately.

Third, the ecosystem has settled on it. The OASIS technical committee is chaired by people from CISA and Cisco, CISA publishes its own advisories as CSAF on GitHub, and large vendors run full CSAF feeds. If you sell into regulated customers, expect "do you publish CSAF?" to start appearing in security questionnaires. To be clear, the EU Cyber Resilience Act requires manufacturers to share and publicly disclose information about fixed vulnerabilities, but it does not mandate a format. CSAF is a sensible way to meet that duty, not a legal requirement in itself.

A note on versions: CSAF 2.1 exists as a Committee Specification Draft (the third draft was dated 11 September 2026), but it is not yet an OASIS Standard. Publish 2.0 today.

The five profiles

Every CSAF document sets /document/category, which selects a profile. Each profile adds required fields on top of the base.

Profile/document/categoryUse it forAdds on top of the base
CSAF Baseanything not reserved belowCatch-allCore document metadata only
Security incident responsecsaf_security_incident_responseResponding to a breach or incidentA descriptive note and an external reference
Informational advisorycsaf_informational_advisoryGuidance that isn't about a specific vulnerabilityNotes and references, and no vulnerabilities section
Security advisorycsaf_security_advisoryA vulnerability and its remediationproduct_tree, and for each vulnerability notes and product_status
VEXcsaf_vexStating which products are or are not affectedproduct_tree, plus at least one of fixed, known affected, known not affected

The VEX profile has a rule that catches people out. Every product listed as known_not_affected needs a matching impact statement explaining why, either as a flag or as a threat with category impact (mandatory test 6.1.27.9). "Not affected" without a reason isn't valid VEX.

A minimal security advisory that validates

This is the smallest useful csaf_security_advisory: one product with two versions, one vulnerability, one fix. It passes the official CSAF 2.0 JSON schema.

{
  "document": {
    "category": "csaf_security_advisory",
    "csaf_version": "2.0",
    "publisher": {
      "category": "vendor",
      "name": "Example Corp Product Security",
      "namespace": "https://example.com"
    },
    "title": "Example Gateway: authentication bypass in the admin API",
    "tracking": {
      "id": "EXAMPLE-SA-2026-0042",
      "status": "final",
      "version": "1",
      "initial_release_date": "2026-10-13T09:00:00Z",
      "current_release_date": "2026-10-13T09:00:00Z",
      "revision_history": [
        {"number": "1", "date": "2026-10-13T09:00:00Z", "summary": "Initial release."}
      ]
    }
  },
  "product_tree": {
    "branches": [{
      "category": "vendor", "name": "Example Corp",
      "branches": [{
        "category": "product_name", "name": "Example Gateway",
        "branches": [
          {"category": "product_version", "name": "4.2.0",
           "product": {"product_id": "EG-4.2.0", "name": "Example Gateway 4.2.0"}},
          {"category": "product_version", "name": "4.2.1",
           "product": {"product_id": "EG-4.2.1", "name": "Example Gateway 4.2.1"}}
        ]
      }]
    }]
  },
  "vulnerabilities": [{
    "title": "Authentication bypass in the admin API",
    "cwe": {"id": "CWE-287", "name": "Improper Authentication"},
    "notes": [
      {"category": "description", "text": "A crafted header lets an unauthenticated request reach admin API routes."}
    ],
    "product_status": {
      "known_affected": ["EG-4.2.0"],
      "fixed": ["EG-4.2.1"]
    },
    "remediations": [
      {"category": "vendor_fix", "details": "Upgrade to Example Gateway 4.2.1.", "product_ids": ["EG-4.2.0"]}
    ]
  }]
}

A few things to notice:

  • product_tree is the backbone. Products are defined once, with stable product_id values, and every other section refers to those IDs. Version branches let a tool match "Example Gateway 4.2.0" to what is installed.
  • tracking is versioned. Each update to the advisory adds a revision_history entry and bumps version and current_release_date. Tools use these to notice that an advisory changed.
  • CVE is optional. If the vulnerability has a CVE, add "cve": "CVE-YYYY-NNNN" to the vulnerability. Many vendors publish before a CVE is assigned.

The filename is not free choice either. Section 5.1 of the standard says to lower-case /document/tracking/id, replace any run of characters outside a-z, 0-9, + and - with a single underscore, and add .json. So EXAMPLE-SA-2026-0042 must be published as example-sa-2026-0042.json.

Check a document before you publish it

This script validates a document against the official schema, prints the filename the standard requires, and adds the two security-advisory profile checks the schema itself doesn't enforce. It needs pip install jsonschema.

#!/usr/bin/env python3
"""Schema-check a CSAF 2.0 document and print the filename the standard requires.
Usage: python3 csaf_check.py advisory.json   (needs: pip install jsonschema)"""
import json, re, sys, urllib.request
from jsonschema import Draft202012Validator

SCHEMA = "https://docs.oasis-open.org/csaf/csaf/v2.0/csaf_json_schema.json"
schema = json.load(urllib.request.urlopen(SCHEMA, timeout=20))
doc = json.load(open(sys.argv[1]))

errors = sorted(Draft202012Validator(schema).iter_errors(doc), key=lambda e: list(e.path))
for e in errors:
    print("SCHEMA", "/" + "/".join(map(str, e.path)), e.message[:120])

# Section 5.1: lower-case the tracking ID, collapse anything outside [+\-a-z0-9] to "_", add .json
filename = re.sub(r"[^+\-a-z0-9]+", "_", doc["document"]["tracking"]["id"].lower()) + ".json"
print("filename:", filename)

# Two profile checks for csaf_security_advisory (section 4.4) that the schema alone does not enforce
profile = []
if doc["document"]["category"] == "csaf_security_advisory":
    for i, v in enumerate(doc.get("vulnerabilities", [])):
        for field in ("notes", "product_status"):
            if field not in v:
                profile.append(f"vulnerabilities[{i}] is missing {field}")
for p in profile:
    print("PROFILE", p)
print(f"schema errors: {len(errors)}, profile errors: {len(profile)}")
sys.exit(1 if errors or profile else 0)

On the example above:

filename: example-sa-2026-0042.json
schema errors: 0, profile errors: 0

On a copy with tracking.status and the vulnerability notes deleted:

SCHEMA /document/tracking 'status' is a required property
filename: example-sa-2026-0042.json
PROFILE vulnerabilities[0] is missing notes
schema errors: 1, profile errors: 1

A schema check is the floor, not the ceiling. The standard defines many more mandatory tests (for example, that every product ID you reference is actually defined). For full conformance, run the open-source csaf_validator from the gocsaf/csaf project, or author documents in Secvisogram, a web editor that validates as you type.

Publishing so tools can find you

A valid document on a random web page is only half the job. CSAF defines roles that describe how complete your publishing is:

RoleWhat it means in practice
CSAF publisherValid documents, correct filenames, served over TLS, TLP:WHITE documents freely accessible
CSAF providerAll of the above, plus access control for TLP:AMBER and TLP:RED, a provider-metadata.json, at least one discovery method, and a predictable distribution (folders or ROLIE feeds)
CSAF trusted providerAll of the above, plus hash files and OpenPGP signatures for each document and a published public key

For a provider, the metadata file is what a tool finds first. The standard gives three discovery routes, and you need at least one:

  1. a CSAF: field in security.txt pointing to provider-metadata.json (the field is in the IANA registry)
  2. the file served at https://<your-domain>/.well-known/csaf/provider-metadata.json
  3. a host named csaf.data.security.<your-domain> that serves it

For the documents themselves, the simplest layout is directory-based. Put each document in a folder named after the year of its initial release, for example 2026/example-sa-2026-0042.json. Next to those folders, keep an index.txt listing every file and a changes.csv listing each file with its current_release_date, newest first. ROLIE feeds are the alternative for teams that already run Atom-style feeds.

See it working: discover a real provider

This script follows the same discovery routes a tool would, and summarises what it finds. It uses only the Python standard library.

#!/usr/bin/env python3
"""Find a vendor's CSAF provider metadata and summarise it.
Usage: python3 csaf_discover.py redhat.com"""
import json, sys, urllib.request

def get(url):
    req = urllib.request.Request(url, headers={"User-Agent": "csaf-discover/1.0"})
    with urllib.request.urlopen(req, timeout=20) as r:
        return r.read().decode("utf-8", "replace")

domain = sys.argv[1]
candidates = []
try:  # requirement 8: CSAF field in security.txt
    for line in get(f"https://{domain}/.well-known/security.txt").splitlines():
        if line.lower().startswith("csaf:"):
            candidates.append(line.split(":", 1)[1].strip())
except Exception:
    pass
candidates += [f"https://{domain}/.well-known/csaf/provider-metadata.json",   # requirement 9
               f"https://csaf.data.security.{domain}/provider-metadata.json"]  # requirement 10 (path assumed)

for url in candidates:
    try:
        meta = json.loads(get(url))
    except Exception as e:
        print(f"  no metadata at {url} ({type(e).__name__})")
        continue
    pub = meta.get("publisher", {})
    print(f"found:      {url}")
    print(f"canonical:  {meta.get('canonical_url')}")
    print(f"publisher:  {pub.get('name')} ({pub.get('category')}), namespace {pub.get('namespace')}")
    print(f"role:       {meta.get('role')}")
    print(f"updated:    {meta.get('last_updated')}")
    print(f"keys:       {len(meta.get('public_openpgp_keys', []))} OpenPGP key(s)")
    for d in meta.get("distributions", []):
        if "directory_url" in d:
            base = d["directory_url"].rstrip("/") + "/"
            try:
                n = len(get(base + "index.txt").splitlines())
                print(f"directory:  {base} ({n} documents in index.txt)")
            except Exception as e:
                print(f"directory:  {base} (index.txt not readable: {type(e).__name__})")
        if "rolie" in d:
            for feed in d["rolie"].get("feeds", []):
                print(f"ROLIE feed: {feed.get('url')} ({feed.get('tlp_label')})")
    break
else:
    print(f"{domain}: no CSAF provider metadata found")

Run against Red Hat at the time of writing, it found the metadata through Red Hat's security.txt and reported:

found:      https://security.access.redhat.com/data/csaf/v2/provider-metadata.json
canonical:  https://security.access.redhat.com/data/csaf/v2/provider-metadata.json
publisher:  Red Hat Product Security (vendor), namespace https://www.redhat.com
role:       csaf_trusted_provider
updated:    2024-07-10T14:00:00.0Z
keys:       1 OpenPGP key(s)
directory:  https://security.access.redhat.com/data/csaf/v2/advisories/ (29968 documents in index.txt)
directory:  https://security.access.redhat.com/data/csaf/v2/vex/ (71329 documents in index.txt)

That is a trusted provider with separate directories for advisories and VEX documents, both with tens of thousands of entries. Your first version can be one advisory in a 2026/ folder with a three-line index.txt. The structure is the same.

Mistakes to avoid

Hand-editing JSON. Generate CSAF from the same source as your human-readable advisory, or author it in an editor that validates. A hand-typed product ID that doesn't match the product tree silently breaks matching for every customer.

Changing a document without a revision. If you correct an affected version range, add a revision_history entry and bump version. Tools cache documents, and an unversioned change may never reach them.

Product names that don't match what customers run. The product tree is only useful if its names and versions line up with what scanners detect. Where you can, add identifiers such as a CPE or a package URL (purl) through the product's product_identification_helper.

Using redirects. Requirement 6 says redirects should not be used, and if they are unavoidable, only HTTP header redirects are allowed. Some clients don't follow anything else.

When CSAF is more than you need

It is fair to say CSAF is heavy for a small open-source library. If your project's advisories already go through GitHub Security Advisories, they flow into the GitHub Advisory Database and OSV, which most scanners consume, and that may be enough. CSAF earns its keep when you ship versioned products to customers who run their own vulnerability management: appliances, on-prem software, firmware, anything where "which versions are affected" is the question every customer asks after every advisory.

For those vendors, the step that pays off first is simple. Publish one valid security advisory in the right place, point your security.txt at your metadata, and your next advisory can be answered by your customers' tools instead of their inboxes.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.