Skip to contents

Checks an SBOM file against the schema of its format, and checks the content of a file or of an <sbom> object against integrity rules and the NTIA 2021 minimum elements. A file can come from sbom or from any other tool, in any of the eight formats that read_sbom() reads.

Usage

validate_sbom(
  x,
  checks = c("schema", "content"),
  profile = "ntia-2021",
  error_on = c("never", "error", "warning")
)

# S3 method for class 'sbom_validation'
print(x, ...)

# S3 method for class 'sbom_validation'
as.data.frame(x, ...)

Arguments

x

For validate_sbom(), the path of an SBOM file, or an <sbom> object. For the methods, an <sbom_validation>.

checks

The layers to run: "schema", "content" or both. An <sbom> object gets the content checks only.

profile

The content profile: "ntia-2021" checks the NTIA 2021 minimum elements as well as the integrity rules, and "none" checks only the integrity rules.

error_on

When to signal an error: "never" returns the result whatever it holds, "error" signals an error when any finding is an error, and "warning" when any finding is an error or a warning. The error has class sbom_validation_error, and its field validation holds the result. Use it in scripts and continuous integration.

...

Ignored.

Value

validate_sbom() returns an <sbom_validation> object, a list:

  • status: "pass", "fail", "partial" or "skipped".

  • findings: a data frame with one row per finding and the columns check, severity ("error", "warning" or "info"), location and message (a sentence). The location is a JSON pointer into the document, such as /packages/3, or "" for the document as a whole. A file that is not JSON was read into a JSON model, into which the pointer points: the CycloneDX JSON model for CycloneDX XML, and the SPDX JSON model for SPDX YAML, XML and tag-value. An XSD finding (CycloneDX XML, and SWID with the option sbom.swid_xsd) has the location "", since xml2 does not say where libxml2 found the problem. A finding of sbom's structural rules for SWID has an XPath-like location, such as /SoftwareIdentity/Entity[2] for the second Entity. For an <sbom> object, the location is "component <ref>" or a metadata field. The findings are ordered by severity, the schema findings first, then by check and by location in document order.

  • format and version: the format, as in sbom_formats(), and its version; NA for an object or a file that could not be read.

  • file: the file's name, without its folder; NA for an object.

  • profile: the content profile.

  • checks: the layers asked for; for an object, "content".

print() reports the findings for a reader, grouped by severity and check, with a hint for each check, and returns x invisibly. It names the profile only when the content layer was asked for, since no other layer applies one. Control characters in the text it shows, which a hostile file could use to rewrite the terminal, are shown as spaces, as they are in the message of the error_on error. as.data.frame() returns the findings, for scripts and for review.

Details

validate_sbom() runs two layers of checks on a file.

The schema layer checks the file against the schema of its format and version. sbom bundles the schemas, except the ISO schema of SWID tags, so validation works offline:

  • CycloneDX JSON, SPDX JSON and YAML, and SPDX 3 JSON-LD are checked with jsonvalidate, which must be installed.

  • CycloneDX XML is checked against its XSD with xml2.

  • SPDX has no official schema for XML or tag-value, so those files are read into the SPDX JSON model and checked against the SPDX JSON schema. At best they are partial.

  • SPDX 3 is also partial at best, since SPDX's SHACL rules are not checked.

  • sbom does not bundle the ISO/IEC 19770-2:2015 schema of SWID tags, since ISO's notice does not clearly allow it to be redistributed. So a SWID tag is checked against sbom's structural rules, and is partial at best. They are no stricter than the ISO schema: the root is SoftwareIdentity in the 2015 namespace, with a name and a tagId; every Entity has a name and a role, and every Link an href and a rel; the roles, rel and versionScheme are tokens, and tagVersion an integer. As the standard requires, an Entity must also have the role tagCreator.

    To check a tag fully, download the ISO schema from https://standards.iso.org/iso/19770/-2/2015-current/schema.xsd and set the option sbom.swid_xsd to the path of the file. sbom accepts only that file, by its SHA-256: as ISO publishes it, with CRLF line endings, or with LF line endings, as git or an editor may leave it. It checks the tag against it with xml2, giving pass or fail, and against the rule that it has a tagCreator. sbom bundles the two W3C schemas that the ISO schema imports, so the check needs no network. With any other value, the option gives the structural rules, and a note that says where to download the file; the note is not an error, even when the tag fails the structural rules.

A version with no bundled schema, such as CycloneDX 1.3, is not checked, and a note says so. Each finding of this layer has the check schema. It is an error when the file fails its schema, and otherwise a note that says what could not be checked, or why a SWID tag got the structural rules.

The content layer reads the file as read_sbom() does, and checks the values the document itself holds, not the <sbom> that read_sbom() makes. So nothing read_sbom() leaves out can hide a problem. The integrity checks always run: unique ids, relationships that name elements of the document, license expressions against the bundled SPDX License List, purls, and the names and versions of R packages. The profile adds its own checks. The layer fails when any of its findings is an error; warnings and notes do not fail it.

When the file cannot be read, as when it is not an SBOM, the result is fail with one finding, of check read, and nothing else runs.

An <sbom> object gets the content checks only, on its components, relationships and metadata, since it has no file to check against a schema. With the default checks the schema layer is left out, and asking for it is an error. To check both, write the object with write_sbom() and validate the file. An <sbom> from read_sbom() holds values that read_sbom() supplies, such as a timestamp when the document has none, a placeholder root and sbom as the tool, and lacks those it cannot keep, such as CPE identifiers, which may be a component's only identifier. Either changes the result, so validate the file itself to check the document as it was written.

Statuses

Each layer that runs gives a status, and the result's status joins them:

  • "fail": a layer failed. The file could not be read, it fails its schema, or its content has an error.

  • "skipped": no layer failed, and every layer that ran was skipped. A schema layer is skipped when jsonvalidate is not installed, when sbom bundles no schema for the version, when the document is too large, when the JSON schema check could not run, or when an XML document uses entities declared in its DTD, which sbom does not expand.

  • "partial": no layer failed, but one was skipped or could check only part of the rules (SPDX XML, SPDX tag-value and SPDX 3, and SWID unless the option sbom.swid_xsd names the ISO schema).

  • "pass": every layer that ran passed.

The NTIA minimum elements

The "ntia-2021" profile checks the minimum elements of the NTIA's 2021 report, "The Minimum Elements For a Software Bill of Materials". A value is present when it is not empty and not NOASSERTION. A value asserted with an override counts like any other. The component checks apply to components, not to files and snippets (SPDX files, and CycloneDX components of type file). The relationships are those of the primary component, the one the document describes. Each element is read from these fields:

ElementCycloneDXSPDX 2SPDX 3SWID
Suppliersupplier, manufacturer, publisher or an authorsupplier or originatorsuppliedBy or originatedBya distributor, softwareCreator or maintainer entity
Namenamenamenamename
VersionversionversionInfosoftware_packageVersionversion
Identifierpurl, cpe or swida purl, CPE or SWID referencea purl, CPE or SWID identifiertagId
Relationshipsits dependencies entry, or nested componentsany but DESCRIBESany but describesrequires and component links
Authormetadata.authors or metadata.manufacturera Person: or Organization: creatora person or organization in createdBythe tagCreator entity
Timestampmetadata.timestampcreationInfo.createdcreatednone

A document that names only tools as its authors (CycloneDX tools, SPDX 2 Tool: creators, SPDX 3 software agents) gets a warning rather than an error, since the NTIA asks for the person or organization that made the SBOM data. For an <sbom> object, the elements are each component's supplier_name or maintainer, name, version, purl and relationships, and the metadata's author or organization and timestamp; sbom itself is a tool author.

A SWID tag describes only itself, so only the tag gets the component checks, and a note counts the tags it links to. It has no field for the time it was made, so a SWID tag cannot meet the NTIA minimum elements: under "ntia-2021" it always fails with an ntia-timestamp error.

Checks

Each finding has one of these checks:

  • read (error): the file is not an SBOM in a format sbom reads.

  • schema: an error where the file fails its schema; otherwise a note.

  • duplicate-id (error): elements share an id.

  • dangling-relationship (error): a relationship names an element the document does not define. An element of another document is not checked: one named through an SPDX DocumentRef- or an SPDX 3 import, or a CycloneDX BOM-Link (urn:cdx:...), as in a VEX document.

  • license-invalid (error): a license expression is not a valid SPDX expression.

  • license-unknown (error): it uses an id that is not on the SPDX License List.

  • license-deprecated (warning): it uses a deprecated SPDX id.

  • license-ref-undefined (error, SPDX 2 only): it uses a LicenseRef- that the document does not define in hasExtractedLicensingInfos.

  • purl-invalid (error): a purl does not parse.

  • r-package-name (error): a component whose purl says it is a CRAN package has a name that is not a valid R package name.

  • r-version (warning): such a component has a version that is not an R package version.

  • r-purl-name (warning): its purl names another package.

  • ntia-author, ntia-timestamp, ntia-supplier, ntia-name, ntia-version, ntia-identifier and ntia-relationships (errors): an NTIA minimum element is missing. ntia-author is a warning when the document names only tools.

  • swid-links (note): the tags a SWID tag links to were not checked.

Limits

  • The JSON schema check runs ajv inside V8, and its time grows quadratically with the length of a CycloneDX component array: about 45 seconds at 20,000 components. The content layer takes seconds; use checks = "content" to leave the schema out for a large SBOM.

  • V8 cannot hold a very long string, and trying aborts R. A document whose JSON, escaped for V8, is longer than 32 MB is not checked against its schema: the schema layer is skipped, with a note, and the content checks still run.

  • SPDX YAML nested more than 200 levels deep, or holding more than one YAML document, is refused before it is parsed, from a scan of its text that follows libyaml's rules. A crafted SPDX YAML file can still take tens of seconds to refuse.

  • The JSON schema check calls the validator that jsonvalidate keeps inside V8, which is not part of jsonvalidate's documented interface. If a version of jsonvalidate changes it, the schema layer reports skipped, with a note, never pass.

See also

read_sbom() to read an SBOM, and write_sbom() to write one.

Examples

x <- sbom_package(system.file("DESCRIPTION", package = "sbom"), system = FALSE)
path <- file.path(tempdir(), "sbom.cdx.xml")
write_sbom(x, path)
v <- validate_sbom(path)
v
#> CycloneDX XML 1.6, sbom.cdx.xml: FAIL
#> Profile: ntia-2021.
#> 
#> ── Errors ──
#> 
#> ✖ ntia-identifier: 4 findings
#>   • R 4.6.2 has no purl, CPE or SWID identifier. (/components/0)
#>   • methods 4.6.2 has no purl, CPE or SWID identifier.
#>     (/components/0/components/0)
#>   • utils 4.6.2 has no purl, CPE or SWID identifier.
#>     (/components/0/components/1)
#>   … and 1 more.
#>   ℹ Give each component a purl, CPE or SWID tag id; if sbom made this SBOM,
#>     assert a purl it cannot find with an override of purl.
#> ✖ ntia-supplier: 3 findings
#>   • R 4.6.2 has no supplier. (/components/0)
#>   • methods 4.6.2 has no supplier. (/components/0/components/0)
#>   • utils 4.6.2 has no supplier. (/components/0/components/1)
#>   ℹ State each component's supplier; if sbom made this SBOM, assert the ones it
#>     cannot find with an override of supplier_name.
#> 
#> ── Warnings ──
#> 
#> ! The document names only a tool (sbom) as its author; NTIA asks for the person
#>   or organization that made the SBOM data. (/metadata/tools/components/0)
#>   ℹ Name the person or organization that made the SBOM data as its author; if
#>     sbom made this SBOM, give its author or organization argument.
#> 
#> Use as.data.frame() to see every finding.
as.data.frame(v)
#>             check severity                     location
#> 1 ntia-identifier    error                /components/0
#> 2 ntia-identifier    error   /components/0/components/0
#> 3 ntia-identifier    error   /components/0/components/1
#> 4 ntia-identifier    error          /metadata/component
#> 5   ntia-supplier    error                /components/0
#> 6   ntia-supplier    error   /components/0/components/0
#> 7   ntia-supplier    error   /components/0/components/1
#> 8     ntia-author  warning /metadata/tools/components/0
#>                                                                                                                  message
#> 1                                                                           R 4.6.2 has no purl, CPE or SWID identifier.
#> 2                                                                     methods 4.6.2 has no purl, CPE or SWID identifier.
#> 3                                                                       utils 4.6.2 has no purl, CPE or SWID identifier.
#> 4                                                                        sbom 0.1.0 has no purl, CPE or SWID identifier.
#> 5                                                                                               R 4.6.2 has no supplier.
#> 6                                                                                         methods 4.6.2 has no supplier.
#> 7                                                                                           utils 4.6.2 has no supplier.
#> 8 The document names only a tool (sbom) as its author; NTIA asks for the person or organization that made the SBOM data.

# An <sbom> object gets the content checks only.
validate_sbom(x)
#> <sbom> object: FAIL
#> Profile: ntia-2021.
#> 
#> ── Errors ──
#> 
#> ✖ ntia-identifier: 4 findings
#>   • R 4.6.2 has no purl, CPE or SWID identifier. (component R)
#>   • methods 4.6.2 has no purl, CPE or SWID identifier. (component R/methods)
#>   • utils 4.6.2 has no purl, CPE or SWID identifier. (component R/utils)
#>   … and 1 more.
#>   ℹ Give each component a purl, CPE or SWID tag id; if sbom made this SBOM,
#>     assert a purl it cannot find with an override of purl.
#> ✖ ntia-supplier: 3 findings
#>   • R 4.6.2 has no supplier. (component R)
#>   • methods 4.6.2 has no supplier. (component R/methods)
#>   • utils 4.6.2 has no supplier. (component R/utils)
#>   ℹ State each component's supplier; if sbom made this SBOM, assert the ones it
#>     cannot find with an override of supplier_name.
#> 
#> ── Warnings ──
#> 
#> ! The document names only a tool (sbom) as its author; NTIA asks for the person
#>   or organization that made the SBOM data. (metadata$tool_version)
#>   ℹ Name the person or organization that made the SBOM data as its author; if
#>     sbom made this SBOM, give its author or organization argument.
#> 
#> Use as.data.frame() to see every finding.