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 classsbom_validation_error, and its fieldvalidationholds 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 columnscheck,severity("error","warning"or"info"),locationandmessage(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 optionsbom.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 secondEntity. 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.formatandversion: the format, as insbom_formats(), and its version;NAfor an object or a file that could not be read.file: the file's name, without its folder;NAfor 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
partialat 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
partialat best. They are no stricter than the ISO schema: the root isSoftwareIdentityin the 2015 namespace, with anameand atagId; everyEntityhas anameand arole, and everyLinkanhrefand arel; the roles,relandversionSchemeare tokens, andtagVersionan integer. As the standard requires, anEntitymust also have the roletagCreator.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_xsdto 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, givingpassorfail, and against the rule that it has atagCreator. 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 optionsbom.swid_xsdnames 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:
| Element | CycloneDX | SPDX 2 | SPDX 3 | SWID |
| Supplier | supplier, manufacturer, publisher or an author | supplier or originator | suppliedBy or originatedBy | a distributor, softwareCreator or maintainer entity |
| Name | name | name | name | name |
| Version | version | versionInfo | software_packageVersion | version |
| Identifier | purl, cpe or swid | a purl, CPE or SWID reference | a purl, CPE or SWID identifier | tagId |
| Relationships | its dependencies entry, or nested components | any but DESCRIBES | any but describes | requires and component links |
| Author | metadata.authors or metadata.manufacturer | a Person: or Organization: creator | a person or organization in createdBy | the tagCreator entity |
| Timestamp | metadata.timestamp | creationInfo.created | created | none |
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 SPDXDocumentRef-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 aLicenseRef-that the document does not define inhasExtractedLicensingInfos.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-identifierandntia-relationships(errors): an NTIA minimum element is missing.ntia-authoris 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, neverpass.
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.