Component plugin contract v1¶
A plugin reports the contract versions it speaks with
bomify-plugin-<kind> contract (see
contract versions).
The authoritative spec for a bomify-plugin-<kind> binary's component
plugin subcommands (component pull, component push, component
remote), which bomify build and bomify distribute call. It's
independent of the
SBOM generation,
security scanning,
and signing
contracts; a binary can implement any combination.
Go plugins should use
pkg/plugin:
implement its ComponentPlugin interface and plugin.ComponentCommand
provides every flag, the --check rules, the --log logger, and the
output rules below.
A plugin is a standalone executable. Every input is a command-line flag; every output is stdout, stderr, the exit code, or the files it's told to write.
Naming and discovery¶
A plugin for purl type <kind> (e.g. oci, helm, npm) is named
bomify-plugin-<kind> (.exe on Windows) and installed in
<data-dir>/plugins (~/.bomify/plugins by default), usually with
bomify plugin install (see
plugins/README.md).
bomify never searches PATH.
<kind> is the component purl's type: pkg:oci/nginx@1.27 needs
bomify-plugin-oci. The exception is pkg:bomify-plugin/..., a plugin
binary bomify handles itself (see
publishing a plugin).
Commands¶
All three subcommands take --purl, --log, and --log-color (see
Logging).
component pull¶
Fetch or build the component --purl identifies into --output.
| Flag | Required | Meaning |
|---|---|---|
--purl |
yes | The component's package URL. Everything the plugin needs to know comes from it. |
--output |
unless --check |
Directory to write the artifact into. bomify creates it, empty, beforehand. Write a single file or a tree, whatever suits the artifact. Not passed with --check=true. |
--check |
no | See Check mode. Default false. |
On success, print one Result and exit 0.
component push¶
bomify-plugin-<kind> component push --purl <purl> --input <dir> --remote <endpoint> --log <path> --log-color <bool>
Publish what a prior pull wrote into --input to --remote.
| Flag | Required | Meaning |
|---|---|---|
--purl |
yes | Same purl as the pull that filled --input. |
--input |
unless --check |
Directory a successful pull for the same purl wrote into, exactly as it left it. Not passed with --check=true, and no prior pull is needed then. |
--remote |
yes | Where to publish. Its shape is up to the plugin (a registry prefix, a URL, ...); document it in --help. |
--check |
no | See Check mode. Default false. |
On success, print one Result and exit 0. Leave hash
unset.
component remote¶
Report where the component comes from or is published under, without
fetching or publishing anything. bomify distribute matches this against
a rule's --match and, for a non-empty match, keeps whatever follows
the matched prefix when building the --remote it passes to push, so
a mirror rule preserves each component's path.
On success, print one RemoteResult and exit 0.
remote must be a pure function of --purl: no network, registry, or
files. bomify may call it any number of times and never caches it.
Check mode¶
--check=true on pull/push asks the plugin to confirm a real
transfer would succeed (the artifact exists and is readable, or the
destination is reachable and writable) without transferring content.
--output/--input aren't passed; don't require them.
Use the cheapest check available: a manifest HEAD, a resolve, a small
index fetch. For pull --check, a full pull is an acceptable last
resort. For push --check, a real write never is, since it can have
side effects (consuming a single-use presigned URL, overwriting
something); report whatever partial check was possible instead, as
bomify-plugin-generic does.
Print one Result on success, or exit non-zero with a one-line stderr
message. hash may be set if the check learns it for free (e.g. a
registry HEAD returning a digest).
Standard streams¶
| Stream | Reserved for |
|---|---|
| stdout | Exactly one Result or RemoteResult, only on success. Nothing else, ever: bomify fails if stdout isn't that one JSON object. |
| stderr | One short fatal error message, only on failure. bomify appends it to its own error. |
| exit code | 0 on success; non-zero on failure. |
Everything else goes to the --log file.
Result¶
Printed by pull and push. A missing key and a zero value are
equivalent, so hash may be absent, {}, or fully set.
{
"outputPath": "path/to/artifact/or/remote/reference",
"message": "optional human-readable summary",
"hash": { "algorithm": "SHA-256", "value": "9d5aec50e4ace532e44f5459f1f4d9c17180c96318103a6f64b765864967d31" }
}
| Field | Type | Required | Meaning |
|---|---|---|---|
outputPath |
string | yes | pull: the path written (normally --output). push: the reference published (e.g. <remote>/<name>:<version>). --check: whatever identifies what was checked (a resolved reference, a download URL). |
message |
string | no | Short human-readable summary. Logged, never parsed. |
hash |
object | no | The pulled artifact's SHA-256 (see Hashes). pull only; unset for push, and when it can't be computed. Never guess. |
hash.algorithm |
string | with hash.value |
Always SHA-256. |
hash.value |
string | with hash.algorithm |
Hex digest. Compared case-insensitively; lowercase by convention. |
Schema: result.schema.json.
Hashes¶
bomify checks every pulled component against the SHA-256 its SBOM
declares and keys its data directory by SHA-256, so that's the only
algorithm a plugin reports (plugin.NewHash builds one from a hex
digest). If a plugin can't compute it, it omits hash rather than
failing.
RemoteResult¶
Printed by remote:
| Field | Type | Required | Meaning |
|---|---|---|---|
remote |
string | yes | Where the component comes from or is published under, in the plugin's own shape (registry namespace, URL, ...). |
Report remote in the shape your push expects --remote in, since a
distribution rule can hand it back as --remote:
- If
pushappends the component's name/tag to--remote(as the OCI and Helm plugins do), leave that off:docker.io/library, notdocker.io/library/nginx. - If
pushuses--remoteas the complete destination (asbomify-plugin-genericdoes, since a presigned URL can't be appended to), report the complete address.
Components with a common origin must report values sharing a common
/-separated prefix; that's what rules match and substitute.
Schema: remote-result.schema.json.
Logging¶
--log <path>: a file bomify creates empty right before the call. Append your log lines to it unconditionally. bomify streams it to its stdout under--verboseand deletes it when the plugin exits, so it's not a durable log.--log-color <bool>: whether ANSI colors are allowed in the log. bomify decides from its own stdout; don't detect it yourself.
plugin.ComponentCommand hands each call a ready-made *slog.Logger for
this (plugin.OpenLog opens one directly), in bomify's own log format.
What bomify handles¶
Caching, concurrency, and state are bomify's job:
- Each invocation is fresh; nothing needs remembering between calls.
- bomify decides when a
pullcan be skipped and prevents concurrent pulls of the same purl. --output/--inputare always in the state described above.
Given the same flags, a plugin should do the same thing. remote in
particular must always report the same value for the same --purl.
Schemas¶
result.schema.json¶
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/alejandro-velasco/bomify/main/plugins/contracts/component/v1/result.schema.json",
"title": "bomify plugin Result",
"description": "The single JSON object a bomify-plugin-<kind> binary must print to stdout on success, per plugins/contracts/component/v1/CONTRACT.md. 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": ["outputPath"],
"properties": {
"outputPath": {
"type": "string",
"minLength": 1,
"description": "For pull: the local path the artifact was written to (normally --output, echoed back). For push: the reference the artifact was published under at --remote. For either subcommand with --check=true: no path was written to or read from, so this identifies the artifact/destination that was checked instead (e.g. the resolved registry reference or download URL)."
},
"message": {
"type": "string",
"description": "Optional, human-readable summary of what happened. Omit this key entirely rather than sending an empty string."
},
"hash": {
"type": "object",
"additionalProperties": false,
"description": "May be omitted entirely, or present as {} or with both fields set — bomify parses a missing key and an empty object identically. algorithm and value are either both present or both absent.",
"properties": {
"algorithm": {
"type": "string",
"const": "SHA-256",
"description": "The one algorithm bomify uses; value is the pulled artifact's SHA-256."
},
"value": {
"type": "string",
"minLength": 1,
"pattern": "^[0-9a-fA-F]+$",
"description": "The digest, hex-encoded. Case-insensitive; lowercase is the convention every first-party plugin follows."
}
},
"dependencies": {
"algorithm": ["value"],
"value": ["algorithm"]
}
}
}
}
remote-result.schema.json¶
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/alejandro-velasco/bomify/main/plugins/contracts/component/v1/remote-result.schema.json",
"title": "bomify plugin RemoteResult",
"description": "The single JSON object a bomify-plugin-<kind> binary must print to stdout on success for its \"remote\" subcommand, per plugins/contracts/component/v1/CONTRACT.md. 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": ["remote"],
"properties": {
"remote": {
"type": "string",
"minLength": 1,
"description": "Where the component comes from or is published under, in whatever shape is meaningful for this plugin's kind (a registry/repository address, a source URL, etc.)."
}
}
}