Skip to content

Bureau — Blue Team (defensive)

SBOM-AI

A signed provenance ledger that lets anyone trace an AI artifact (probe-pack, model card, MCP server) back to who signed it and when.

Posture: 🔵 Blue Team (defensive)   ·   Status: alpha

What it does

A traditional Software Bill of Materials (SBOM) lists every dependency in a software release so consumers know what they're running. SBOM-AI does the same job for AI artifacts: probe-packs (the test suites Dragnet and Nuclei run), model cards (HuggingFace / OpenAI ModelCard JSON), and MCP server releases (the tarball of a Model Context Protocol server).

Every artifact gets attested as a signed in-toto Statement and published to Sigstore Rekor under a single predicate type, https://pluck.run/SbomAi.Entry/v1. Anyone in the world can ask "what is the provenance of this sha256 digest?" and walk a complete signed chain – who published it, when, what kind of artifact, with what metadata.

Think of it as the foundation that lets the rest of the Bureau exist. Without SBOM-AI, a poisoned community probe-pack could quietly compromise every operator running it. With SBOM-AI, every consumer can require trustTier === "verified" before honoring an artifact as authoritative.


Who would use it

  • A probe-pack author publishing the canonical record of "I made this pack, here's its sha256, here's my fingerprint" before pushing it to Nuclei.
  • A model-vendor compliance team publishing the canonical signed copy of a model card so external auditors don't have to trust the website copy.
  • An MCP server author tagging every release with a signed SBOM entry so downstream agents can refuse to load tampered binaries.
  • A Dragnet operator pulling SBOM-AI before running any community probe-pack – verified-tier only, please.
  • A regulator at the EU AI Office asking "give me every SBOM entry for OpenAI's models published since the Act took effect" and getting a deterministic answer.

What you'll need

  • Node.js 18+ and the Pluck CLI.
  • An operator key: pluck bureau keys generate --out ./keys --name "alice".
  • The artifact you want to attest – a probe-pack JSON, a model-card JSON, or an MCP-server tarball.
  • Internet access to reach Sigstore Rekor and --accept-public set when publishing to the public log (entries are PUBLIC and PERMANENT).

Step-by-step

1. Publish a probe-pack to SBOM-AI

Shell
pluck bureau sbom-ai publish probe-pack ./packs/canon-honesty.json \
  --keys ./keys \
  --accept-public

Output:

sbom-ai/probe-pack: published canon-honesty (digest=a1b2c3...) → rekor uuid 9f3a8b1c4d5e6f7a... (logIndex=12345)

That uuid is what you pass to Nuclei's --sbom-rekor-uuid when publishing the same pack to the community registry.

2. Publish a model card

Shell
pluck bureau sbom-ai publish model-card ./gpt-4o-card.json \
  --keys ./keys \
  --accept-public

3. Publish an MCP server

Shell
pluck bureau sbom-ai publish mcp-server ./server-v1.0.tar.gz \
  --name "my-mcp-server" \
  --keys ./keys --accept-public

The tarball's bytes are sha256-hashed; the digest becomes the canonical artifact identifier.

4. Verify someone else's published entry

Shell
pluck bureau sbom-ai verify 9f3a8b1c4d5e6f7a... \
  --fingerprint b4d5e6...

Output:

sbom-ai/verify: OK (kind=probe-pack name=canon-honesty digest=a1b2c3... signer=b4d5e6...)

The verifier walks every gate: predicate-type, fingerprint match, schema version, bounded metadata, ISO 8601, Ed25519 signature with keyid binding. Any failure returns a stable reason code.

5. Look up entries by digest

If you have a sha256 and want to know who signed it:

Shell
pluck bureau sbom-ai lookup a1b2c3... --seed 9f3a8b1c4d5e6f7a...

(The --seed is a known Rekor uuid that pre-populates the local registry; Phase 2+ wires the Kite Event Log so this becomes automatic.)

6. Index entries since a date

Shell
pluck bureau sbom-ai index --since 2026-04-01T00:00:00Z --seed 9f3a8b1c4d5e6f7a...

Run it yourself

Drop this into a Node 18+ project (npm install @sizls/pluck-bureau-sbom-ai @sizls/pluck-bureau-core tsx):

TypeScript
// index.ts
import { createHash } from "node:crypto";
import { createSbomAiSystem, computeEnvelopeHash } from "@sizls/pluck-bureau-sbom-ai";
import type { SbomEntry } from "@sizls/pluck-bureau-sbom-ai";
import { generateOperatorKey } from "@sizls/pluck-bureau-core";

async function main() {
  const operator = generateOperatorKey();
  const system = createSbomAiSystem({
    signingKey: operator.privateKeyPem,
    disablePausePoll: true,
    disableLogging: true,
  });

  try {
    // Pretend artifact: a probe-pack JSON the operator wants to attest.
    const artifactBytes = Buffer.from(JSON.stringify({ packId: "canon-honesty-v0.1", probes: [] }));
    const artifactDigest = createHash("sha256").update(artifactBytes).digest("hex");

    const entry: SbomEntry = {
      schemaVersion: 1,
      kind: "probe-pack",
      artifactDigest,
      name: "canon-honesty-v0.1",
      authorFingerprint: operator.fingerprint,
      publishedAt: new Date().toISOString(),
      metadata: { license: "MIT" },
    };

    const { statement, envelope } = system.publish(entry);
    const envelopeHash = computeEnvelopeHash(envelope);

    console.log(`sbom-ai: kind=${entry.kind} name=${entry.name}`);
    console.log(`  artifactDigest: ${entry.artifactDigest.slice(0, 16)}...`);
    console.log(`  envelopeHash:   ${envelopeHash.slice(0, 16)}...`);
    console.log(`  predicate:      ${statement.predicateType}`);
    console.log(`  signatures:     ${envelope.signatures.length}`);
    console.log(`  total entries:  ${system.facts.entries().length}`);
  } finally {
    await system.shutdown();
  }
}

main().catch((err) => { console.error(err); process.exit(1); });

Run with tsx index.ts. Expected output:

sbom-ai: kind=probe-pack name=canon-honesty-v0.1
  artifactDigest: 9d4f72ab8c3e1502...
  envelopeHash:   8c7b6a5d4e3f2109...
  predicate:      https://pluck.run/SbomAi.Entry/v1
  signatures:     1
  total entries:  1

▶ Open in StackBlitz – runs in your browser, no install required.


What you get

  • A signed Rekor entry for each artifact, immutably timestamped against Sigstore's public log.
  • A stable canonical artifact digest (sha256 of the canonical-JSON or raw bytes) every downstream consumer can verify.
  • A trust-tier systemfindByDigestVerifiedOnly() returns ONLY entries whose signer is on your trusted-author roster; findByDigest() returns both verified and TOFU-ingested. Always use the verified-only accessor for production decisions.
  • An embeddable provenance badge at studio.pluck.run/bureau/sbom-ai/badge/<sha256> for vendor pages and READMEs.

What it can't do

  • SBOM-AI does not validate the content of the artifact. A model card with hallucinated benchmarks, a probe-pack with broken assertions – both can be published. SBOM-AI's job is provenance: who signed it, when, and that the bytes haven't been tampered with since.
  • The verifier is fail-closed. Any unrecognised shape, off-by-one digest, or revoked-fingerprint hit returns { ok: false, reason: <stable code> }. That's a feature – but it means a verifier with a stale revocation cache can reject otherwise-good entries until it refreshes.
  • Today's alpha is local-only registry. Phase 2 wires the Kite Event Log so the registry is hot-cached across the host process and lookups stop needing --seed.
  • Maximum metadata size is 64 KiB canonical-JSON. Don't try to attest a model card with the full training-data manifest inline; reference it by digest instead.

A real-world example

In April 2026, an AI vendor publishes the canonical signed copy of their flagship model card to SBOM-AI. The entry's Rekor uuid goes in their press release. Within hours, an OSS maintainer embeds the badge in the readme of every wrapper library that targets that model.

Three months later, a researcher claims the vendor edited the model card on their website to remove a previously-public benchmark. The vendor denies it. The researcher pulls the original Rekor entry, runs cosign verify-attestation, and produces the canonical-JSON of the original card – with the benchmark intact. The vendor's options are to republish a corrected card under a new SBOM entry (creating a public diff trail) or to acknowledge the original. There is no third option.

That's the whole game. SBOM-AI doesn't catch lies – it just makes the receipts permanent.


For developers

Predicate URI

https://pluck.run/SbomAi.Entry/v1

One URI, three kind values inside the predicate body: probe-pack / model-card / mcp-server. Verifiers MUST inspect the predicate-type at the envelope layer first; the inner kind discriminator is content, not control.

Programs composed

  • attest – wraps the artifact body in a DSSE Statement v1 envelope.
  • notarize – posts the envelope to Rekor (default public Sigstore log).
  • dsseSign – signs the canonical-JSON body with the operator key.
  • fetchRekorEntry – pulls a published entry by uuid for verification.

See Concepts: Act → Notarisation for the underlying pipeline.

Trust tiers – ingest != trust

registry.ingest() accepts an optional authorRoster of trusted signing-key fingerprints. With a roster, an entry whose signer is on it is recorded with trustTier: "verified". Without a roster, the ingest is TOFU and the entry gets trustTier: "ingested".

Verifiers MUST check trustTier === "verified" before honoring an entry as authoritative. findByDigest returns BOTH tiers; callers that only want authoritative provenance MUST use findByDigestVerifiedOnly or filter by trustTier themselves.

TypeScript
import { createSbomRegistry } from "@sizls/pluck-bureau-sbom-ai";

const roster = new Set([
  "a1b2c3...".toLowerCase(),  // trusted publisher's fingerprint
]);

const registry = createSbomRegistry();
registry.ingest(rekorEntry, { authorRoster: roster });

// trustTier === "verified" iff signer was in roster
const trusted = registry.findByDigestVerifiedOnly(artifactDigest);

Threat model and limits

  • Ed25519 only, signed over RAW PAE bytes – cosign and sigstore-go interop.
  • schemaVersion: 1 literal – no string drift, no parser ambiguity.
  • artifactDigest validated as lowercase 32-byte sha256 hex on every entry, both at sign time AND at verify time.
  • authorFingerprint = full 64-hex SPKI sha256 (no truncation).
  • publishedAt strict ISO 8601 UTC (Z-terminated).
  • metadata capped at 64 KiB canonical-JSON.
  • name allowlist[a-z0-9][a-z0-9._\-/]* with no ...
  • Subject digest cross-checked against predicate artifactDigest.
  • verifySbomEntry is fail-closed – any unrecognised shape returns { ok: false, reason: <stable code> }.

Studio routes

  • studio.pluck.run/bureau/sbom-ai – global registry browser, filter by kind / fingerprint / date.
  • studio.pluck.run/bureau/sbom-ai/<sha256> – entry detail with cosign verify command.
  • studio.pluck.run/bureau/sbom-ai/badge/<sha256> – embeddable provenance badge for vendor pages.

Library surface

TypeScript
import {
  attestProbePack,
  verifySbomEntry,
  createSbomRegistry,
} from "@sizls/pluck-bureau-sbom-ai";

const entry = await attestProbePack({
  pack: signedPack,
  signingKey: operator.privateKeyPem,
  rekorUrl: "https://rekor.sigstore.dev",
  acceptPublic: true,
});

const result = verifySbomEntry(entry, trustedAuthorFingerprint);
// { ok: true, kind: "probe-pack", artifactDigest, publishedAt, ... }

See also

Edit this page on GitHub
Previous
Oath
Next
Rotate

Ready to build?

Install Pluck and follow the Quick Start guide to wire MCP-first data pipelines into your agents and fleets in minutes.

Get started →