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:
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.
| 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-sigstoreagainst a signer you trust, from a matchingbomify trustrule or--verify-option(key=<public key>, orcertificate-identity=...pluscertificate-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://...).pushandpush --checksupport OCI only, since classic repositories are read-only. - SBOM generation: renders a chart locally, as
helm templatedoes, 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>, withrepository_urlnaming another hub), and pushes it to another namespace or hub, large files through Xet storage. - SBOM generation: describes a model as a
machine-learning-modelcomponent 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, orcosign generate-key-pair); an encrypted key's password comes fromSIGSTORE_PASSWORD. Fully offline, which suits private registries and air-gappedsave/load. See the signing how-to. - Keyless: with no
key, signing trades an OIDC token (SIGSTORE_ID_TOKENor--sign-option identity-token=..., audiencesigstore) for a short-lived Fulcio certificate and logs to Rekor. In GitHub Actions,permissions: id-token: writelets a workflow sign as itself. Tokens last minutes, so fetch one right before signing. Verifying needscertificate-identity(or-regexp) andcertificate-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'spkg/through areplacedirective, so its dependencies stay out of bomify'sgo.modand every other plugin's. - Rust (a
Cargo.toml):bomify-plugin-huggingfaceis a Rust crate, since Hugging Face's own client for the Hub and Xet storage, hf-hub, is Rust. Withoutpkg/plugin, it implements its contracts' flags, results, and logging itself, and readsbomify login's credentials from bomify'sauth.json(and Docker's credential helpers) directly; a contract change must be made there too. Itsrust-toolchain.tomlpins the toolchain. A static Linux build needsmusl-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."
}
}
}