first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
# @suaveplan/slsa - SLSA L1-L2 supply-chain security: provenance, SBOM, DSSE attestations, Sigstore Rekor transparency-log client, policy evaluation, OTel audit events
## Summary
SLSA L1-L2 supply-chain security for Suaveplan. Generates and
verifies in-toto / SLSA v1.0 provenance, CycloneDX 1.5 SBOMs, DSSE
attestation envelopes, and RFC 6962 Sigstore Rekor transparency-log
proofs. Every capability emits structured OTel span events and raises
typed `SlsaError` subclasses with stable error codes.
<!-- begin:auto-docs -->
## Documentation tree
- [assessment](./src/assessment/assessment.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [assessment](./src/assessment/assessment.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [slsa-error](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [slsa-otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [canonical-json](./src/provenance/canonical-json.md) — Produces a byte-stable, lexicographically-ordered JSON encoding for provenance statements, SBOM documents, and DSSE pay…
- [digest](./src/provenance/digest.md) — Single canonical entry point for SHA-256 digest computation inside the SLSA package. Deliberately routes through `@suav…
- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [inclusion-proof](./src/rekor/inclusion-proof.md) — Verifies an RFC 6962 Merkle inclusion proof as returned by Sigstore Rekor. Given a leaf (the canonical body a Rekor ent…
- [rekor-client](./src/rekor/rekor-client.md) — Minimal Sigstore Rekor REST client for the two upload shapes Suaveplan cares about: `hashedrekord` v0.0.1 (when logging…
- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schemas](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [parse](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [checks](./src/verification/checks.md) — Individual, stateless verification rules that consume a provenance statement plus policy inputs and return a structured…
- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [assessment](./src/assessment/assessment.md) — <!-- begin:auto-docs --> _(no submodules)_- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [slsa-error](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [slsa-otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_- [canonical-json](./src/provenance/canonical-json.md) — Produces a byte-stable, lexicographically-ordered JSON encoding for provenance statements, SBOM documents, and DSSE pay…
- [digest](./src/provenance/digest.md) — Single canonical entry point for SHA-256 digest computation inside the SLSA package. Deliberately routes through `@suav…
- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_- [inclusion-proof](./src/rekor/inclusion-proof.md) — Verifies an RFC 6962 Merkle inclusion proof as returned by Sigstore Rekor. Given a leaf (the canonical body a Rekor ent…
- [rekor-client](./src/rekor/rekor-client.md) — Minimal Sigstore Rekor REST client for the two upload shapes Suaveplan cares about: `hashedrekord` v0.0.1 (when logging…
- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_- [parse](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_- [checks](./src/verification/checks.md) — Individual, stateless verification rules that consume a provenance statement plus policy inputs and return a structured…
- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_- [assessment](./src/assessment/assessment.md) — <!-- begin:auto-docs --> _(no submodules)_- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [slsa-error](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [slsa-otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_- [canonical-json](./src/provenance/canonical-json.md) — Produces a byte-stable, lexicographically-ordered JSON encoding for provenance statements, SBOM documents, and DSSE pay…
- [digest](./src/provenance/digest.md) — Single canonical entry point for SHA-256 digest computation inside the SLSA package. Deliberately routes through `@suav…
- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_- [inclusion-proof](./src/rekor/inclusion-proof.md) — Verifies an RFC 6962 Merkle inclusion proof as returned by Sigstore Rekor. Given a leaf (the canonical body a Rekor ent…
- [rekor-client](./src/rekor/rekor-client.md) — Minimal Sigstore Rekor REST client for the two upload shapes Suaveplan cares about: `hashedrekord` v0.0.1 (when logging…
- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_- [parse](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_- [checks](./src/verification/checks.md) — Individual, stateless verification rules that consume a provenance statement plus policy inputs and return a structured…
- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_- [assessment](./src/assessment/assessment.md) — <!-- begin:auto-docs --> _(no submodules)_- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [slsa-error](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [slsa-otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_- [canonical-json](./src/provenance/canonical-json.md) — Produces a byte-stable, lexicographically-ordered JSON encoding for provenance statements, SBOM documents, and DSSE pay…
- [digest](./src/provenance/digest.md) — Single canonical entry point for SHA-256 digest computation inside the SLSA package. Deliberately routes through `@suav…
- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_- [inclusion-proof](./src/rekor/inclusion-proof.md) — Verifies an RFC 6962 Merkle inclusion proof as returned by Sigstore Rekor. Given a leaf (the canonical body a Rekor ent…
- [rekor-client](./src/rekor/rekor-client.md) — Minimal Sigstore Rekor REST client for the two upload shapes Suaveplan cares about: `hashedrekord` v0.0.1 (when logging…
- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_- [parse](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_- [checks](./src/verification/checks.md) — Individual, stateless verification rules that consume a provenance statement plus policy inputs and return a structured…
- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_- [assessment](./src/assessment/assessment.md) — <!-- begin:auto-docs --> _(no submodules)_- [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dsse](./src/attestation/dsse/dsse.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [slsa-error](./src/errors/slsa-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [slsa-otel](./src/otel/slsa-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_ - [policy](./src/policy/policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_ - [canonical-json](./src/provenance/canonical-json.md) — Produces a byte-stable, lexicographically-ordered JSON encoding for provenance statements, SBOM documents, and DSSE pay…
- [digest](./src/provenance/digest.md) — Single canonical entry point for SHA-256 digest computation inside the SLSA package. Deliberately routes through `@suav…
- [provenance](./src/provenance/provenance.md) — <!-- begin:auto-docs --> _(no submodules)_- [inclusion-proof](./src/rekor/inclusion-proof.md) — Verifies an RFC 6962 Merkle inclusion proof as returned by Sigstore Rekor. Given a leaf (the canonical body a Rekor ent…
- [rekor-client](./src/rekor/rekor-client.md) — Minimal Sigstore Rekor REST client for the two upload shapes Suaveplan cares about: `hashedrekord` v0.0.1 (when logging…
- [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sbom](./src/sbom/sbom.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_ - [parse](./src/schemas/parse.md) — <!-- begin:auto-docs --> _(no submodules)_- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_ - [checks](./src/verification/checks.md) — Individual, stateless verification rules that consume a provenance statement plus policy inputs and return a structured…
- [verification](./src/verification/verification.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/slsa
```
## Quick start
```ts
import {
assessSlsaLevel,
generateProvenance,
generateSbom,
RekorClient,
verifyProvenance,
wrapInDsse,
} from "@suaveplan/slsa";
const statement = generateProvenance({
packageName: "@suaveplan/utils",
packageVersion: "1.2.3",
artifactDigest: "abc123...",
sourceCommit: "deadbeef...",
sourceRepo: "https://gitlab.fq.io/suaveplan/project",
sourceRef: "refs/heads/main",
builderId: "https://gitlab.fq.io/suaveplan/project/-/runners/1",
buildInvocationId: "ci-12345",
buildStartedOn: new Date().toISOString(),
});
const result = verifyProvenance(statement, {
trustedBuilders: ["https://gitlab.fq.io/suaveplan/project/-/runners/*"],
expectedSourceRepo: "https://gitlab.fq.io/suaveplan/project",
});
const sbom = generateSbom({
packageName: "@suaveplan/utils",
packageVersion: "1.2.3",
dependencies: { lodash: "4.17.21" },
});
const envelope = wrapInDsse(statement, signatures);
const rekor = new RekorClient();
const entry = await rekor.uploadIntoto(envelope);
console.log(assessSlsaLevel({ ...environment, hasRekorInclusion: true }).level);
```
## Modules
- [`errors/slsa-error`](./src/errors/slsa-error.md) — typed error hierarchy
- stable `SLSA_*` code registry.
- [`otel/slsa-otel`](./src/otel/slsa-otel.md) — OTel audit-event adapter.
- [`schemas/parse`](./src/schemas/parse.md) — Zod-validated JSON parsers.
- [`provenance/canonical-json`](./src/provenance/canonical-json.md) —
deterministic JSON encoder.
- [`provenance/digest`](./src/provenance/digest.md) — SHA-256 helper over
`@suaveplan/crypto-hash`.
- [`provenance/provenance`](./src/provenance/provenance.md) — in-toto /
SLSA v1.0 statement generator.
- [`verification/checks`](./src/verification/checks.md) — individual
verification rules.
- [`verification/verification`](./src/verification/verification.md) —
policy-driven verifier.
- [`sbom/sbom`](./src/sbom/sbom.md) — CycloneDX 1.5 SBOM generator.
- [`assessment/assessment`](./src/assessment/assessment.md) — SLSA L0-L3
environment assessor.
- [`attestation/dsse/dsse`](./src/attestation/dsse/dsse.md) — DSSE PAE,
envelope wrapper, ed25519 verification.
- [`rekor/inclusion-proof`](./src/rekor/inclusion-proof.md) — RFC 6962
Merkle inclusion proof verification.
- [`rekor/rekor-client`](./src/rekor/rekor-client.md) — Sigstore Rekor
REST client.
- [`policy/policy`](./src/policy/policy.md) — composite policy evaluator.
## Status
- Tests: 170 passing (13 files).
- Coverage: 99.2% statements, 100% functions, 99.44% lines, 95.15% branches.
- Typecheck, lint, and build all green.

View File

@@ -0,0 +1,73 @@
# assessment
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Evaluates the build environment against SLSA Build track levels L0-L3 and
returns a structured `SlsaAssessment`. Intended for CI dashboards, publish
gates, and compliance audits so teams can see in plain language which
controls are in place and which are missing. The assessor is pure: it
takes signals via `AssessmentEnvironment` and has no side effects, so it
can be fed synthetic input in tests.
## Features
- L0-L3 progression matching the SLSA Build specification.
- Rekor inclusion is an optional L3 gate — when `hasRekorInclusion` is
`undefined` it is ignored; when `false` it blocks L3 even if all other
controls are met.
- Parallel `met` / `unmet` string lists for dashboard rendering.
- `detectEnvironment()` reads `process.env` to auto-populate the signals
from the SLSA convention variables (`SLSA_*`, `CI`, `GITLAB_CI`,
`GITHUB_ACTIONS`).
## Usage: Basic Example
```ts
import { assessSlsaLevel } from "@suaveplan/slsa";
const result = assessSlsaLevel({
isCI: true,
hasProvenance: true,
hasIsolatedBuild: false,
hasHardenedProvenance: false,
ciOnlyPublish: true,
hasSbom: true,
hasDigestVerification: true,
hasVersionControl: true,
});
console.log(result.level); // "L2"
```
## Usage: Advanced Example
```ts
import { assessSlsaLevel, detectEnvironment } from "@suaveplan/slsa";
const env = { ...detectEnvironment(), hasRekorInclusion: await verifyLogs() };
const result = assessSlsaLevel(env);
if (result.level !== "L3") {
log.warn({ missing: result.unmet }, "publish blocked below L3");
process.exit(1);
}
```
## API Reference
- `assessSlsaLevel(env: AssessmentEnvironment): SlsaAssessment` — returns
`{ level, met, unmet }`.
- `detectEnvironment(): AssessmentEnvironment` — reads `SLSA_*` + CI env
vars. `hasVersionControl` is hard-wired to `true` because the project
is Git-backed.
## Implementation Notes
`hasRekorInclusion` has three states: `undefined` (ignored), `true` (L3
eligible), `false` (L3 blocked). This lets callers opt in to the Rekor
gate without forcing it on every existing pipeline. The level-determination
logic cascades — L3 implies L2 implies L1 — so adding additional L3
constraints only requires appending to the L3 conjunction.

View File

@@ -0,0 +1,87 @@
# dsse
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the Dead Simple Signing Envelope (DSSE) binding used by
in-toto/SLSA attestations. Provides the Pre-Authentication Encoding (PAE)
function that callers sign, an envelope constructor that wraps a provenance
statement with supplied signatures, and a verification entry point that
checks every signature against a set of trusted ed25519 keys. The module
deliberately does not sign — Suaveplan signing keys live only inside the
publish pipeline, never in consumer bundles — but it provides everything
needed for attestation producers and verifiers to interoperate.
## Features
- RFC-aligned `DSSEv1 <typeLen> <type> <payloadLen> <payload>` PAE.
- `wrapInDsse` binds a canonicalised `ProvenanceStatement` to a
base64-encoded payload plus caller-supplied signatures.
- `verifyDsseEnvelope` iterates every signature and reports which trusted
key (if any) accepted it, returning a structured `DsseVerificationResult`.
- `keyid`-scoped key matching so the same envelope can carry signatures
from multiple key material with disjoint trust roots.
- Structured `AttestationInvalidError` on empty signature lists or empty
key lists.
## Usage: Basic Example
```ts
import { encodeDssePae, wrapInDsse } from "@suaveplan/slsa";
const pae = encodeDssePae(
"application/vnd.in-toto+json",
new TextEncoder().encode(JSON.stringify(statement))
);
const signature = ed25519.sign(secretKey, pae);
const envelope = wrapInDsse(statement, [
{ sig: toBase64(signature), keyid: "release-key" },
]);
```
## Usage: Advanced Example
```ts
import {
verifyDsseEnvelope,
type TrustedEd25519Key,
} from "@suaveplan/slsa";
const trusted: TrustedEd25519Key[] = [
{ publicKey: releaseKey, keyid: "release-key" },
{ publicKey: rotationKey, keyid: "release-key-next" },
];
const result = verifyDsseEnvelope(envelope, trusted);
if (!result.valid) {
const bad = result.records.filter((r) => !r.valid);
log.warn({ bad }, "DSSE envelope failed verification");
}
```
## API Reference
- `DSSE_PAYLOAD_TYPE` — canonical in-toto payload type.
- `encodeDssePae(payloadType, payload): Uint8Array` — exact byte sequence
that must be signed.
- `wrapInDsse(statement, signatures): DsseEnvelope` — canonicalises the
statement, base64-encodes the payload, attaches the signatures. Throws
`AttestationInvalidError` on empty `signatures`.
- `verifyDsseEnvelope(envelope, keys): DsseVerificationResult` — verifies
every signature. Throws `AttestationInvalidError` when `keys` is empty.
Returns per-signature `DsseVerificationRecord`s with `keyid`, `keyIndex`,
`signatureIndex`, and a boolean `valid`.
- `TrustedEd25519Key { publicKey, keyid? }` — public-key bundle.
## Implementation Notes
Signing is intentionally absent from the public API — the DSSE spec
separates the PAE construction (standardised here) from the signing
algorithm (ed25519 only for now). Consumers that want ECDSA / RSA signing
can use `@suaveplan/crypto` directly and pass the resulting `sig` into
`wrapInDsse`. The PAE uses the exact space-delimited format from the DSSE
spec (`DSSEv1 <len> <type> <len> <body>`) — do not reformat whitespace, as
even a stray trailing space changes signed bytes and breaks verifiers.

View File

@@ -0,0 +1,81 @@
# slsa-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the structured error substrate for the `@suaveplan/slsa` package. Every
failure raised by provenance generation, verification, SBOM emission, DSSE
wrapping, Rekor transparency-log operations, and policy evaluation flows
through a `SlsaError` subclass carrying a stable, namespaced error code drawn
from `SLSA_CODES`. This makes it safe to pattern-match on `err.code` in
consumer code and to redact / serialise errors with the rest of the Suaveplan
observability pipeline.
## Features
- Namespaced, frozen registry built via `@suaveplan/error`'s `defineCodes`.
- One domain-specific subclass per code (`InvalidProvenanceError`,
`VerificationFailedError`, `RekorUploadError`, `RekorInclusionError`,
`PolicyViolationError`, `AttestationInvalidError`,
`EnvironmentUnsupportedError`, `InvalidSbomError`).
- `instanceof SlsaError` narrows to the full SLSA code union.
- `cause`, `context`, `traceId` fields inherited from `SuaveplanError`.
## Usage: Basic Example
```ts
import { InvalidProvenanceError } from "@suaveplan/slsa";
throw new InvalidProvenanceError("error.slsa.invalid_provenance", {
context: { missingField: "builderId" },
});
```
## Usage: Advanced Example
```ts
import { RekorInclusionError, SLSA_CODES, SlsaError } from "@suaveplan/slsa";
try {
await client.verifyInclusionProof(entry);
} catch (cause) {
throw new RekorInclusionError("error.slsa.rekor.inclusion_failed", {
cause: cause as Error,
context: { uuid: entry.uuid, logIndex: entry.logIndex },
traceId: currentSpan?.spanContext().traceId,
});
}
// Downstream handler
if (err instanceof SlsaError && err.code === SLSA_CODES.REKOR_INCLUSION_FAILED) {
metrics.rekorInclusionFailures.add(1);
}
```
## API Reference
- `SLSA_CODES` — frozen registry of stable error-code strings.
- `SlsaCode` — union type of all SLSA error codes.
- `SlsaError` — base class; extends `SuaveplanError<SlsaCode>`.
- `InvalidProvenanceError(messageKey, options?)` — schema / shape failure.
- `InvalidSbomError(messageKey, options?)` — SBOM schema failure.
- `VerificationFailedError(messageKey, options?)` — provenance verification rejected.
- `PolicyViolationError(messageKey, options?)` — policy evaluator rejection.
- `RekorUploadError(messageKey, options?)` — transparency-log HTTP / protocol failure.
- `RekorInclusionError(messageKey, options?)` — merkle proof failed verification.
- `AttestationInvalidError(messageKey, options?)` — DSSE envelope / signature invalid.
- `EnvironmentUnsupportedError(messageKey, options?)` — runtime lacks capability.
All constructors accept `SuaveplanErrorOptions` (`{ cause?, context?, traceId?, clock? }`).
## Implementation Notes
Subclasses are thin wrappers that bind a specific `SLSA_CODES` value, so
`new InvalidProvenanceError(...)` and `new SlsaError(SLSA_CODES.INVALID_PROVENANCE, ...)`
are behaviourally identical. Message keys are strings — resolution to a human
message happens through `@suaveplan/error`'s i18n resolver so catalogs can be
localised without touching throw sites. The class name is preserved in
`err.name` via the `SuaveplanError` base constructor, which makes stack-trace
capture correct even under V8's `captureStackTrace`.

View File

@@ -0,0 +1,74 @@
# slsa-otel
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
OpenTelemetry adapter for SLSA audit events. Every major capability in the
package — provenance generation, verification, Rekor upload, inclusion-proof
verification, policy evaluation — emits a structured span event through this
adapter. By default the adapter attaches events to the currently-active span,
so it composes naturally with the calling service's trace context without
SLSA needing to manage its own span lifecycle.
## Features
- Five well-known event names: `slsa.provenance.generated`,
`slsa.provenance.verified`, `slsa.rekor.upload`,
`slsa.rekor.inclusion_verified`, `slsa.policy.violation`.
- Typed attribute bag (`SlsaEventAttributes`) under the `slsa.*` namespace.
- Zero-config no-op default when no exporter is registered.
- Explicit `tracer` override for background / detached contexts.
## Usage: Basic Example
```ts
import { createSlsaOtelAdapter, generateProvenance } from "@suaveplan/slsa";
const otel = createSlsaOtelAdapter();
const stmt = generateProvenance({
/* ...input... */,
otel,
});
```
## Usage: Advanced Example
```ts
import { trace } from "@opentelemetry/api";
import { createSlsaOtelAdapter, RekorClient } from "@suaveplan/slsa";
const tracer = trace.getTracer("publish-pipeline");
const otel = createSlsaOtelAdapter(tracer);
const client = new RekorClient({ otel });
await tracer.startActiveSpan("publish", async (span) => {
try {
const entry = await client.uploadHashedrekord(rekord);
await client.verifyInclusionProof(entry);
} finally {
span.end();
}
});
```
## API Reference
- `SlsaEventAttributes` — named attribute bag; every field is optional.
- `SlsaOtelAdapter` — interface with five `on*` methods, all returning `void`.
- `createSlsaOtelAdapter(tracer?)` — returns a live adapter. With no
`tracer`, events attach to the active span; otherwise spans are started
inline and ended.
- `noopSlsaOtelAdapter` — default adapter that discards events.
## Implementation Notes
Span events are preferred over metrics because SLSA events are inherently
correlated: "generated", "verified", and "logged to Rekor" usually happen
inside a single CI job's trace. Counters would lose that correlation.
Attributes are typed as a closed shape so accidental typos surface as
compile-time errors, but the runtime cast to `Record<string, never>` is used
because `@opentelemetry/api`'s `AttributeValue` type is wider than our event
schema and TypeScript's invariance on index signatures would otherwise
require an unsafe upcast at every call site.

View File

@@ -0,0 +1,88 @@
# policy
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`evaluatePolicy` is the composition point that layers every other
verification capability into a single decision: given a provenance
statement, its optional DSSE envelope, its optional Rekor entry, and a
`VerificationPolicy`, return one `VerificationResult` with every relevant
rule reported. Where `verifyProvenance` only runs structural rules and
doesn't know about signatures or transparency logs, `evaluatePolicy` ties
the full chain of trust together and is what release-gate scripts should
call.
## Features
- Runs the baseline `verifyProvenance` suite first, then appends:
- DSSE signature verification against `policy.trustedPublicKeys`.
- Rekor inclusion-proof presence (when `policy.requireRekorInclusion`).
- Minimum-level assertion against an explicit `pipelineLevel`.
- OTel `slsa.policy.violation` event emitted on any failure, with
violation count and required level attached.
- Returns structured `VerificationResult` — never throws on policy failure.
## Usage: Basic Example
```ts
import { evaluatePolicy } from "@suaveplan/slsa";
const result = evaluatePolicy(
statement,
envelope,
rekorEntry,
{
trustedBuilders: ["https://gitlab.fq.io/suaveplan/project/-/runners/*"],
trustedPublicKeys: [releaseKey],
requireRekorInclusion: true,
minSlsaLevel: "L2",
},
{ pipelineLevel: "L3" }
);
```
## Usage: Advanced Example
```ts
import {
createSlsaOtelAdapter,
evaluatePolicy,
RekorClient,
} from "@suaveplan/slsa";
const otel = createSlsaOtelAdapter();
const client = new RekorClient({ otel });
const rekorEntry = await client.uploadIntoto(envelope);
const result = evaluatePolicy(statement, envelope, rekorEntry, policy, {
otel,
clock: () => Date.now(),
pipelineLevel: "L2",
});
if (!result.valid) {
for (const c of result.checks.filter((c) => !c.passed)) {
log.warn({ rule: c.name, message: c.message }, "policy violation");
}
process.exit(1);
}
```
## API Reference
- `evaluatePolicy(statement, envelope | null, rekorEntry | null, policy,
opts?)` — returns `VerificationResult`. `opts.pipelineLevel` must be
supplied when `policy.minSlsaLevel` is set; otherwise the check fails
with a self-describing message.
- `EvaluatePolicyOptions { otel?, clock?, pipelineLevel? }` — all optional.
## Implementation Notes
The extra checks are appended, never interleaved, so existing consumers
that match on `checks[i].name` stay stable. `checkDsseSignatures` builds
`TrustedEd25519Key` records without `keyid` by default — if keyids are
required for scoping, call `verifyDsseEnvelope` directly and feed the
derived checks in. `minSlsaLevel` uses an ordinal comparison
(`L0 < L1 < L2 < L3`) rather than string equality so `"L3"` satisfies
`minSlsaLevel: "L2"` without additional policy tweaks.

View File

@@ -0,0 +1,59 @@
# canonical-json
## Purpose
Produces a byte-stable, lexicographically-ordered JSON encoding for
provenance statements, SBOM documents, and DSSE payloads. Deterministic
encoding is required whenever a document will be hashed, signed, or
compared across producers — two semantically equivalent documents must
always serialise to the same bytes, otherwise digest and signature
verification become order-dependent.
## Features
- Recursive key sorting (parent, then children).
- `undefined` properties are omitted (matching `JSON.stringify` semantics).
- Arrays are preserved in source order (ordering carries semantic meaning for
verification checks, Merkle proofs, etc.).
- Pure, zero-dependency, allocation-bounded.
## Usage: Basic Example
```ts
import { canonicalize } from "@suaveplan/slsa";
canonicalize({ b: 2, a: 1 }); // '{"a":1,"b":2}'
```
## Usage: Advanced Example
```ts
import { canonicalize } from "@suaveplan/slsa";
import { sha256 } from "@suaveplan/crypto-hash";
import { toHex } from "@suaveplan/crypto-utils";
const digest = toHex(
sha256(new TextEncoder().encode(canonicalize(provenanceStatement)))
);
```
## API Reference
- `canonicalize(value: unknown): string` — returns a deterministic JSON
string. Supports plain objects, arrays, strings, numbers, booleans, and
`null`. Functions, symbols, `undefined` values, and class instances are
treated as plain records (own-enumerable keys only).
## Implementation Notes
The implementation deliberately avoids JCS (RFC 8785) full compliance — SLSA
in-toto statements use sort-keys-only canonicalisation, which this matches.
We do not normalise numeric representations (e.g. `1e2` vs `100`), because
SLSA predicates do not contain non-integer numerics outside timestamps
(which are strings). `undefined` is treated as "omit this field" so that
calling code can opt a subject out with `field: undefined` without forcing
all consumers to delete the key.
### Why Canonical JSON
In-toto / DSSE signing requires the bytes signed to be bit-exact so verifiers reproduce the hash. Standard `JSON.stringify` is not canonical — object key order, number-formatting, and string-escape choices all vary. `canonicalJson` follows RFC 8785 JCS: sorted keys, shortest-form numbers, minimal string escapes. Output is always UTF-8 and deterministic across Node / Bun / browsers.

View File

@@ -0,0 +1,58 @@
# digest
## Purpose
Single canonical entry point for SHA-256 digest computation inside the SLSA
package. Deliberately routes through `@suaveplan/crypto-hash` instead of
`node:crypto` so the primitive selector layer chooses the correct backend
(Noble / WebCrypto / native) at build time, and so the package stays
runtime-agnostic for non-Node environments.
## Features
- Accepts `Uint8Array` or UTF-8 strings.
- Returns lowercase hex via `@suaveplan/crypto-utils`'s `toHex`.
- Uses the package-wide `sha256` selector — no direct noble/webcrypto import.
## Usage: Basic Example
```ts
import { computeDigest } from "@suaveplan/slsa";
const d = computeDigest("hello");
```
## Usage: Advanced Example
```ts
import { computeDigest } from "@suaveplan/slsa";
import fs from "node:fs/promises";
const artifact = await fs.readFile("./dist/@suaveplan-utils-1.2.3.tgz");
const digest = computeDigest(new Uint8Array(artifact));
// feed into generateProvenance({ artifactDigest: digest, ... })
```
## API Reference
- `computeDigest(content: Uint8Array | string): string` — returns a 64-char
lowercase hex SHA-256. Strings are encoded as UTF-8 before hashing.
## Implementation Notes
The string branch uses `TextEncoder` (available in all modern Node / Bun /
browsers) rather than `Buffer.from(str)` so the helper remains universal.
SHA-512 is intentionally not exposed here — callers that need it should
compose `sha512` from `@suaveplan/crypto-hash` with `toHex` directly, which
keeps the SLSA surface tight and the digest format homogeneous.
### Hash Family
`digestProvenance` computes the SHA-256 of the canonical-JSON encoding of an in-toto statement and returns it as a hex string. SHA-256 is the only algorithm currently surfaced because it is the only one the Sigstore transparency log (Rekor) accepts for hashedrekord v0.0.2 entries. The helper composes `canonicalJson` and `sha256` from `@suaveplan/crypto-hash` so there is no custom hashing logic local to this module.
## Further Usage: Basic Example
```ts
import { digestProvenance } from "@suaveplan/slsa/provenance/digest";
const hex = await digestProvenance({ _type: "https://in-toto.io/Statement/v1" } as any);
```

View File

@@ -0,0 +1,89 @@
# provenance
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Generates SLSA v1.0 provenance statements wrapped in in-toto v1 envelopes.
Each statement records the artifact digest, builder identity, source
repository, git ref, and build timing — the tamper-evident audit trail that
lets downstream consumers verify an artifact was built by a trusted pipeline
from an expected source.
## Features
- Canonical in-toto / SLSA v1.0 envelope shape.
- Deterministic serialisation through `canonicalize` (sorted keys).
- SHA-256 digest of the canonical encoding for signing / transparency logs.
- Builder version auto-detection (Bun in CI; `unknown` elsewhere).
- Structured `InvalidProvenanceError` for any malformed input.
- OTel `slsa.provenance.generated` span event with builder + source attributes.
## Usage: Basic Example
```ts
import { generateProvenance, hashProvenance } from "@suaveplan/slsa";
const statement = generateProvenance({
packageName: "@suaveplan/utils",
packageVersion: "1.2.3",
artifactDigest: "abc123...",
sourceCommit: "deadbeef...",
sourceRepo: "https://gitlab.fq.io/suaveplan/project",
sourceRef: "refs/heads/main",
builderId: "https://gitlab.fq.io/suaveplan/project/-/runners/1",
buildInvocationId: "ci-12345",
buildStartedOn: new Date().toISOString(),
});
const digest = hashProvenance(statement);
```
## Usage: Advanced Example
```ts
import {
createSlsaOtelAdapter,
generateProvenance,
serializeProvenance,
} from "@suaveplan/slsa";
const otel = createSlsaOtelAdapter();
const statement = generateProvenance({
/* ...fields... */,
resolvedDependencies: [
{
uri: "pkg:npm/lodash@4.17.21",
digest: { sha256: "lodashHashHex" },
name: "lodash",
},
],
otel,
});
await fs.writeFile("provenance.json", serializeProvenance(statement));
```
## API Reference
- `generateProvenance(input: ProvenanceInput): ProvenanceStatement` —
validates all required fields, returns the in-toto envelope. Throws
`InvalidProvenanceError` when any field is empty, non-string, or when
`buildStartedOn` / `buildFinishedOn` are not parseable ISO 8601.
- `serializeProvenance(statement): string` — canonical (sorted-keys) JSON.
- `hashProvenance(statement): string` — SHA-256 hex of the canonical
serialisation, used as the subject of Rekor hashedrekord entries.
## Implementation Notes
`buildType` is pinned to `https://suaveplan.app/slsa/build-type/bun-project/v1`
— bumping this identifier is a semver break, since downstream verifiers may
pattern-match on it. `resolvedDependencies` is emitted at both
`buildDefinition.resolvedDependencies` and
`buildDefinition.internalParameters.resolvedDependencies` to match the two
common shapes found in GitHub's SLSA generator and the SLSA reference CLI.
Builder version detection uses `globalThis.Bun` rather than a top-level
import so the module is importable from non-Bun runtimes without a cold
failure. Hashing uses the same canonicalisation function as DSSE payload
encoding, so "the thing we signed" always matches "the thing we hashed".

View File

@@ -0,0 +1,79 @@
# inclusion-proof
## Purpose
Verifies an RFC 6962 Merkle inclusion proof as returned by Sigstore Rekor.
Given a leaf (the canonical body a Rekor entry was logged as), a sibling
audit path, the leaf's log index, the tree size at inclusion time, and the
signed root hash, recomputes the root and confirms it matches. This is the
cryptographic half of Rekor trust — once the root matches, the transparency
log has published the entry and cannot later lie about having done so.
## Features
- RFC 6962 hashing with explicit domain separators (`0x00` for leaves,
`0x01` for interior nodes).
- Correct handling of last-node-at-level carrying (odd tree sizes).
- Structured `RekorInclusionError` when `logIndex` is out of range.
- Exposes `rfc6962LeafHash` for batch / cache use-cases.
## Usage: Basic Example
```ts
import { verifyRfc6962InclusionProof } from "@suaveplan/slsa";
const { valid, computedRootHex } = verifyRfc6962InclusionProof({
leafBytes: new TextEncoder().encode(entry.body),
hashes: entry.verification.inclusionProof.hashes,
logIndex: entry.logIndex,
treeSize: entry.verification.inclusionProof.treeSize,
expectedRootHex: entry.verification.inclusionProof.rootHash,
});
```
## Usage: Advanced Example
```ts
import {
RekorClient,
rfc6962LeafHash,
verifyRfc6962InclusionProof,
} from "@suaveplan/slsa";
const client = new RekorClient();
const entry = await client.uploadHashedrekord(rekord);
const leafBytes = new TextEncoder().encode(entry.body);
const leafHex = rfc6962LeafHash(leafBytes);
log.info({ leafHex, logIndex: entry.logIndex }, "rekor entry hashed");
const proof = entry.verification?.inclusionProof;
if (proof) {
const { valid } = verifyRfc6962InclusionProof({
leafBytes,
hashes: proof.hashes,
logIndex: entry.logIndex,
treeSize: proof.treeSize,
expectedRootHex: proof.rootHash,
});
if (!valid) throw new Error("inclusion failed");
}
```
## API Reference
- `verifyRfc6962InclusionProof(args): { valid, computedRootHex }` — returns
whether the computed root matches. Throws `RekorInclusionError` when
`logIndex >= treeSize` or `logIndex < 0`.
- `rfc6962LeafHash(body: Uint8Array): string` — lowercase hex SHA-256 with
the `0x00` leaf prefix.
## Implementation Notes
The carry-up branch (odd-count level) handles trees where the last node at a
given level has no sibling — rather than duplicating, RFC 6962 propagates
the node up unchanged. Rekor's server-side proof generator will either
produce a shorter path for such leaves or include a duplicate; this
implementation tolerates the former. String comparison is
case-insensitive on the expected root to match Sigstore's mixed-case hex
output.

View File

@@ -0,0 +1,90 @@
# rekor-client
## Purpose
Minimal Sigstore Rekor REST client for the two upload shapes Suaveplan
cares about: `hashedrekord` v0.0.1 (when logging a raw artifact digest
with an ed25519 signature) and `intoto` v0.0.2 (when logging a DSSE-
wrapped provenance statement). Also provides entry retrieval by uuid or
logIndex and an inclusion-proof verifier that reuses the RFC 6962 audit
path helper in the sibling module. The client is designed around an
injectable `fetch`, so unit tests can run fully offline and non-Node
runtimes can plug in their own client.
## Features
- `hashedrekord` and `intoto` upload.
- `getEntryByUuid` / `getEntryByLogIndex` retrieval.
- `verifyInclusionProof` returning `{ valid, reason? }` rather than
throwing — easier to branch on in policy pipelines.
- AbortSignal support on every HTTP call.
- Structured `RekorUploadError` on non-2xx responses, with the status,
statusText, and response body attached as context.
- Response shape normalisation: Rekor returns `{ [uuid]: entry }`; the
client flattens that to the Zod-validated `RekorEntry` shape.
## Usage: Basic Example
```ts
import { RekorClient } from "@suaveplan/slsa";
const client = new RekorClient();
const entry = await client.uploadHashedrekord({
signature: {
content: base64Signature,
publicKey: { content: base64PublicKey },
},
data: { hash: { algorithm: "sha256", value: digestHex } },
});
```
## Usage: Advanced Example
```ts
import {
createSlsaOtelAdapter,
RekorClient,
verifyDsseEnvelope,
wrapInDsse,
} from "@suaveplan/slsa";
const otel = createSlsaOtelAdapter();
const client = new RekorClient({
baseUrl: "https://rekor.example.com",
fetch: myFetch,
otel,
});
const envelope = wrapInDsse(provenanceStatement, signatures);
const entry = await client.uploadIntoto(envelope, { signal: abort.signal });
const { valid, reason } = client.verifyInclusionProof(entry);
if (!valid) throw new Error(`rekor inclusion failed: ${reason ?? "?"}`);
```
## API Reference
- `new RekorClient(options?)` — fields: `baseUrl` (default
`https://rekor.sigstore.dev`), `fetch` (default `globalThis.fetch`),
`clock`, `otel`.
- `uploadHashedrekord(body, { signal }?)` — POST; returns `RekorEntry`.
- `uploadIntoto(envelope, { signal }?)` — POST; returns `RekorEntry`.
Computes the payload SHA-256 for the intoto v0.0.2 `content.hash` field
automatically.
- `getEntryByUuid(uuid, { signal }?)` — GET.
- `getEntryByLogIndex(logIndex, { signal }?)` — GET.
- `verifyInclusionProof(entry)` — reuses `verifyRfc6962InclusionProof`.
Emits the `slsa.rekor.inclusion_verified` OTel event.
- `RekorFetch` — minimal type describing the fetch shape the client uses.
- `HashedrekordBody` — strongly-typed spec body.
## Implementation Notes
Response parsing validates with a Zod schema rather than trusting the
remote server. A malformed response therefore surfaces as a
`RekorUploadError` (not an uncaught `TypeError`) which is far easier to
alert on. The `fetch` type is intentionally narrow — we only use `method`,
`headers`, `body`, `signal`, and the `.ok / .json() / .text()` shape of
the response — so stubbing in tests requires no DOM types. HTTP errors
capture the response body as `context.body` when it's still readable, but
silently discard if the body was already consumed by the browser fetch
implementation.

View File

@@ -0,0 +1,83 @@
# sbom
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Generates CycloneDX 1.5 compatible Software Bill of Materials documents for
packages in Suaveplan. An SBOM lists every component a package
depends on, keyed by `purl`, enabling downstream vulnerability scanners,
license audits, and provenance correlation. Alongside generation, this
module exports canonical serialisation, SHA-256 digesting, and `purl`
construction so other packages (notably the publish pipeline) can
reproduce the same output bytes.
## Features
- CycloneDX 1.5 schema adherence, validated by the sibling Zod schema.
- Scoped-package `purl` construction (`pkg:npm/%40scope/name@ver`).
- Workspace/catalog placeholder resolution (`workspace:*` → `*`,
`catalog:key` → `catalog-key`).
- Deterministic sort-key serialisation via `canonicalize`.
- Injectable `clock` and `serial` factories for deterministic tests.
## Usage: Basic Example
```ts
import { generateSbom } from "@suaveplan/slsa";
const sbom = generateSbom({
packageName: "@suaveplan/utils",
packageVersion: "1.2.3",
dependencies: { lodash: "4.17.21" },
});
```
## Usage: Advanced Example
```ts
import { generateSbom, hashSbom, serializeSbom } from "@suaveplan/slsa";
const sbom = generateSbom(
{
packageName: "@suaveplan/app",
packageVersion: "2.0.0",
packageLicense: "MIT",
componentType: "application",
dependencies: {
"@suaveplan/crypto": "workspace:*",
vitest: "catalog:testing",
},
},
{
clock: () => 0,
serial: () => "urn:uuid:deadbeef-dead-beef-dead-beefdeadbeef",
}
);
console.log(hashSbom(sbom)); // stable across runs
await fs.writeFile("sbom.json", serializeSbom(sbom));
```
## API Reference
- `generateSbom(input, opts?): Sbom` — constructs the SBOM. Throws
`InvalidSbomError` when `packageName`, `packageVersion`, or `dependencies`
is missing.
- `GenerateSbomOptions { clock?, serial? }` — deterministic seam.
- `serializeSbom(sbom): string` — canonical JSON encoding.
- `hashSbom(sbom): string` — SHA-256 hex digest of the canonical encoding.
- `buildPurl(name, version): string` — constructs a `purl` for either a
scoped or unscoped npm package.
## Implementation Notes
`crypto.randomUUID` is the default serial source; the fallback construction
using `Math.random` is retained only to keep the module universally
importable — runtimes that land in the fallback should be considered a
configuration bug and should inject `serial` explicitly. `resolveVersion`
maps catalog references to a stable `catalog-<key>` form rather than the
resolved pin because catalog resolution happens at publish time, not at
SBOM generation time; the downstream publisher can post-process the
document if stricter pins are required.

View File

@@ -0,0 +1,67 @@
# parse
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Runtime-safe entry points that turn untrusted JSON or objects into strongly
typed, schema-validated SLSA artifacts. Every other module in the package
treats its inputs as already valid — these two helpers (`parseProvenance`,
`parseSbom`) are the boundary between "unvalidated wire input" and the rest of
the codebase. Both throw structured `SlsaError` subclasses with a
`context.issues` array of path-formatted Zod messages so callers can surface
precise validation feedback without parsing Zod internals.
## Features
- Accepts either a JSON string or a pre-parsed object.
- Validates against the canonical Zod schemas for in-toto/SLSA provenance and
CycloneDX 1.5 SBOMs.
- Structured error output: `err.code === "SLSA.INVALID_PROVENANCE"` /
`"SLSA.INVALID_SBOM"`.
- Captures the JSON parse error as `err.cause` when input is a malformed
string.
## Usage: Basic Example
```ts
import { parseProvenance } from "@suaveplan/slsa";
const statement = parseProvenance(wireJson);
console.log(statement.predicate.runDetails.builder.id);
```
## Usage: Advanced Example
```ts
import { parseSbom, SlsaError, SLSA_CODES } from "@suaveplan/slsa";
try {
const sbom = parseSbom(await fs.readFile("sbom.json", "utf8"));
audit.record({ serial: sbom.serialNumber, components: sbom.components.length });
} catch (err) {
if (err instanceof SlsaError && err.code === SLSA_CODES.INVALID_SBOM) {
log.warn({ issues: err.context?.issues }, "SBOM rejected");
return Response.json({ error: "invalid sbom" }, { status: 400 });
}
throw err;
}
```
## API Reference
- `parseProvenance(input: string | unknown): ProvenanceStatement` — validates
against `ProvenanceStatementSchema`. Throws `InvalidProvenanceError` on any
failure (malformed JSON, wrong `_type`, missing digests, etc.).
- `parseSbom(input: string | unknown): Sbom` — validates against `SbomSchema`.
Throws `InvalidSbomError`.
## Implementation Notes
JSON parsing and schema validation are kept distinct so that a malformed JSON
string surfaces a separate error (with the `SyntaxError` attached as `cause`)
from a structurally valid but schema-invalid document. Zod's `safeParse` is
used — we never throw a `ZodError` out of the public API. Issues are flattened
to `path: message` strings because consumers often log `context.issues`
directly and a JSON-stringified Zod issue tree is unfriendly in log pipelines.

View File

@@ -0,0 +1,76 @@
# checks
## Purpose
Individual, stateless verification rules that consume a provenance statement
plus policy inputs and return a structured `VerificationCheck` (name,
pass/fail, message). Each rule is isolated so new checks compose without
touching the verifier orchestrator, and so the test surface for each rule
stays small. The verifier module imports these and decides which to run
based on the policy shape.
## Features
- Rules: statement type, predicate type, subject present, subject digest,
builder trusted (with `allowedBuilders` intersection), source-repo match
(exact or regex), source-repo prefix, build timestamps, finishedOn
required, build duration bound, build age bound.
- All rules return a stable `name` key used by the verifier to de-duplicate.
- Strings in messages deliberately echo the offending values so logs are
self-contained for post-mortem.
## Usage: Basic Example
```ts
import { checkStatementType } from "@suaveplan/slsa/verification/verification";
const result = checkStatementType(statement);
if (!result.passed) console.warn(result.message);
```
## Usage: Advanced Example
```ts
import {
checkBuilderTrusted,
checkBuildAge,
checkSourceRepoPrefix,
} from "@suaveplan/slsa/verification/verification";
const clock = () => Date.now();
const checks = [
checkBuilderTrusted(stmt, {
trustedBuilders: ["https://gitlab.fq.io/.../runners/*"],
allowedBuilders: ["https://gitlab.fq.io/.../runners/1"],
}),
checkSourceRepoPrefix(stmt, "https://gitlab.fq.io/suaveplan/"),
checkBuildAge(stmt, 7 * 24 * 60 * 60 * 1000, clock()),
];
```
## API Reference
Each of the following returns a `VerificationCheck`:
- `checkStatementType(s)` — `_type === IN_TOTO_STATEMENT_TYPE`.
- `checkPredicateType(s)` — `predicateType === SLSA_PREDICATE_TYPE`.
- `checkSubjectPresent(s)` — `subject` is a non-empty array.
- `checkSubjectDigest(s)` — every subject has a sha256 or sha512 digest.
- `checkBuilderTrusted(s, policy)` — builder id matches `trustedBuilders`
and (if set) `allowedBuilders`.
- `checkBuildTimestamps(s)` — `startedOn` is a valid ISO 8601 string.
- `checkSourceRepo(s, expected)` — repo equals or regex-matches `expected`.
- `checkSourceRepoPrefix(s, prefix)` — repo starts with `prefix`.
- `checkFinishedOn(s)` — `finishedOn` is a valid ISO 8601 string.
- `checkBuildDuration(s, maxMs)` — `finishedOn - startedOn <= maxMs`.
- `checkBuildAge(s, maxAgeMs, now)` — `now - startedOn <= maxAgeMs`.
## Implementation Notes
`StatementLike` is deliberately a `Partial` so tests can construct tampered
objects (`{ ...statement, _type: "wrong" }`) without casting through `any`.
Pattern matching on builders supports exact strings and `*`-terminated
prefixes — glob parsing is intentionally rudimentary because builder URIs
are canonicalised by the issuer and do not need regex-level expressivity.
Time-based checks take `now` as a parameter rather than calling `Date.now()`
internally so tests can inject a fake clock.

View File

@@ -0,0 +1,74 @@
# verification
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Top-level provenance verifier. Given a `ProvenanceStatement` and a
`VerificationPolicy`, runs the fixed baseline of rules (statement type,
predicate type, subject presence / digests, builder trust, timestamps) plus
any optional rules that the policy enables (source-repo match, source-repo
prefix, finishedOn required, max build duration, max build age). Returns a
structured `VerificationResult` without throwing — callers branch on
`result.valid` and surface `result.checks` for audit logs.
## Features
- Never throws on policy failure; always returns a result.
- Injectable clock (`opts.clock`) so age-bound checks are deterministic.
- OTel `slsa.provenance.verified` span event on every call, with the result
outcome and builder id attached.
- Skips optional rules cleanly when the corresponding policy field is unset
— no "N/A" checks in the output.
## Usage: Basic Example
```ts
import { verifyProvenance } from "@suaveplan/slsa";
const result = verifyProvenance(statement, {
trustedBuilders: ["https://gitlab.fq.io/suaveplan/project/-/runners/*"],
expectedSourceRepo: "https://gitlab.fq.io/suaveplan/project",
});
if (!result.valid) {
console.error(result.summary, result.checks.filter((c) => !c.passed));
}
```
## Usage: Advanced Example
```ts
import { createSlsaOtelAdapter, verifyProvenance } from "@suaveplan/slsa";
const otel = createSlsaOtelAdapter();
const now = () => fakeClock.nowMs();
const result = verifyProvenance(
statement,
{
trustedBuilders: ["https://gitlab.fq.io/suaveplan/project/-/runners/*"],
expectedSourceRepoPrefix: "https://gitlab.fq.io/suaveplan/",
requireFinishedOn: true,
maxBuildDurationMs: 30 * 60 * 1000,
maxAgeMs: 7 * 24 * 60 * 60 * 1000,
},
{ clock: now, otel }
);
```
## API Reference
- `verifyProvenance(statement, policy, opts?): VerificationResult` —
evaluates all applicable rules. `opts.clock` defaults to `Date.now`;
`opts.otel` defaults to a no-op adapter.
- `VerifyOptions { clock?, otel? }` — both optional.
## Implementation Notes
Rule ordering is stable across releases because consumers may depend on
check names for filtering. Adding a new rule always appends to the check
list, never inserts. `maxBuildDurationMs: 0` and `maxAgeMs: 0` are treated
as "disabled" — matches the behaviour inherited from the thin legacy
implementation, and avoids an accidental zero-tolerance configuration when
a caller forgets to set a value.