Skip to content

Plugins

The first-party plugins. Each directory is a standalone bomify-plugin-<kind> binary implementing one or more contracts:

  • Component (component pull|push|remote, contracts/component/v1): fetches and publishes the components an SBOM describes.
  • SBOM generation (sbom generate, contracts/sbom/v1): builds an SBOM for a deployment medium.
  • Security scanning (security scan|supported-components, contracts/security/v1): reports the vulnerabilities a purl is affected by.
  • Signing (signature sign|attest|verify|verify-attestation|supported-types, contracts/signing/v1): signs and verifies whole packages and their build provenance.
Plugin Handles Built on Contracts
bomify-plugin-oci pkg:oci, pkg:docker go-containerregistry Component
bomify-plugin-helm pkg:helm; Helm charts Helm SDK Component, SBOM generation
bomify-plugin-generic pkg:generic net/http Component
bomify-plugin-huggingface pkg:huggingface models; Hub models hf-hub, Hugging Face's Rust client for the Hub and Xet Component, SBOM generation
bomify-plugin-grype most purl types, images, and generic files grype Security scanning
bomify-plugin-sigstore Sigstore bundles sigstore-go Signing

Contract versions

Each contract has a major version, in its spec's title (e.g. "Signing plugin contract v1"). Every first-party plugin speaks version 1 of the contracts it implements. Changes follow this policy:

  • Minor: adding an optional flag, command, or result field keeps the version. Plugins and bomify written before it ignore it.
  • Major: removing or renaming anything, or changing what it means, bumps the version. bomify refuses a plugin that doesn't speak the version it needs (see below).

Each version has its own folder, holding its spec and the JSON Schemas of its results:

plugins/contracts/
├── contract-result.schema.json    the "contract" command's result, below
├── component/v1/                  CONTRACT.md, result, remote-result
├── sbom/v1/                       CONTRACT.md, generate-output
├── security/v1/                   CONTRACT.md, security-result, supported-components-result
└── signing/v1/                    CONTRACT.md, sign-result, verify-result,
                                   verify-attestation-result, supported-signature-types-result

A new version is a new folder (v2/) alongside the old one, which stays as the spec of what plugins speaking it still do.

Every plugin reports the versions it speaks:

bomify-plugin-<kind> contract

No flags. Print one JSON object to stdout and exit 0; it must always report the same thing. bomify asks before it first calls a plugin, and refuses one that doesn't speak the version it needs, or can't answer.

{ "contracts": { "component": 1, "sbom": 1 } }
Field Type Required Meaning
contracts object yes Each contract the plugin implements (component, sbom, security, or signing), mapped to the major version it speaks. Contracts it doesn't implement are absent.

Schema: contract-result.schema.json.

A Go plugin gets this for free: pkg/plugin's NewRootCommand adds contract, reporting the version pkg/plugin implements for each contract subcommand (component, sbom, security, signature) the plugin registers. Other plugins implement it themselves.

Installing plugins

bomify only looks for plugins in <data-dir>/plugins (~/.bomify/plugins by default), never on PATH. Plugins are published as bomify packages and installed with bomify plugin install:

bomify plugin install oci             # latest
bomify plugin install grype:1.12.0    # a specific version
bomify plugin list                    # what's installed, and from where

install <name> pulls ghcr.io/alejandro-velasco/bomify/plugins/<name> (change the prefix with --registry) and installs the binary for your OS and architecture, replacing any earlier install.

It only installs a plugin something vouches for, checked before downloading:

  • A signature verified by bomify-plugin-sigstore against a signer you trust, from a matching bomify trust rule or --verify-option (key=<public key>, or certificate-identity=... plus certificate-oidc-issuer=...).
  • Or a digest pin, <name>@sha256:<digest>, which every pulled blob is checked against.

Anything else is refused. Each binary must also match the SHA-256 its SBOM declares (integrity, not authenticity). --verify=false skips the signature check.

bomify's own plugins are signed keyless by its release workflow, and each release's plugin-digests.txt lists their pinned references. Since nothing can verify bomify-plugin-sigstore before it's installed, install it by digest, then trust the workflow for everything else:

# The .../plugins/sigstore@sha256:... line from the release's plugin-digests.txt
bomify plugin install sigstore@sha256:<digest>

bomify trust create sigstore --match ghcr.io/alejandro-velasco/bomify/plugins \
  --option certificate-identity=https://github.com/alejandro-velasco/bomify/.github/workflows/release.yml@refs/heads/main \
  --option certificate-oidc-issuer=https://token.actions.githubusercontent.com

From then on, installs from that registry, including upgrades of sigstore, fail unless that workflow signed them.

Each package also carries its build provenance, signed by the same workflow.

Publishing a plugin

A plugin package is a bomify package whose SBOM lists the binaries as pkg:bomify-plugin/<kind> components, one per platform, with os/arch qualifiers in GOOS/GOARCH terms. bomify build copies each binary from the path (or file:// URL) in its distribution external reference, relative to the SBOM. Declare each binary's SHA-256: build checks it and install requires it. Record the contract versions it speaks, its contract output's contracts object, as the land.bomify.plugin.contracts property: install refuses a binary that records none, or a version bomify doesn't speak.

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "version": 1,
  "components": [
    {
      "type": "application",
      "name": "bomify-plugin-mykind",
      "version": "v1.0.0",
      "purl": "pkg:bomify-plugin/mykind@v1.0.0?os=linux&arch=amd64",
      "hashes": [{ "alg": "SHA-256", "content": "<sha256 of the binary>" }],
      "properties": [{ "name": "land.bomify.plugin.contracts", "value": "{\"component\":1}" }],
      "externalReferences": [{ "type": "distribution", "url": "dist/linux-amd64/bomify-plugin-mykind" }]
    }
  ]
}
bomify build plugin.cdx.json --tag registry.example.com/plugins/mykind:v1.0.0
bomify push registry.example.com/plugins/mykind:v1.0.0 --sign sigstore --sign-option key=signing.key
bomify plugin install mykind:v1.0.0 --registry registry.example.com/plugins --verify-option key=signing.pub

install downloads only the installing machine's binary, so one package can carry every platform. The first-party plugins are published this way by make plugin-packages and make push-plugin-packages (see hack/pluginpackages).

bomify-plugin-oci

Pulls and pushes pkg:oci/... and pkg:docker/... images. The reference comes from the purl's tag qualifier, else a digest-shaped version, else a plain tag, else the bare repository; repository_url overrides the registry address.

bomify-plugin-helm

  • Component: pulls from HTTP(S) chart repositories (pkg:helm/<name>@<version>?repository_url=https://...) and OCI registries (repository_url=oci://...). push and push --check support OCI only, since classic repositories are read-only.
  • SBOM generation: renders a chart locally, as helm template does, and reports every image it references plus the chart itself. See its README for flags and examples.

The Helm SDK pulls in much of k8s.io/client-go, so this binary is larger than the others.

bomify-plugin-generic

Handles pkg:generic/<name>@<version>?download_url=<url>: pull GETs download_url; push PUTs the file to --remote exactly as given (with a real Content-Length), which suits presigned upload URLs. push --check only tries a HEAD, since a presigned URL can't be checked without writing to it.

bomify-plugin-huggingface

  • Component: pulls a Hugging Face Hub model repository at a commit (pkg:huggingface/<namespace>/<name>@<commit>, with repository_url naming another hub), and pushes it to another namespace or hub, large files through Xet storage.
  • SBOM generation: describes a model as a machine-learning-model component pinned to a commit, with its hash and a model card from the Hub's metadata, without downloading the weights. See its README for options, authentication, and examples.

It's written in Rust, on Hugging Face's hf-hub client (see Plugin languages).

bomify-plugin-grype

Scans with grype as a library. Most purl types name one package and are matched directly. An oci/docker purl is an image, so it's cataloged with syft first and every package in it is matched; each is returned as a component, with evidence.occurrences showing where syft found it.

Matches come from grype's vulnerability database (cached where the grype CLI caches it) and are converted to CycloneDX by hand. When available, ratings include the EPSS score and a CISA KEV flag.

supported-components lists, scanned by purl, the types grype has a matcher for (apk, deb, rpm, alpm, bitnami, npm, golang, maven, pypi, gem, cargo, nuget, hex) plus oci/docker; and generic, scanned from its pulled files: syft catalogs the file (a Go or Rust binary's modules, a Java archive, a known binary's version), and a file it finds nothing in is reported unscanned.

bomify-plugin-sigstore

Signs and verifies with sigstore-go, producing v0.3 Sigstore bundles (application/vnd.dev.sigstore.bundle.v0.3+json):

  • Key pair: --sign-option key=signing.key, --verify-option key=signing.pub. Any PEM pair works (OpenSSL, plain or encrypted PKCS#8, or cosign generate-key-pair); an encrypted key's password comes from SIGSTORE_PASSWORD. Fully offline, which suits private registries and air-gapped save/load. See the signing how-to.
  • Keyless: with no key, signing trades an OIDC token (SIGSTORE_ID_TOKEN or --sign-option identity-token=..., audience sigstore) for a short-lived Fulcio certificate and logs to Rekor. In GitHub Actions, permissions: id-token: write lets a workflow sign as itself. Tokens last minutes, so fetch one right before signing. Verifying needs certificate-identity (or -regexp) and certificate-oidc-issuer (or -regexp), and network access to Sigstore's trusted root.

signature attest (see the contract) signs a DSSE envelope instead, in the bundle format cosign and gh attestation verify read; signature verify-attestation verifies one with the same options as signature verify.

key can't be combined with keyless options, and unknown options are rejected.

Plugin languages

A first-party plugin's language is named by the manifest in its directory, and everything that builds plugins goes by it: the Makefile (GO_PLUGINS, RUST_PLUGINS), plugins.yml, CodeQL, and Dependabot. Each language has a build script, hack/build-<lang>-plugins.sh, writing binaries laid out as <dir>/<kind>/<os>-<arch>/bomify-plugin-<kind>[.exe], for this machine by default or, given PLATFORMS, statically for a release; CI and the Makefile both build through it, and it runs as well locally.

  • Go (a go.mod): every Go plugin is its own module, using this repo's pkg/ through a replace directive, so its dependencies stay out of bomify's go.mod and every other plugin's.
  • Rust (a Cargo.toml): bomify-plugin-huggingface is a Rust crate, since Hugging Face's own client for the Hub and Xet storage, hf-hub, is Rust. Without pkg/plugin, it implements its contracts' flags, results, and logging itself, and reads bomify login's credentials from bomify's auth.json (and Docker's credential helpers) directly; a contract change must be made there too. Its rust-toolchain.toml pins the toolchain. A static Linux build needs musl-tools.
Target Does
make plugins (or make plugin-<kind>) Builds every plugin (or one) for this machine into bin/; -j builds them in parallel.
make test test-go runs each Go module's tests; test-rust checks each crate's formatting and lints, and runs its tests.
make prebuilt-plugins Builds release binaries into PREBUILT_PLUGINS_DIR: each Go plugin for every platform, each Rust one for RUST_PLATFORMS (this machine's, since Rust can't cross-compile to every platform from one).
make plugin-packages Packages them all from PREBUILT_PLUGINS_DIR, compiling nothing.

Every plugin is built before it's packaged: plugins.yml builds each in a job of its own, the Rust ones on each platform's own runner (outside a release, only linux/amd64 unless something Rust-related changed), and release packages what it built, so the job that signs compiles none of them. The container image takes them prebuilt too, compiling none: CI gives it the linux/amd64 binaries build-plugins built, and locally make container-prebuilt builds them (see the README).

A plugin in another language needs a hack/build-<lang>-plugins.sh writing the same layout; <LANG>_PLUGINS, build, and test targets in the Makefile; a job in plugins.yml uploading plugins-<lang>-... artifacts; and, where they support the language, CodeQL and Dependabot entries. Packaging and the container image need nothing new.

Schemas

contract-result.schema.json

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://raw.githubusercontent.com/alejandro-velasco/bomify/main/plugins/contracts/contract-result.schema.json",
  "title": "bomify plugin ContractResult",
  "description": "The single JSON object every bomify-plugin-<kind> binary must print to stdout on success for its \"contract\" subcommand, per plugins/README.md#contract-versions. Validate a plugin's actual stdout output (one line, one object) against this schema to check it for structural conformance.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "contracts"
  ],
  "properties": {
    "contracts": {
      "type": "object",
      "propertyNames": {
        "enum": [
          "component",
          "sbom",
          "security",
          "signing"
        ]
      },
      "additionalProperties": {
        "type": "integer",
        "minimum": 1
      },
      "description": "Each plugin contract the plugin implements, mapped to the major version of it the plugin speaks. A contract it doesn't implement is absent."
    }
  }
}