Security scanning plugin contract v1¶
A plugin reports the contract versions it speaks with
bomify-plugin-<type> contract (see
contract versions).
The spec for a bomify-plugin-<type> binary's security scanning
subcommands, security scan and security supported-components, which
bomify security scan <type> <tag> (and a scan on pull/load) call.
It's independent of the
component,
SBOM generation,
and signing
contracts.
Here <type> names the scanning tool (e.g. grype, trivy), not a
purl type: one scanner handles every component in a package.
Go plugins should implement pkg/plugin's SecurityPlugin interface and
use plugin.SecurityCommand, which provides both subcommands, their
flags, and output.
Naming and discovery¶
As for the component contract:
bomify-plugin-<type> (.exe on Windows), installed in
<data-dir>/plugins.
What bomify does¶
- Resolves
<tag>to a local package and walks its SBOM. - Calls
security supported-componentsonce, and skips every component whose purl type isn't listed. A type the plugin scans from files is scanned once bomify has the component's pulled files (see Scan modes). - Calls
security scan --purl <purl>once per remaining component, adding--input <dir>for a type scanned from files, up to--concurrencyat a time. - Writes each result as that component's report, unless the
plugin reports it couldn't analyze the component (
unscanned).
The plugin never sees the SBOM or any component but the one it's asked about.
Commands¶
bomify-plugin-<type> security scan --purl <purl> [--input <dir>]
bomify-plugin-<type> security supported-components
security scan:--purl(required) is the component to scan.--inputis the directory holding the component's pulled files, as bomify pulled them; it's passed only for a type the plugin scans from files (see Scan modes). On success, print oneSecurityResultand exit0.security supported-components: no flags. On success, print oneSupportedComponentsResultand exit0. It must always report the same thing.
There's no --check, --output, or --log/--log-color.
Standard streams¶
| Stream | Reserved for |
|---|---|
| stdout | Exactly one JSON result, only on success. Nothing else, ever. |
| stderr | Diagnostics are allowed, but on failure it must hold only one short fatal message, which bomify appends to its error. |
| exit code | 0 on success; non-zero on failure. |
A failed scan fails the whole bomify security scan, as a failed
pull fails build. A failed supported-components fails it before
anything is scanned.
SecurityResult¶
{
"vulnerabilities": [
{
"bom-ref": "CVE-2024-12345",
"id": "CVE-2024-12345",
"ratings": [{ "severity": "high" }],
"affects": [{ "ref": "pkg:npm/left-pad@1.3.0" }]
}
],
"components": []
}
| Field | Type | Required | Meaning |
|---|---|---|---|
vulnerabilities |
array | yes (may be empty) | Every CycloneDX vulnerability affecting --purl, valid against CycloneDX's JSON Schema. Empty means nothing was found; that's a valid result, not an error. |
components |
array | no | CycloneDX components, valid against CycloneDX's JSON Schema, that the plugin unpacked --purl into to scan it (e.g. the packages inside an image). Omit if it scanned the purl directly. |
unscanned |
string | no | Why the plugin couldn't analyze the component at all, e.g. nothing in its files it recognizes. bomify then counts it as not scanned, not as clean, and ignores the rest of the result. |
The plugin sets affects; bomify never changes it.
- Scanned directly:
affectsis one entry, the--purlgiven. - Unpacked:
affectsnames the affected pieces by theirbom-reffromcomponents, never the top-level purl. A vulnerability affecting several pieces lists them all in one object.
Give each vulnerability a stable bom-ref (its ID works) and each
component a stable bom-ref (its purl works). Report every piece you
unpacked, not just affected ones; bomify filters them.
Schema: security-result.schema.json.
SupportedComponentsResult¶
| Field | Type | Required | Meaning |
|---|---|---|---|
types |
object | yes, non-empty | Each purl type the plugin can scan (oci, npm, ...), mapped to how it scans it: purl or files (see Scan modes). Components of other types are never sent to scan. |
scans |
array of strings | yes | Scan categories (e.g. sca, sast). Informational: bomify only logs them. |
Scan modes¶
purl: the plugin scans the component from its purl alone — a lookup in an ecosystem, or, for an image, pulling it itself. bomify never passes--input.files: the plugin scans the component only from its pulled files, which bomify passes as--input(a scan on pull or load runs once they're downloaded). bomify skips a component whose files it doesn't have, counting it as not scanned.
A mode bomify doesn't know is skipped the same way, so a newer plugin's mode never fails an older bomify's scan.
Schema: supported-components-result.schema.json.
Reports¶
bomify writes each result as a CycloneDX document at
<data-dir>/vulnerabilities/<purl-hash>/<type>.json:
metadata.componentis the scanned component,bom-refset to its purl, so a directly scanned component'saffectsresolve.componentsare only the unpacked pieces someaffectsnames.vulnerabilitiesare exactly what the plugin reported, unmodified.metadata.timestampandmetadata.toolsrecord when and by which plugin it was scanned.
Reports are keyed by purl and scanner, so packages sharing a component share its reports, and a scanner's newest scan replaces only its own report. On push or save, reports travel as an OCI referrer, so re-scanning never changes the package digest (see the architecture docs).
What bomify handles¶
Concurrency, filtering by type, and storing results are bomify's job. Each call is fresh, the plugin is never asked about an unsupported type, and it needs no knowledge of the package or other components. Don't depend on the binary's other contracts, if it has any.
Schemas¶
security-result.schema.json¶
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/alejandro-velasco/bomify/main/plugins/contracts/security/v1/security-result.schema.json",
"title": "bomify plugin SecurityResult",
"description": "The single JSON object a bomify-plugin-<type> binary must print to stdout on success for its \"security scan\" subcommand, per plugins/contracts/security/v1/CONTRACT.md. Its vulnerabilities and components are CycloneDX's own objects, referenced from CycloneDX's schema (1.7's definitions); this one adds only the bomify-specific requirements.",
"type": "object",
"required": ["vulnerabilities"],
"properties": {
"vulnerabilities": {
"type": "array",
"description": "Every vulnerability the scanned --purl is affected by.",
"items": {
"allOf": [
{ "$ref": "https://cyclonedx.org/schema/bom-1.7.schema.json#/definitions/vulnerability" },
{
"required": ["id"],
"properties": {
"bom-ref": { "description": "A stable identifier for this vulnerability (its id is a reasonable choice). bomify writes it into the scanned component's vulnerability report unchanged." },
"affects": {
"minItems": 1,
"description": "Set by the plugin, not bomify. For a purl scanned directly (no \"components\" reported), a single entry referencing that same purl string back. For a purl unpacked into pieces (\"components\" is non-empty), one entry per affected piece, referencing its bom-ref from \"components\" - never the top-level purl itself."
}
}
}
]
}
},
"components": {
"type": "array",
"description": "Optional. Every piece the plugin found while unpacking the scanned --purl to scan it at all (e.g. the packages found by cataloging a container image). Omit entirely, or leave empty, if the plugin scanned the purl directly. bomify writes this list as the top-level components of the scanned component's vulnerability report, whose metadata component is the scanned --purl itself.",
"items": {
"allOf": [
{ "$ref": "https://cyclonedx.org/schema/bom-1.7.schema.json#/definitions/component" },
{
"required": ["bom-ref"],
"properties": {
"bom-ref": { "description": "A stable identifier for this piece (a purl, if it has one, is a reasonable choice): what a vulnerability's affects[].ref points at when this piece is the one affected." }
}
}
]
}
},
"unscanned": {
"type": "string",
"minLength": 1,
"description": "Optional. Why the plugin couldn't analyze the component at all, e.g. nothing in its files it recognizes. bomify then counts the component as not scanned rather than clean, and ignores the rest of the result."
}
}
}
supported-components-result.schema.json¶
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/alejandro-velasco/bomify/main/plugins/contracts/security/v1/supported-components-result.schema.json",
"title": "bomify plugin SupportedComponentsResult",
"description": "The single JSON object a bomify-plugin-<type> binary must print to stdout on success for its \"security supported-components\" subcommand, per plugins/contracts/security/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": ["types", "scans"],
"properties": {
"types": {
"type": "object",
"minProperties": 1,
"propertyNames": { "minLength": 1 },
"additionalProperties": { "enum": ["purl", "files"] },
"description": "Every component purl type (e.g. \"oci\", \"helm\", \"npm\", \"generic\") this plugin knows how to scan, mapped to how it scans it: \"purl\" from the purl alone, or \"files\" only from the component's pulled files, passed as \"security scan --input\". A component whose purl type isn't in this map is never sent to \"security scan\"."
},
"scans": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"description": "The categories of scan this plugin performs, e.g. \"sca\", \"sast\". Purely informational today — bomify logs it, but doesn't yet act on it."
}
}
}