Safeguard
Compliance•

CERT-In SBOM Guidelines v2.0: The 21 Data Fields, Mapped to CycloneDX

CERT-In's Technical Guidelines on SBOM, QBOM and CBOM, AIBOM and HBOM (v2.0, 9 July 2025) ask for 21 data fields per component. How binding they are, where each field goes in CycloneDX, why the sample pkg:supplier identifier isn't a valid Package URL type, the VEX and newer BOM sections, and a stdlib checker.

Safeguard Security Team
Security Research
12 min read

CERT-In's Technical Guidelines on SBOM, QBOM and CBOM, AIBOM and HBOM, version 2.0, dated 9 July 2025, are the reference Indian buyers point to when they ask a vendor for an SBOM. They list 21 data fields for every software component, and several of them, such as criticality and end-of-life date, can't come from a scanner at all. The guidelines also give a sample identifier format that doesn't match the Package URL standard most tools use. Neither problem is hard to fix once you see it.

This post is for software vendors selling to Indian government departments, public sector undertakings, banks and other essential services, and for the security teams on the buying side. It covers who the guidelines are aimed at and how binding they are, the 21 SBOM fields and where each fits in CycloneDX, the identifier issue, what the guidelines say about VEX and the newer BOM types, and a script that checks a CycloneDX SBOM against the 21 fields.

The short answer

CERT-In's BOM guidelines (version 2.0, 9 July 2025) are recommendations, not a regulation: they encourage government, public sector and essential services organisations to make SBOMs a mandatory part of software procurement, and they apply to software consumers, developers and system integrators. Each SBOM component should carry 21 data fields, from name, version and supplier to patch status, end-of-life date, criticality and a unique identifier, in SPDX or CycloneDX. Eight of the fields have no native CycloneDX field, so vendors need a convention such as named properties. The guidelines' sample identifier uses pkg:supplier/..., which isn't a registered Package URL type. Use standard PURLs, and record a CERT-In-style identifier separately if a buyer asks for one.

Who the guidelines apply to, and how binding they are

CERT-In addresses the guidelines especially to government, the public sector, essential services organisations, and companies in software exports and software services. Within those, it names three roles: software consumers, software developers, and system integrators or resellers.

The language is mostly advisory. Departments and organisations are "encouraged" to make SBOMs a mandatory standard practice in procurement and development, and the document recommends that government, public sector and essential services organisations include SBOM requirements in all software purchases. The recommendations in section 7.1 do use "must" (software supplied to government, public sector and essential services organisations "must be accompanied by a complete SBOM"), but they sit in a recommendations chapter of a technical guideline, so the obligation reaches vendors through purchase requirements. Sector regulators can also set their own SBOM rules. SEBI's cybersecurity framework, for example, separately makes SBOMs mandatory for its regulated entities, which our SEBI CSCRF SBOM guide covers.

The guidelines also expect SBOMs to stay current: they should be produced for every new release and updated for any change, including when new information about an unchanged component appears.

The 21 SBOM data fields, mapped to CycloneDX

Table 5 of the guidelines lists the data fields. CERT-In accepts SPDX or CycloneDX but doesn't map the fields to either. Here's our mapping to CycloneDX 1.6 JSON. Where there's no native field, we use a component property named cert-in: plus the field, such as cert-in:origin. That's a convention, not a standard:

#CERT-In fieldCycloneDX location
1Component Namecomponents[].name
2Component Versioncomponents[].version
3Component Descriptioncomponents[].description
4Component Suppliercomponents[].supplier (or publisher, manufacturer)
5Component Licensecomponents[].licenses
6Component Origin (proprietary, open source, third-party vendor)property cert-in:origin
7Component Dependenciesdependencies[] entry for the component's bom-ref
8Vulnerabilitiesvulnerabilities[] with affects pointing at the component
9Patch Statusvulnerabilities[].analysis, or property cert-in:patch-status
10Release Dateproperty cert-in:release-date (releaseNotes.timestamp is close, but it dates the release note)
11End-of-Life (EOL) Dateproperty cert-in:eol-date
12Criticalityproperty cert-in:criticality
13Usage Restrictions (export control, IP rights)property cert-in:usage-restrictions
14Checksums or Hashescomponents[].hashes
15Comments or Notesannotations[] with subjects pointing at the component (1.5 and later), or property cert-in:comments
16Author of SBOM Datametadata.authors, metadata.manufacturer or metadata.tools (in 1.5 and later the object form; the array form is deprecated)
17Timestampmetadata.timestamp
18Executable Propertyproperty cert-in:executable
19Archive Propertyproperty cert-in:archive
20Structured Propertyproperty cert-in:structured
21Unique Identifiercomponents[].purl

The guidelines also set out "Automation Support" (the SPDX and CycloneDX formats) and "Practices and Processes": frequency, depth, known unknowns, distribution and delivery, access control and accommodation of mistakes. Those are about how you run an SBOM programme, not what's in the file.

The fields that generators rarely fill are the ones that need a human or another system: origin, criticality, end-of-life date, usage restrictions and patch status. Criticality, in particular, is about the component's importance to your application, which no scanner can infer.

The identifier problem

The guidelines describe the unique identifier as a Package URL and give this example:

pkg:supplier/ApacheSoftwareFoundation/ApacheTomcat@9.0.71?arch=x86_64&os=linux#server/webapps

The Package URL specification has no supplier type. Its types directory defines 42, from maven and npm to generic, and tools that validate PURLs or match them against vulnerability data expect one of those. The embedded Tomcat artifact from Maven Central, in standard PURL form, is:

pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@9.0.71

The guidelines' intent, a stable identifier that survives a rename or a change of supplier, is a good one. The practical answer is to emit standard PURLs, which is what vulnerability databases key on, use pkg:generic/... for components that don't come from a package ecosystem, and if a buyer explicitly wants the supplier-style identifier, add it as a property rather than replacing the PURL.

VEX, CSAF and the other BOMs

Section 6 asks suppliers to publish VEX documents after a vulnerability is found, with one of four statuses (not affected, affected, fixed, under investigation), updated as the analysis changes, followed by a CSAF advisory with the description, affected versions, severity and mitigations. That's the same model CycloneDX and CSAF already support, and it fills fields 8 and 9 in a way buyers can process.

Version 2.0 added four more BOM types, each with its own minimum elements:

  • QBOM and CBOM (section 8): cryptographic assets such as algorithms, keys and protocols, with fields like primitive, mode, classical security level and OID, plus a quantum-readiness and migration strategy.
  • AIBOM (section 9): model name, version, type and developer, licensing, dependencies, datasets and data sources, performance metrics, hardware, security requirements, inputs, outputs and intended use.
  • HBOM (section 10): product and manufacturer details, manufacturing date, supplier information, model and serial numbers, technical specifications and compliance declarations.

Most buyers start with the SBOM. Section 8 ties CBOM to post-quantum migration planning, so buyers with long-lived cryptography are likely to ask for it.

Check an SBOM against the 21 fields

This script reads a CycloneDX JSON SBOM and reports, for each component, how many of the 21 fields it fills, which are missing, and whether the identifier uses a registered PURL type. It uses only the Python standard library and exits with status 1 if any component is missing a field:

#!/usr/bin/env python3
"""Report which of CERT-In's 21 SBOM data fields (Technical Guidelines v2.0,
Table 5) a CycloneDX JSON SBOM fills, per component.

The CycloneDX mapping is ours, not CERT-In's. Fields with no native CycloneDX
home are read from component properties named "cert-in:<field>".

Usage: python3 certin_sbom_check.py sbom.cdx.json"""
import json
import sys

# Registered PURL types (package-url/purl-spec, types/ directory)
PURL_TYPES = {"alpm", "apk", "bazel", "bitbucket", "bitnami", "brew", "cargo",
              "chrome-extension", "cocoapods", "composer", "conan", "conda", "cpan",
              "cran", "deb", "docker", "gem", "generic", "git", "github", "golang",
              "hackage", "hex", "huggingface", "julia", "luarocks", "maven", "mlflow",
              "npm", "nuget", "oci", "opam", "otp", "pub", "pypi", "qpkg", "rpm",
              "swid", "swift", "vcpkg", "vscode-extension", "yocto"}


def prop(c, name):
    return any(p.get("name") == f"cert-in:{name}" and p.get("value")
               for p in c.get("properties", []))


def check(bom):
    meta = bom.get("metadata", {})
    deps = {d["ref"] for d in bom.get("dependencies", []) if d.get("dependsOn") is not None}
    vuln_refs = {a.get("ref") for v in bom.get("vulnerabilities", []) for a in v.get("affects", [])}
    patched = {a.get("ref") for v in bom.get("vulnerabilities", []) if v.get("analysis", {}).get("state")
               for a in v.get("affects", [])}
    has_vuln_section = "vulnerabilities" in bom
    noted = {s for a in bom.get("annotations", []) for s in a.get("subjects", [])}
    sbom_author = bool(meta.get("authors") or meta.get("manufacturer") or meta.get("tools"))
    rows = []
    for c in bom.get("components", []):
        ref = c.get("bom-ref")
        purl = c.get("purl", "")
        ptype = purl[4:].split("/", 1)[0] if purl.startswith("pkg:") else ""
        fields = {
            "Component Name": bool(c.get("name")),
            "Component Version": bool(c.get("version")),
            "Component Description": bool(c.get("description")),
            "Component Supplier": bool(c.get("supplier") or c.get("publisher") or c.get("manufacturer")),
            "Component License": bool(c.get("licenses")),
            "Component Origin": prop(c, "origin"),
            "Component Dependencies": ref in deps,
            "Vulnerabilities": ref in vuln_refs or (has_vuln_section and prop(c, "vulnerabilities")),
            "Patch Status": ref in patched or prop(c, "patch-status"),
            "Release Date": prop(c, "release-date"),
            "End-of-Life (EOL) Date": prop(c, "eol-date"),
            "Criticality": prop(c, "criticality"),
            "Usage Restrictions": prop(c, "usage-restrictions"),
            "Checksums or Hashes": bool(c.get("hashes")),
            "Comments or Notes": ref in noted or prop(c, "comments"),
            "Author of SBOM Data": sbom_author or bool(c.get("authors")),
            "Timestamp": bool(meta.get("timestamp")),
            "Executable Property": prop(c, "executable"),
            "Archive Property": prop(c, "archive"),
            "Structured Property": prop(c, "structured"),
            "Unique Identifier": ptype in PURL_TYPES,
        }
        rows.append((c.get("name", "?"), c.get("version", "?"), purl, fields))
    return rows


def main(path):
    rows = check(json.load(open(path, encoding="utf-8")))
    missing_any = False
    for name, version, purl, fields in rows:
        have = sum(fields.values())
        gaps = [f for f, ok in fields.items() if not ok]
        missing_any |= bool(gaps)
        print(f"{name} {version}: {have}/21 fields")
        if purl and not fields["Unique Identifier"]:
            print(f"  identifier {purl!r} does not use a registered PURL type")
        if gaps:
            print("  missing: " + ", ".join(gaps))
    totals = {}
    for *_, fields in rows:
        for f, ok in fields.items():
            totals[f] = totals.get(f, 0) + ok
    weakest = sorted(totals, key=totals.get)[:5]
    print(f"{len(rows)} components; least covered: " + ", ".join(f"{f} ({totals[f]})" for f in weakest))
    return 1 if missing_any else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1]))

On a small sample SBOM with three components: one as a generator might emit it, one enriched with cert-in: properties, and one vendor SDK using the guidelines' identifier style:

tomcat-embed-core 11.0.26: 9/21 fields
  missing: Component Description, Component Origin, Vulnerabilities, Patch Status, Release Date, End-of-Life (EOL) Date, Criticality, Usage Restrictions, Comments or Notes, Executable Property, Archive Property, Structured Property
postgresql 42.7.8: 16/21 fields
  missing: Vulnerabilities, Patch Status, End-of-Life (EOL) Date, Usage Restrictions, Comments or Notes
VendorPay SDK 7.17.0: 4/21 fields
  identifier 'pkg:supplier/VendorPay/VendorPaySDK@7.17.0' does not use a registered PURL type
  missing: Component Description, Component Supplier, Component License, Component Origin, Component Dependencies, Vulnerabilities, Patch Status, Release Date, End-of-Life (EOL) Date, Criticality, Usage Restrictions, Checksums or Hashes, Comments or Notes, Executable Property, Archive Property, Structured Property, Unique Identifier
3 components; least covered: Vulnerabilities (0), Patch Status (0), End-of-Life (EOL) Date (0), Usage Restrictions (0), Comments or Notes (0)

The generator-style component fills 9 of 21 fields. Enrichment gets the second one to 16, and what's left (vulnerabilities, patch status, end-of-life date, usage restrictions, notes) needs a vulnerability scan, a VEX statement and a decision from someone who knows the product. The sample is illustrative. Run the script on your own SBOMs to see your real gaps.

Generating the base SBOM is the easy part, and enrichment builds on it. Here's Safeguard's interactive walkthrough of generating an SBOM:

What to do

  1. Vendors: generate a CycloneDX or SPDX SBOM per release, then run the check and decide how you'll fill each missing field. Write the convention down so every release uses the same property names.
  2. Keep standard PURLs as the identifier, and add the CERT-In-style one only if a buyer asks.
  3. Publish VEX for vulnerabilities that affect, or don't affect, your product, and keep it current. It's the cleanest way to fill vulnerabilities and patch status.
  4. Buyers: say which fields you need and in what form. A procurement clause that only says "per CERT-In guidelines" invites 21 different interpretations.
  5. Plan for CBOM. If you sell to government, public sector or essential services organisations, an inventory of your cryptographic algorithms and keys is the next question.

The fair view

CERT-In's guidelines are broad, practical and aimed at the right people: the consumers who have to ask for SBOMs, not just the developers who produce them. Asking for criticality, end-of-life dates and patch status pushes SBOMs toward being useful for risk decisions, not just component lists, and the VEX and CSAF guidance matches what the rest of the world is converging on.

The gaps are in the translation to files. Twenty-one fields without a format mapping, and an identifier example that doesn't follow the standard it's named after, leave every vendor and buyer to invent their own conventions. Until a mapping is published, the safest course is to agree the format with each buyer, document it, and keep the standard parts standard.

Check yourself

Are CERT-In's SBOM guidelines legally mandatory for software vendors?

Not by themselves. They encourage government, public sector and essential services organisations to make SBOMs mandatory in procurement. Vendors meet them through those purchase requirements. Sector regulators can set their own SBOM rules too: SEBI's framework, for example, separately makes SBOMs mandatory for its regulated entities.

Should you replace pkg:maven PURLs with the pkg:supplier format from the guidelines' example?

No. supplier isn't a registered Package URL type, so tools that validate PURLs or match them to vulnerability data won't recognise it. Keep standard PURLs and add a supplier-style identifier as a separate property if a buyer specifically asks for it.

Which of the 21 fields can't a scanner fill on its own?

Criticality, end-of-life date, usage restrictions, origin and comments need human or business input, and patch status depends on vulnerability analysis, ideally published as VEX. Name, version, PURL, hashes, licences and dependencies are usually generated.

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.