Attestation Reference
This is the sign/verify/vuln-gate capability reference for Supply-Chain Attestations — what each capability needs installed, the exact composition shape, and the KEV/EPSS gating fields.
Prerequisites
Section titled “Prerequisites”What each capability needs installed. The dir/zip/jar SBOM path is the only one that runs with nothing external installed.
| Capability | External tool | Tool-free? |
|---|---|---|
generate-sbom — dir / zip / jar | none (pure-TypeScript lockfile backend) | Yes |
generate-sbom — image | syft, via an explicitly injected toolSbomGenerator (shipped in #610) | No |
sign / attest-provenance / verify | cosign | No |
vuln-gate | grype or trivy (auto-detected at scan time) | No |
Generating an SBOM tool-free
Section titled “Generating an SBOM tool-free”Before any of the signing/provenance material below, there’s a step that needs no external tool at all: generate-sbom. Every artifact type it supports (dir, zip, jar — not image, see the aside below) defaults to lockfileSbomGenerator (packages/core/src/components/verbs/lockfile-sbom-generator.ts), a pure-TypeScript backend that parses a package-lock.json/pom.xml already on disk and emits a real SPDX/CycloneDX document. No syft, no network, no Docker.
import { phase, type Component } from "@intentius/chant/components/component";
export const myLib: Component = { name: "my-lib", archetype: "producer-library", dependsOn: [], deploy: [ phase("Sbom", [ { kind: "generate-sbom", artifactType: "dir", path: ".", format: "spdx" }, ]), ],};Run it:
chant run --components my-lib --env localThat writes sbom.spdx.json next to the scanned package-lock.json (lockfileSbomGenerator’s default outDir) and folds an sbom-kind entry into the component’s build-archive manifest. examples/supply-chain is this exact pattern, plus a second, structural BOM over a synthesized IaC template via extract-config-bom — clone it and run npm install && npm run supply-chain to see both artifacts land on disk.
First-signing walkthrough
Section titled “First-signing walkthrough”One digest carries all three attestations — SBOM, provenance, and signature — and verify checks that digest before a deploy is allowed to proceed.
The composition below is the real authoring shape — a Component built from phase()/Step (packages/core/src/components/component.ts), the exact shape chant run --components dispatches, not a paraphrase. It’s written here as { kind: … } literals to keep every field visible; each step also has a typed builder (sign({ … }), verify({ … })) — see Component Contract:
import { phase, type Component } from "@intentius/chant/components/component";
export const webService: Component = { name: "web-service", archetype: "service", dependsOn: [], build: { kind: "docker-build", context: "." }, deploy: [ phase("Build", [ { kind: "docker-build", context: ".", into: "web-service.tar", sourceRef: "$env.GIT_SHA" }, ]), phase("Publish", [ // "@Build.digest" reads the prior "Build" phase's docker-build output — // publish-image's own output, "@Publish.uri", is what every step below wires from. { kind: "publish-image", from: "archive:web-service.tar", to: "123.dkr.ecr.us-east-1.amazonaws.com/web-service" }, ]), phase("Sbom", [ { kind: "generate-sbom", artifactType: "image", path: "archive:web-service.tar", digest: "@Build.digest" }, ]), phase("Attest", [ { kind: "attest-provenance", imageRef: "@Publish.uri", // { sourceRef, artifactDigest } — the same link docker-build folded into the archive manifest (#614) provenance: { sourceRef: "$env.GIT_SHA", artifactDigest: "@Build.digest" }, builderId: "https://github.com/actions/runner", }, ]), phase("Sign", [ { kind: "sign", imageRef: "@Publish.uri" }, // keyless — no key config needed ]), phase("Verify", [ { kind: "verify", imageRef: "@Publish.uri", policy: { expectedIssuer: "https://token.actions.githubusercontent.com", expectedIdentity: "https://github.com/my-org/my-repo/.github/workflows/release.yml@refs/heads/main", }, }, ]), phase("Apply", [ { kind: "cfn-deploy", template: "archive:web-service.template.json" }, // only runs if Verify passed ]), ],};Run it: chant run --components web-service --env prod. Every step from Sbom onward takes the same "@Publish.uri" digest (or "@Build.digest", threaded from the Build phase) — build once, attest three times, verify once, all against bytes that never change underneath you.
“Done right” here means: the digest deployed is the digest that was signed, and verify ran and passed before apply — not signed-and-forgotten, and not a signature that gets checked manually after the fact if someone remembers. Put verify before the Apply phase in the composition and a failed check throws, halting the deploy the same way any other thrown capability error does (see Orchestration) — an unsigned or wrongly-signed artifact never reaches apply.
VEX/CVE gating with vuln-gate
Section titled “VEX/CVE gating with vuln-gate”vuln-gate scans an SBOM for known CVEs, suppresses the ones a VEX document says don’t actually apply, checks a license policy, and throws VulnGateFailedError on a violation — the same throw-to-halt mechanism as verify:
phase("VulnGate", [ { kind: "vuln-gate", sbom: "@Sbom.sbom", // wired from the generate-sbom step's output, same phase-output convention as "@Publish.uri" policy: {}, // filled from chant.config.ts's vulnPolicy section (#629) when left empty },]),The default policy (DEFAULT_VULN_POLICY, packages/core/src/components/verbs/vuln-gate.ts) is beginner-safe: block only critical + fixable + not-VEX-suppressed findings; warn on high; license findings are report-only unless failOnLicense: true. Exploitability gating (KEV/EPSS, next section) is off by default for the same reason.
Exploitability gating: KEV and EPSS
Section titled “Exploitability gating: KEV and EPSS”Severity answers “how bad is this bug if someone exploits it.” It says nothing about whether anyone actually is. Two public data sources answer that second question, and vuln-gate can gate on both (#1461): KEV membership (see the glossary — the CVE is being exploited in the wild, today) and the EPSS score (how likely exploitation is in the next 30 days). A medium-severity CVE in the KEV catalog is usually more urgent than a critical one nobody has weaponized; these fields let the gate say so.
Four vulnPolicy fields control it, settable project-wide in chant.config.ts or per-step in the policy field (step wins field-by-field, same as every other vulnPolicy field):
| Field | What it does | Default |
|---|---|---|
failOnKev | Block any finding in the CISA KEV catalog, regardless of severity. | false |
failEpssAtOrAbove | Block a finding whose EPSS score is at or above this (0.0–1.0). | unset — EPSS ignored |
warnEpssAtOrAbove | Warn (never block) at or above this EPSS score. | unset |
exploitabilityFixableOnly | Mirror fixableOnly for KEV/EPSS blocks: an unfixable KEV finding warns instead of blocking, since no upgrade can action it. | true |
A recommended strict posture you can paste:
export default { vulnPolicy: { failOnKev: true, // block anything actively exploited failEpssAtOrAbove: 0.1, // and anything likely to be warnEpssAtOrAbove: 0.01, },};The defaults are deliberately off. Upgrading chant does not newly block anyone’s deploy — a KEV finding under the default policy gates exactly as it did before these fields existed. Turning failOnKev on is a policy decision your team makes, in a config file a reviewer sees, not something an upgrade makes for you.
Three rules govern how exploitability interacts with the rest of the gate:
- It escalates, never de-escalates. KEV and EPSS blocks are OR’d with the severity rules. A critical fixable finding blocks whether or not it is in KEV; a low EPSS score exempts nothing. A finding that trips several rules is reported once, under the most specific reason (
kevoverepss-thresholdoverseverity-threshold), andVulnGateFailedError’s message names the rule that fired for each finding —CVE-2024-12345 (medium, libfoo) — in CISA KEV since 2024-03-11, known ransomware use, not “policy violation.” - VEX still wins, KEV included. A VEX
not_affectedstatement suppresses a KEV finding. This is deliberate: VEX is a claim that the CVE does not apply to this artifact as built; the KEV catalog says the CVE is exploited somewhere. A specific claim about your artifact outranks a general one about the CVE — and if a team VEX-suppresses a KEV finding wrongly, that is a reviewable statement in a document, which is the whole point of VEX. - Absent data never matches. A finding with no EPSS score is “not scored,” not “scored zero” — it can never trip an EPSS threshold, even a threshold of
0.
Read next
Section titled “Read next”- Supply-Chain Attestations — what SBOM, provenance, and signing actually are, in plain language, and why keyless signing and digest-based verification.
- Build Archive — how the SBOM is generated and where the digest
sign/verifyoperate on comes from. - Observability — the build ledger’s referrer discovery, which can surface a digest’s signature/provenance/SBOM referrers after the fact.
- Orchestration — how a thrown
VerificationFailedErrorhalts a composition beforeapplyruns. - examples/supply-chain — the runnable version of this page:
npm install && npm run supply-chainproduces a real SBOM + config-BOM tool-free, plus the tool-gated steps above as a separate, clearly-marked component.