Skip to content

Plugins

A plugin binary can implement any of four independent contracts:

Contract Subcommands Used by <kind> names
Component component pull/push/remote build, distribute a purl type
SBOM generation sbom generate sbom generate a deployment medium
Security scanning security scan/supported-components security scan, scan on pull a scanner
Signing signature sign/attest/verify/verify-attestation/supported-types --sign, --verify a signing scheme

Every call bomify makes to a plugin goes through plugin.Invoke: run the binary, parse stdout as the contract's JSON result, and fold stderr into the error on failure. plugin.Find, which locates the binary first, refuses one that doesn't speak the contract version bomify needs (see Plugin installation). On the plugin side, pkg/plugin's ComponentCommand, SecurityCommand, and SignatureCommand build each contract's subcommands (flags, validation, logging, output) around a small Go interface, so a first-party plugin's cmd package is only an adapter over its internal logic. NewRootCommand adds the contract subcommand every plugin answers with the version of each contract it speaks (see contract versions).

Component plugins

For each SBOM component, bomify build and bomify distribute:

  1. Detect the kind from the purl type (plugin.Detect): pkg:oci/... is oci. A missing or unparseable purl fails.
  2. Find bomify-plugin-<kind> in plugins/. The exception is pkg:bomify-plugin/...: a plugin binary itself, which build copies directly (see Plugin installation) and distribute skips.
  3. Delegate: component pull --purl --output or component push --purl --input --remote. The plugin prints one JSON {outputPath, message, hash}, where hash is the SHA-256 of what it pulled. distribute first calls component remote to learn the component's origin, then picks the destination: --remote <kind>=<endpoint>, else the best conf/distribution.json rule matching that origin (internal/distribution).

stdout is reserved for the result and stderr for one fatal message. Plugins log to --log, logs/<purlHash>.log, which bomify creates fresh before each call, streams to its own stdout under --verbose (prefixed [<verb> <purl>]), and deletes afterwards. --log-color passes on whether bomify's stdout is a terminal.

--check on build/distribute passes --check=true instead of --output/--input, asking the plugin to cheaply confirm the transfer would succeed. plugin.CheckPull/CheckPush are stateless: no pid file, manifest, or layer directory, and build --check records no build.

Plugin dispatch sequence

Source: docs/diagrams/plugin-dispatch.mmd

SBOM generation

A plugin's sbom generate takes its options as a --config file, which it validates itself. Its SBOM is the part of a CycloneDX BOM bomify reads (metadata, components, dependencies), following rules strict enough to merge several plugins' SBOMs into one: every component identified by its purl, nothing nested, no timestamp. pkg/plugin's SBOMCommand implements both sides for a Go plugin, and its ValidateGenerated checks the rules; cyclonedx-go itself checks the CycloneDX objects, as bomify decodes them rejecting unknown fields.

bomify sbom compose is its caller (internal/sbom's Composition). It finds every source's plugin before running any, reads each SBOM strictly (sbom.DecodeStrict), then merges them: components by purl (sources disagreeing on one's type, name, version, or hashes fail), dependencies unioned, and the composition as the root, depending on each source's root. Everything is sorted and the spec version pinned, so an unchanged composition yields the same SBOM, and bomify build the same package.

Concurrent, idempotent pulls

plugin.Pull is safe to call concurrently, even across processes, for components sharing a purl hash:

  • manifests/<purlHash>.pid holds the PID of the process pulling it. A second caller waits until the file is gone or its process is dead.
  • A stale pid file (process gone) is discarded and the pull redone.
  • If a manifest exists and nothing is pulling, the result is reused. A reused or fresh result is always checked against the SBOM's declared hash, so a same-purl component with a different hash never slips through.