Skip to content

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.

  1. Only metadata, components, and dependencies, besides bomFormat, specVersion, $schema, and version: nothing else would survive merging with other SBOMs.
  2. metadata.component describes what was generated from (e.g. the chart), with a name, a version, and a purl.
  3. Every component, metadata.component included, has a name and a purl, and its bom-ref is that purl. Everything to package is in the top-level components, with unique bom-refs. That includes what metadata.component describes, if it's to be packaged too, so the two share a bom-ref.
  4. No component has nested components: bomify build only packages top-level ones.
  5. Every dependencies entry's ref and dependsOn names a bom-ref in the document.
  6. No serialNumber or metadata.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."
            }
          }
        }
      ]
    }
  }
}