diff --git a/.github/labeler.yml b/.github/labeler.yml index a12677a27..af15c82b6 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -533,6 +533,12 @@ contract:report-relay: - any-glob-to-any-file: - 'tools/forwarder-relay/**' +contract:sbom-pipeline: + - any: + - changed-files: + - any-glob-to-any-file: + - 'tools/sbom-pipeline/**' + contract:scan-format: - any: - changed-files: diff --git a/docs/labels-and-capabilities.md b/docs/labels-and-capabilities.md index 03eb6e354..3eb13acb5 100644 --- a/docs/labels-and-capabilities.md +++ b/docs/labels-and-capabilities.md @@ -149,6 +149,7 @@ framework substrate: | `contract:security-tracker` | contract | Where the security skills keep embargoed reports: body fields, status rollup, attachments, a confidentiality guard, and a publish step for working-copy backends (GitHub, local files, private Jira). | | `contract:body-field` | contract | How the `### ` fields of a tracker issue body are found, read and rewritten: field boundaries, closed code blocks, the refusals, and a language-neutral conformance suite every implementation passes. | | `contract:hook-manager` | contract | Git-hook managers (prek, pre-commit, lefthook, husky, …) under the secure agent setup: where a manager's install puts its hook, and how its home is split between sandboxed and host runs. | +| `contract:sbom-pipeline` | contract | SBOM generation in a project's build: one CycloneDX 1.7 SBOM per distributable artifact, with the same meaning whatever ecosystem produced it, and content from other ecosystems embedded as assemblies. | | `substrate:analytics` | substrate | Read-only metrics / dashboards / renderers. | | `substrate:sandbox` | substrate | Agent isolation, egress control, settings audit. | | `substrate:action-guard` | substrate | Deterministic pre-tool-use command guards. | @@ -393,6 +394,7 @@ or a contract-free mix of substrates (e.g. `tools/spec-inventory` is | [`tools/vcs`](../tools/vcs/) | `contract:source-control` | Backend-dispatching implementation of the source-control (VCS) capability ([`tools/github/source-control.md`](../tools/github/source-control.md)); complete Git, Mercurial (Hg), Fossil, and Subversion (SVN) backends | | [`tools/sourcehut`](../tools/sourcehut/) | `contract:tracker` + `contract:source-control` + `contract:mail-archive` | SourceHut (sr.ht) forge bridge: todo.sr.ht, lists.sr.ht, builds.sr.ht, and git/hg repository reads | | [`tools/hook-manager`](../tools/hook-manager/) | `contract:hook-manager` | Contract for git-hook managers under the secure agent setup: the install-target and home rules, and the registry of adapters (prek, overcommit, lefthook and captainhook today) that the hook-manager shim, the whole-user dispatcher and `sandbox-lint` are generated from | +| [`tools/sbom-pipeline`](../tools/sbom-pipeline/) | `contract:sbom-pipeline` | Contract for SBOM generation in a project's build: the format, subject, component, assembly, reproducibility and distribution rules, the pipeline operations and conformance checks, and the per-ecosystem adapters (Maven, Gradle, JS, Python, Go, Rust; all planned) | | [`tools/tracker`](../tools/tracker/) | `contract:tracker` | Interface spec for the tracker contract's read-only contributor-activity queries (issues filed, triaged and commented on; issues created; first maintainer reply). Pure interface spec; GitHub and Jira implement it, and the rest of the contract is described by `tools/github/tool.md` | | [`tools/body-field-contract`](../tools/body-field-contract/) | `contract:body-field` | The body-field contract: how every reader and writer finds a tracker body's `### ` fields, with a language-neutral conformance suite run against every implementation. Pure interface spec; implementations: `tools/github-body-field/` (reference), `tools/vetted-ops/`, `tools/local-tracker/`, `tools/jira/` | | [`tools/security-tracker`](../tools/security-tracker/) | `contract:security-tracker` | Adapter contract for the security tracker the `security-*` skills read and write, selected by `tracker.platform`. Pure interface spec; backends: `tools/github/` (default), `tools/local-tracker/`, `tools/jira/` | diff --git a/docs/vendor-neutrality.md b/docs/vendor-neutrality.md index 56f1c68d8..71503a497 100644 --- a/docs/vendor-neutrality.md +++ b/docs/vendor-neutrality.md @@ -582,7 +582,7 @@ generated block below. -**Overall vendor-neutrality score: 16/18 capability contracts (89%).** Generated by [`tools/vendor-neutrality-score`](../tools/vendor-neutrality-score/); re-run it to refresh this section. +**Overall vendor-neutrality score: 16/19 capability contracts (84%).** Generated by [`tools/vendor-neutrality-score`](../tools/vendor-neutrality-score/); re-run it to refresh this section. | Capability contract | Neutral? | Class | Backends today | Basis | |---|---|---|---|---| @@ -604,6 +604,7 @@ generated block below. | `contract:security-tracker` | ✅ | vendor-backed | Atlassian, agnostic | 2 backend vendors: Atlassian, agnostic; partial foundation, not counted: asf-security-dash | | `contract:body-field` | ✅ | agnostic | Atlassian, GitHub, agnostic | vendor-neutral by construction — one spec serves every backend | | `contract:hook-manager` | ❌ | vendor-backed | — | no backend implemented yet | +| `contract:sbom-pipeline` | ❌ | vendor-backed | — | no backend implemented yet | **Per-skill assessment: 83/83 skills carry no vendor lock-in.** A skill is *capability-pure* when it names no backend at all, *portable* when every backend it names has an alternative (its contract is green), and *vendor-coupled* only when it reaches for a backend that is the sole implementation of a capability. diff --git a/tools/sbom-pipeline/README.md b/tools/sbom-pipeline/README.md new file mode 100644 index 000000000..bb7b9d5bc --- /dev/null +++ b/tools/sbom-pipeline/README.md @@ -0,0 +1,391 @@ + + + + + +- [tools/sbom-pipeline/](#toolssbom-pipeline) + - [Prerequisites](#prerequisites) + - [Why a contract](#why-a-contract) + - [The rules](#the-rules) + - [1. Format and granularity](#1-format-and-granularity) + - [2. The subject](#2-the-subject) + - [3. Components](#3-components) + - [4. Assemblies](#4-assemblies) + - [5. Reproducibility](#5-reproducibility) + - [6. Distribution](#6-distribution) + - [7. Source releases](#7-source-releases) + - [Operations](#operations) + - [Conformance checks](#conformance-checks) + - [Adapters](#adapters) + - [Adding an adapter](#adding-an-adapter) + - [Configuration](#configuration) + - [Consumers](#consumers) + - [Open questions](#open-questions) + + + + + +# tools/sbom-pipeline/ + +**Capability:** contract:sbom-pipeline + +**Kind:** interface + +**Vendor:** agnostic + +This file defines the contract for an **SBOM pipeline**: +the part of a project's build that produces a Software Bill of Materials +for every artifact the project distributes. +Each build ecosystem (Maven, Gradle, npm / Yarn / pnpm, Python, Go, Rust) has its own SBOM generators, +and each generator fills the CycloneDX format in its own way. +The contract states what every SBOM must say, +regardless of the ecosystem and the generator that produced it. +It takes **no position on which generator a project uses**: +each adapter describes an ecosystem, +what any generator used there has to be configured to say, +and what has to be added after it runs; +the generators an adapter names are existing examples, not requirements. + +The contract is about integrating SBOM generation into a project's build, once. +It is not a release step: +a project that satisfies it produces conformant SBOMs on every build, +and the release skills only find them there. + +## Prerequisites + +- **Runtime:** None of its own — this file is a contract *specification* (pure Markdown). + Concrete prerequisites belong to each ecosystem adapter. +- **CLIs:** None for the contract itself; + the SBOM generator is the project's choice, and each adapter names known examples for its ecosystem. +- **Credentials / auth:** None. +- **Network:** None for the contract; + generators resolve dependencies through the ecosystem's own registry during the build. + +## Why a contract + +An SBOM is useful to the extent that someone outside the project can read it. +Within one ecosystem, +the ecosystem's own metadata (a POM, a `package-lock.json`, a lock file) +already tells its tools most of what an SBOM would. +The value of an SBOM is where ecosystems meet: +a consumer in one ecosystem reading what an artifact from another contains, +and a vulnerability scanner or policy tool that reads every ecosystem the same way. + +That only works when the SBOMs mean the same thing, whatever produced them. +Generators disagree on exactly the points that matter across ecosystems: +how the artifact itself is described, +whether a component carries a package URL, +how licenses are expressed, +and above all what happens to content from another ecosystem bundled into the artifact, +which a generator for the artifact's own ecosystem does not see at all. +The contract fixes those points; +the adapters say how to reconcile a generator's output with it. + +## The rules + +### 1. Format and granularity + +Every SBOM is a **CycloneDX 1.7** document in **JSON**. + +There is **one SBOM per distributable artifact**: +each package the project publishes to an ecosystem registry +(a Maven JAR or WAR, an npm package, a wheel, a Go module, a crate) +or to its download area, +and the source release itself +(see [Source releases](#7-source-releases)). +An SBOM describes exactly one artifact; +a project does not publish one combined SBOM for everything it builds, +and an artifact's SBOM does not depend on any other SBOM to be complete +(see [Assemblies](#4-assemblies)). + +### 2. The subject + +`metadata.component` describes the artifact the SBOM belongs to: + +- `type`, `name` and `version` of the artifact; +- a `purl` in the artifact's own ecosystem + (`pkg:maven/…`, `pkg:npm/…`, `pkg:pypi/…`, `pkg:golang/…`, `pkg:cargo/…`), + with the qualifiers that tell this artifact apart from the release's other artifacts + (`classifier` and `type` for Maven, `file_name` for a Python wheel), + so no two artifacts share a subject `purl`; +- `licenses`, as an SPDX license expression; +- the project's organization as `supplier`, + taken from the project's configuration rather than written into the build by hand, + so every artifact of the project names its supplier the same way. + +`metadata.lifecycles` records the `build` phase: +the SBOM is produced from the build that produced the artifact, +not from a scan of the source tree. + +### 3. Components + +Every component in a package's SBOM carries: + +- a `bom-ref` unique within the document and derived from the component alone: + its `purl`, else `name@version`, + prefixed with the containing component's `bom-ref` when it is nested, + so two runs of any generator give the same values; +- `name`, and a `version` + (or, for an external component, a `versionRange`; see [Assemblies](#4-assemblies)); +- a `purl` whenever its ecosystem has a package-URL type; +- `licenses`, as an SPDX license expression or SPDX license ID, + whenever the ecosystem's metadata declares one; +- `hashes` whenever the generator has the artifact the component was resolved to. + +The `dependencies` graph covers every component in the document, nested ones included, +and only components that ship in the artifact or are required to run it appear in it. +Build-only and test-only dependencies are left out. +A generator that cannot leave them out marks them `scope: excluded`, +and `normalize` removes them, +so a published SBOM never carries a `scope: excluded` component. + +### 4. Assemblies + +Every component is either **embedded**, +physically contained in the artifact, +or **external**, +required at runtime but provided by the environment the artifact is deployed in. +The SBOM records the difference with the component's `isExternal` flag, +and with where the component sits in the document: + +- **Components of the artifact itself** are listed in the top-level `$.components`. + An embedded one leaves `isExternal` at its default, `false`; + an external one has `isExternal: true`. +- **Components of an embedded component** form an **assembly**: + what that component embeds is listed in its own nested `components` array, + with `isExternal` left at `false`. + What it requires externally is **not** nested. + When the artifact itself embeds that requirement + (a shaded library that needs `commons-lang3`, which the WAR ships in `WEB-INF/lib`), + the requirement is met inside the artifact, + and the `dependencies` graph points at that top-level embedded component. + Otherwise it is listed in the top-level `$.components` with `isExternal: true`, + because the requirement passes up to the artifact that contains the component. + The `dependencies` graph still records the external requirement on the component that has it. + +So `isExternal: true` appears only in the top-level `$.components`, +which lists, for the whole artifact, everything the environment has to provide. +An external component may give the versions it accepts as a `versionRange` instead of a `version`. +Two parts that require the same package with different `version`s or `versionRange`s +keep one top-level entry each, with its own `bom-ref`, +and each part's `dependencies` edge points at its own; +requirements are never intersected or dropped. + +The case the contract exists for is content from another ecosystem. +A JavaScript UI bundled into a WAR is an embedded component of the WAR, +the npm packages bundled into that UI are nested under the UI component, +and the servlet API that the WAR's container provides is external: + +```text +metadata.component pkg:maven/org.example/app-web@1.2.0?type=war +components[] +├─ app-ui pkg:npm/%40example/app-ui@1.2.0 +│ └─ components[] +│ ├─ react pkg:npm/react@19.1.0 +│ └─ … +├─ commons-lang3 pkg:maven/org.apache.commons/commons-lang3@3.18.0 (WEB-INF/lib) +└─ jakarta.servlet-api pkg:maven/jakarta.servlet/jakarta.servlet-api@6.1.0 isExternal: true +``` + +The embedded content is described **in the SBOM itself**, +not by a reference to an SBOM published in the other ecosystem: +a consumer must be able to read an artifact's full contents from that artifact's SBOM alone, +without knowing how to find or fetch SBOMs in an ecosystem it does not use. +The ecosystem that produced the embedded content generates its part with its own adapter, +and the pipeline grafts that part into the container's SBOM +(see [`embed`](#operations)). + +The same rule applies within one ecosystem +whenever content is copied into the artifact rather than resolved by the consumer: +classes shaded into a JAR, vendored Go modules, a native library inside a wheel. + +### 5. Reproducibility + +Two builds of the same source produce the same SBOM, byte for byte, +so the SBOM can be part of a reproducible-build comparison: + +- `serialNumber` is a name-based UUID (version 5) derived from the subject's `purl`, + not a random one, + and a build-produced SBOM has `version` `1`; +- `metadata.timestamp` is taken from `SOURCE_DATE_EPOCH`; +- every array of objects is sorted by a canonical key + (`bom-ref` for components, `ref` for `dependencies`, `alg` for `hashes`, + the SPDX identifier or name for `licenses`, `phase` for `metadata.lifecycles`, + `type` then `url` for `externalReferences`, `name` for `properties`), + every array of strings (such as `dependsOn`) is sorted lexically, + and nothing host-specific (absolute paths, user names, tool cache locations) is recorded. + +The serial number departs from a recommendation of the specification, +which says every SBOM *should* have a unique serial number +even when its contents have not changed. +The contract trades that for reproducibility, +and follows the specification's own model of identity instead: +the serial number identifies *the SBOM of one artifact version*, +and a corrected SBOM for the same artifact keeps the serial number and increments `version`. + +### 6. Distribution + +An SBOM is published **next to or inside its artifact, as the artifact's own channel dictates**, +so a consumer finds it where they found the artifact. +Each adapter declares the channel for its ecosystem +(for example, the `cyclonedx` classifier on a Maven artifact, +or the SBOM directory of a wheel's `.dist-info`). +An artifact published to the project's download area carries its SBOM beside it, +with checksums and a signature of its own, made the same way as the artifact's. + +### 7. Source releases + +The source release is a distributable artifact too, and has an SBOM of its own. +Where a package's SBOM records the packages it contains, +the source SBOM records the **files** that do not share the project's terms: +every file in the source release whose license or copyright attribution +differs from the project's own is a component, with + +- `type: file`, and the file's path within the source release as its `name`; +- `hashes` of the file; +- `licenses`, as an SPDX license expression, + and `copyright`, as stated for that file. + +Files that came into the source tree together (a copied third-party library) +may be nested as an assembly under a component describing where they came from, +with that component's `purl` and `version` when it has them; +each file is still listed. +Files under the project's own license and copyright are not listed: +the subject's `licenses` and `supplier` already cover them. + +`metadata.lifecycles` records the `pre-build` phase: +the source SBOM describes the source, not a build of it. + +No ecosystem's metadata records the license and copyright of individual files, +but a project that follows its release policy already has that data: +the release's `LICENSE` file names the bundled files under other licenses, +and its `NOTICE` file carries the copyright attributions they require. +The source SBOM is their machine-readable counterpart. +Its inventory comes from the source tree, +since `NOTICE` carries attributions rather than paths +and a `LICENSE` that names a bundled library does not always list each of its files; +`LICENSE` and `NOTICE` are the cross-check the inventory must agree with. +A project finds the files from their headers, +or with a license scanner of its choice; +as for package SBOMs, the contract fixes what the source SBOM says, +not which tool produces it. + +## Operations + +A pipeline built on the contract runs the steps below; +each adapter implements the ones that are ecosystem-specific. + +| Operation | Ecosystem-specific | What it does | +|----------------------------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `detect(checkout)` | yes | Finds the ecosystem's build files and the distributable artifacts they produce. | +| `configure(artifact)` | yes | The one-time change to the build: adds and configures a generator of the project's choice, or configures the one it already uses. | +| `generate(artifact)` | yes | Runs during the build and writes the artifact's SBOM. | +| `generate-source(release)` | no | Writes the source release's SBOM from its source tree, checked against its `LICENSE` and `NOTICE` files ([Source releases](#7-source-releases)). | +| `embed(container, part)` | no | Grafts the SBOM of embedded content into the container's SBOM ([Assemblies](#4-assemblies)): the part's embedded components are nested under the component that contains them, its external components that the container embeds are pointed at the container's component, the rest are moved to the top-level `$.components` (one entry per distinct requirement), `bom-ref`s are kept unique, and the `dependencies` graphs are merged. | +| `normalize(sbom)` | no | Applies what generators do not: supplier, serial number, timestamp, deterministic `bom-ref`s, ordering, and the removal of `scope: excluded` components ([Reproducibility](#5-reproducibility)). | +| `validate(sbom)` | no | Runs the [conformance checks](#conformance-checks). | +| `publish(artifact, sbom)` | yes | Attaches the SBOM to the artifact through the ecosystem's channel ([Distribution](#6-distribution)). | + +`embed`, `normalize` and `validate` are the same for every ecosystem, +and are implemented once, by a deterministic tool, +not by each adapter. +`generate-source` is the same for every ecosystem too: +it reads the source tree and the release's `LICENSE` and `NOTICE`, +which every release carries whatever its ecosystem. + +## Conformance checks + +`validate` reports each check as passed, failed, or for review, +with the offending `bom-ref`: + +| ID | Check | +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| C1 | The document validates against the CycloneDX 1.7 JSON schema. | +| C2 | `metadata.component` has a `type`, `name`, `version`, `purl`, `licenses` and `supplier`, and the supplier matches the project's configuration. | +| C3 | Every component has a `bom-ref`, a `name`, and a `version` (or, if external, a `versionRange`; a file in a source SBOM has neither), and every `bom-ref` is unique. | +| C4 | Every component of an ecosystem with a package-URL type has a `purl`. | +| C5 | Every `licenses` entry is a valid SPDX expression or SPDX license ID; a named license (`license.name`, not on the SPDX list) is reported for review. | +| C6 | Every `bom-ref` named in `dependencies` exists in the document, and in a package SBOM every component appears in `dependencies`. | +| C7 | `serialNumber` is the version 5 UUID derived from the subject's `purl`, `metadata.timestamp` matches `SOURCE_DATE_EPOCH`, and every array is in the order rule 5 gives. | +| C8 | No nested component has `isExternal: true`, and every component that a nested component depends on in `dependencies` is nested in the same assembly or is in the top-level `$.components`. | +| C9 | In a package SBOM, every embedded component in the top-level `$.components` has the same `purl` type as the subject. | +| C10 | In a source SBOM, every file component has `hashes`, `licenses` and `copyright`, and every license other than the project's own appears in the release's `LICENSE` file. | +| C11 | Every `bom-ref` is the value rule 3 derives for its component, and no component has `scope: excluded`. | + +C9 and the `LICENSE` half of C10 are heuristics, reported for review, not as failures. +An embedded component from another ecosystem at the top level (C9) +is usually one whose own SBOM was flattened instead of grafted as an assembly, +but some are legitimate (a native library copied into a wheel). +A `LICENSE` file (C10) names licenses in prose, +which does not always match an SPDX identifier. + +## Adapters + +One adapter per ecosystem, under `tools/sbom-pipeline-/`. +The generators below are examples of existing tools for each ecosystem, not requirements: +a project uses whichever it prefers, +and one that already produces SBOMs keeps its generator +and adds only what `normalize` has to fill in. + +| Adapter | Status | Example generators | Notes | +|----------|---------|----------------------------------------------------------------|----------------------------------------------------------------------------| +| `maven` | planned | `org.cyclonedx:cyclonedx-maven-plugin` | Many ASF projects already inherit this one from the ASF parent POM. | +| `gradle` | planned | `org.cyclonedx.bom` Gradle plugin | | +| `js` | planned | `@cyclonedx/cyclonedx-npm`, `@cyclonedx/yarn-plugin-cyclonedx` | One adapter for npm, Yarn and pnpm; they share the `npm` package-URL type. | +| `python` | planned | `cyclonedx-py` | The SBOM goes into the wheel's `.dist-info/sboms/` (PEP 770). | +| `go` | planned | `cyclonedx-gomod` | | +| `rust` | planned | `cargo-cyclonedx` | | + +Whether each generator emits CycloneDX 1.7 natively is recorded per adapter; +one that does not yet is upgraded by `normalize`, +and the adapter says so. + +## Adding an adapter + +An adapter README declares, for its ecosystem: + +- how `detect` recognizes the ecosystem and its distributable artifacts; +- the generators it knows of, each with the version the adapter was checked against, + and the build-file change `configure` makes for each; +- for each rule above and each generator, which fields the generator fills + and which `normalize` has to fill instead; +- how a part from this ecosystem is produced + when it is embedded in another ecosystem's artifact; +- the `publish` channel. + +## Configuration + +The contract reads one value from the adopting project's configuration +(`/project.md`, else its organization, else the framework default): +the **supplier** written into every SBOM's `metadata.component` and checked by C2. +Which key holds it is not settled yet +(see [Open questions](#open-questions)). +Each adapter documents any ecosystem-specific keys it adds. + +## Consumers + +| Skill | Status | Use | +|---------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `sbom-pipeline-integrate` | planned | Detects every ecosystem in a checkout, applies each adapter's `configure`, wires `generate-source`, `embed`, `normalize` and `validate` into the build, and hands back a commit for review. | +| `sbom-pipeline-verify` | planned | Read-only: runs `validate` against the SBOMs a build produced. | + +## Open questions + +- **Source release identity.** + A source release belongs to no package ecosystem, + so which `purl` type identifies it in the source SBOM's `metadata.component` + (and so derives its serial number) is not settled. + Until it is, rule 7 is provisional: + no source SBOM can meet rule 5's serial number, + so C7 is reported for review, not as a failure, on source SBOMs. +- **Supplier configuration key.** + Which key of the project or organization manifest names the supplier; + an organization-level default (the ASF for every ASF project) + would let most projects declare nothing. +- **Generator versions.** + Whether the generator versions an adapter was checked against are recorded in a `registry.json` here, + as [`hook-manager`](../hook-manager/) does for its adapters, + or only in each adapter's README. diff --git a/tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py b/tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py index c0d204e40..a187548b7 100644 --- a/tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py +++ b/tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py @@ -401,6 +401,7 @@ "contract:security-tracker", "contract:body-field", "contract:hook-manager", + "contract:sbom-pipeline", "substrate:analytics", "substrate:sandbox", "substrate:action-guard", diff --git a/tools/spec-loop/specs/README.md b/tools/spec-loop/specs/README.md index 1a9d087c4..845b1eb80 100644 --- a/tools/spec-loop/specs/README.md +++ b/tools/spec-loop/specs/README.md @@ -61,7 +61,8 @@ Start with [`overview.md`](overview.md), then: [`vetted-command-surface.md`](vetted-command-surface.md), [`adversarial-review.md`](adversarial-review.md), [`sandbox-diagnostics.md`](sandbox-diagnostics.md), - [`container-gateway.md`](container-gateway.md). + [`container-gateway.md`](container-gateway.md), + [`sbom-pipeline.md`](sbom-pipeline.md). (Agentic Autonomous, the fifth MISSION mode, is deliberately off and has no spec — see the note in [`overview.md`](overview.md).) diff --git a/tools/spec-loop/specs/overview.md b/tools/spec-loop/specs/overview.md index 1d0066d41..164547dcd 100644 --- a/tools/spec-loop/specs/overview.md +++ b/tools/spec-loop/specs/overview.md @@ -50,6 +50,7 @@ Each mode is an independently toggleable set of skills. Maturity mirrors | Agent isolation / layered sandbox | [agent-isolation-sandbox.md](agent-isolation-sandbox.md) | | Sandbox diagnostics — catalog, hint hook, doctor, verify | [sandbox-diagnostics.md](sandbox-diagnostics.md) | | Container gateway — podman / docker inside the sandbox | [container-gateway.md](container-gateway.md) | +| SBOM pipeline — one CycloneDX SBOM per artifact (proposed) | [sbom-pipeline.md](sbom-pipeline.md) | | CVE tooling | [cve-tooling.md](cve-tooling.md) | | Security reporting & dashboards | [security-reporting.md](security-reporting.md) | | Adoption & setup | [adoption-and-setup.md](adoption-and-setup.md) | diff --git a/tools/spec-loop/specs/sbom-pipeline.md b/tools/spec-loop/specs/sbom-pipeline.md new file mode 100644 index 000000000..ab1606975 --- /dev/null +++ b/tools/spec-loop/specs/sbom-pipeline.md @@ -0,0 +1,118 @@ + + +--- +title: SBOM pipeline (one CycloneDX SBOM per distributable artifact) +status: proposed +kind: feature +mode: infra +source: > + tools/sbom-pipeline/README.md (contract:sbom-pipeline, #1900). The + contract is written; no adapter, tool or skill implements it yet. +acceptance: + - Every distributable artifact, and the source release, has exactly one + CycloneDX 1.7 JSON SBOM of its own, complete without any other SBOM. + - Two builds of the same source produce byte-identical SBOMs. + - Content embedded from another ecosystem is described in the + container's SBOM as a nested assembly, never by reference. + - validate reports C1–C11 as passed, failed or for review, naming the + offending bom-ref. +--- + +## What it does + +Defines what a project's SBOMs must contain so that every distributable +artifact a project ships, whatever its ecosystem, carries one CycloneDX 1.7 +JSON SBOM describing exactly what is inside it and what it needs from its +environment, and so that the source release carries one describing the +files that do not share the project's terms. + +The contract fixes what the SBOMs say, not which generator writes them: +per-ecosystem adapters plug a project's existing generators into one +pipeline whose shared steps (`embed`, `normalize`, `validate`) are a single +deterministic tool. + +## Where it lives + +- `tools/sbom-pipeline/README.md` — the contract: rules 1–7, the + operations, conformance checks C1–C11, the planned adapters and + consumers, and the open questions. +- `contract:sbom-pipeline` is registered in + `tools/skill-and-tool-validator` (known capabilities), + `tools/vendor-neutrality-score` (`VENDOR_BACKED`, ❌ until two adapters + ship), `docs/labels-and-capabilities.md` and the generated + `.github/labeler.yml` rule. +- Planned, not present: `tools/sbom-pipeline-/` adapters + (maven, gradle, js, python, go, rust) and the `sbom-pipeline-integrate` + and `sbom-pipeline-verify` skills. + +## Behaviour & contract + +- **One SBOM per artifact.** + An artifact's SBOM is complete on its own; there is no combined SBOM + for a project. +- **Subject.** + `metadata.component` carries the artifact's `type`, `name`, `version`, + SPDX `licenses`, the project's configured `supplier`, and a `purl` in + the artifact's own ecosystem with the qualifiers that tell it apart from + the release's other artifacts, so no two artifacts share a subject + `purl`. +- **Components.** + Each has a deterministic `bom-ref` (its `purl`, else `name@version`, + prefixed by the containing component's `bom-ref` when nested), and the + `dependencies` graph covers every component. + Build- and test-only dependencies are left out; a generator that cannot + leave them out marks them `scope: excluded` and `normalize` removes them. +- **Assemblies.** + Embedded content is nested under the component that contains it. + A nested component's external requirement points at the artifact's own + top-level component when the artifact embeds it, and is otherwise + hoisted to the top level with `isExternal: true`, one entry per distinct + requirement. +- **Reproducibility.** + `serialNumber` is a version 5 UUID derived from the subject's `purl`, a + build-produced SBOM has `version` `1`, `metadata.timestamp` comes from + `SOURCE_DATE_EPOCH`, every array of objects is sorted by a canonical key + and every array of strings lexically, and nothing host-specific is + recorded. +- **Distribution.** + An SBOM is published next to or inside its artifact, as the artifact's + own channel dictates; one beside a download-area artifact carries its + own checksums and signature. +- **Source release.** + The source SBOM lists, as file components, every file whose license or + copyright differs from the project's own; its inventory comes from the + source tree and is cross-checked against `LICENSE` and `NOTICE`. + Rule 7 is provisional until the source release's `purl` type is settled. + +## Out of scope + +- Choosing a generator for a project: the adapters name examples only. +- Vulnerability data (VEX) and SPDX output. +- Publishing an aggregate SBOM for a whole project. + +## Acceptance criteria + +- The README defines rules 1–7, the eight operations and checks C1–C11, + each check reported as passed, failed or for review. +- `contract:sbom-pipeline` is a known capability in the validator, has a + vendor-neutrality row, and a labeler rule. +- No adapter claims `status: available` without a README declaring what + "Adding an adapter" requires. + +## Validation + +```bash +uv run --project tools/skill-and-tool-validator skill-and-tool-validate +uv run --project tools/vendor-neutrality-score vendor-neutrality-score --markdown +``` + +## Known gaps + +- **No adapter, tool or skill implements the contract yet.** + Every adapter and both consumer skills are `planned`. + +- **Open questions in the contract are unresolved.** + The source release's `purl` type, the supplier configuration key, and + where generator versions are recorded. + diff --git a/tools/vendor-neutrality-score/src/vendor_neutrality_score/__init__.py b/tools/vendor-neutrality-score/src/vendor_neutrality_score/__init__.py index e2de672e3..b606021ab 100644 --- a/tools/vendor-neutrality-score/src/vendor_neutrality_score/__init__.py +++ b/tools/vendor-neutrality-score/src/vendor_neutrality_score/__init__.py @@ -121,6 +121,10 @@ VENDOR_BACKED, "Git-hook managers under the secure setup: install target and sandbox / host homes (prek, …)", ), + "contract:sbom-pipeline": ( + VENDOR_BACKED, + "SBOM generation in the build: one CycloneDX 1.7 SBOM per distributable artifact, any ecosystem", + ), } # Which capability *contract* a skill actually invokes, keyed by