SBOM generation plugin contract v1¶
A plugin reports the contract versions it speaks with
bomify-plugin-<kind> contract (see
contract versions).
The spec for a bomify-plugin-<kind> binary's SBOM generation
subcommand, sbom generate. Here <kind> names a
deployment medium (e.g. helm). It's independent of the
component,
security scanning,
and signing
contracts.
A Go plugin gets all of this from
pkg/plugin's
SBOMCommand, which also checks its own output against the rules below.
Naming and discovery¶
As for the component contract:
bomify-plugin-<kind> (.exe on Windows), installed in
<data-dir>/plugins.
What bomify does¶
bomify sbom compose, merging several sources' SBOMs into one, writes
each source's options to a JSON file and runs sbom generate --config
<file> with the composition file's directory as the working directory
and stdin closed. It reads the SBOM from stdout, rejecting fields
cyclonedx-go doesn't know, and checks it against the rules below.
bomify does no caching or concurrency control around a generation. Don't depend on the binary's other contracts, if it has any.
Commands¶
sbom generate¶
| Flag | Meaning |
|---|---|
--config <file> |
The options, a JSON object whose keys are the plugin's to define. Relative paths in it are relative to the working directory. Reject unknown keys, so a typo fails rather than being ignored. |
Document the options' keys in --help. Any other flags are the
plugin's own, for running it directly (such as --output). With
--config, never read stdin or prompt.
Output¶
On success, print the SBOM to stdout as one CycloneDX JSON document,
spec version 1.5 or later, and exit 0. On failure, exit non-zero;
stderr is free for logs and the error. Nothing else may go to stdout.
The SBOM is the part of a CycloneDX BOM bomify reads, each object
CycloneDX's own, following these rules.
generate-output.schema.json
expresses them, referencing
CycloneDX's schema
for the objects rather than restating it. bomify reads the SBOM with
cyclonedx-go, rejecting
fields it doesn't know.
- Only
metadata,components, anddependencies, besidesbomFormat,specVersion,$schema, andversion: nothing else would survive merging with other SBOMs. metadata.componentdescribes what was generated from (e.g. the chart), with aname, aversion, and apurl.- Every component,
metadata.componentincluded, has anameand apurl, and itsbom-refis that purl. Everything to package is in the top-levelcomponents, with uniquebom-refs. That includes whatmetadata.componentdescribes, if it's to be packaged too, so the two share abom-ref. - No component has nested
components:bomify buildonly packages top-level ones. - Every
dependenciesentry'srefanddependsOnnames abom-refin the document. - No
serialNumberormetadata.timestamp: the same options and the same upstream content produce the same document, so a composed SBOM, and the package built from it, only changes when its content does.
Schemas¶
generate-output.schema.json¶
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/alejandro-velasco/bomify/main/plugins/contracts/sbom/v1/generate-output.schema.json",
"title": "bomify plugin \"sbom generate\" output",
"description": "What a bomify-plugin-<kind> binary's \"sbom generate\" must print to stdout on success, per plugins/contracts/sbom/v1/CONTRACT.md: the subset of a CycloneDX BOM bomify reads, its metadata, components, and dependencies. Each is CycloneDX's own object, referenced from CycloneDX's schema (1.7's definitions, which cover 1.5 and 1.6 too), with bomify's rules beside it. Three rules can't be expressed here: every bom-ref equals its component's purl, bom-refs are unique among components, and every dependencies ref and dependsOn names a bom-ref in the document.",
"type": "object",
"additionalProperties": false,
"required": ["bomFormat", "specVersion", "metadata"],
"properties": {
"$schema": { "type": "string" },
"bomFormat": { "const": "CycloneDX" },
"specVersion": {
"type": "string",
"pattern": "^1\\.([5-9]|[1-9][0-9]+)$",
"description": "CycloneDX 1.5 or later."
},
"version": { "type": "integer" },
"metadata": {
"allOf": [
{ "$ref": "https://cyclonedx.org/schema/bom-1.7.schema.json#/definitions/metadata" },
{
"required": ["component"],
"properties": {
"timestamp": {
"not": {},
"description": "Not allowed: the same inputs produce the same document."
},
"component": {
"allOf": [{ "$ref": "#/definitions/packaged" }, { "required": ["version"] }],
"description": "What the SBOM was generated from, e.g. the chart."
}
}
}
]
},
"components": {
"type": "array",
"items": { "$ref": "#/definitions/packaged" },
"description": "Everything to package, all top-level."
},
"dependencies": {
"type": "array",
"items": { "$ref": "https://cyclonedx.org/schema/bom-1.7.schema.json#/definitions/dependency" },
"description": "What depends on what, by bom-ref; optional."
}
},
"definitions": {
"packaged": {
"description": "A CycloneDX component, identified by its purl, with nothing nested.",
"allOf": [
{ "$ref": "https://cyclonedx.org/schema/bom-1.7.schema.json#/definitions/component" },
{
"required": ["purl", "bom-ref"],
"properties": {
"components": {
"maxItems": 0,
"description": "Nothing nested: bomify build only packages top-level components."
}
}
}
]
}
}
}