Skip to content

Compliance evidence

Three commands produce documents that leave the machine and get read by somebody who did not run them: an auditor, a security reviewer, a customer's procurement team. That reader is the design constraint for all three, and it is why each one spends as much space on what it does not claim as on what it found.

Command Produces Signs / sends anything
devrepro attest An in-toto statement about an environment No
devrepro evidence Named controls mapped to scan facts, or a toolchain license inventory No
devrepro advisories Toolchain versions checked against an offline advisory set No

Nothing here contacts a network. That is the same rule as the rest of the tool, and here it also happens to be what makes the feature work in the place that needs it most: an air-gapped build host is exactly where nobody has looked at the toolchain in two years.


devrepro attest — evidence somebody else can verify

devrepro sign-snapshot already exists and signs a snapshot with an HMAC. That proves to you that your own file did not change. It is worth nothing as evidence to anybody else, because HMAC is a shared secret: verifying the signature requires holding the key that could have forged it. An auditor who could have produced the evidence has not verified it.

The missing piece was a format, not a signature. attest emits an in-toto Statement v1 — the shape cosign, Sigstore, GitHub's attestation API and the SLSA verifiers already read.

devrepro snapshot -o snap.json
devrepro attest snap.json -o statement.json

Three kinds:

  • --kind environment (default) — what was installed, what the scan found, and what score the machine earned. The score and the finding counts are carried in the predicate deliberately: they cannot be recomputed from a list of versions afterwards.
  • --kind reproduction --environment-digest <sha256> — binds an artefact to the environment attestation it came out of. Two digests, and the relationship between them is the entire claim.
  • --kind provenance — SLSA Provenance v1 for a container this tool wrote the recipe for.

Why it prints the cosign command instead of running it

Signing with Sigstore opens a browser for an OIDC flow and writes an entry to a public, permanent transparency log. That is the most irreversible action anywhere near this project, and there is no honest way to ask for "publish an immutable record of this machine's identity" from inside a diagnostic command. So attest does everything up to that step and hands you the command:

cosign attest-blob --predicate statement.json \
  --type https://devrepro.dev/attestation/environment/v1 \
  --output-attestation statement.json.att snap.json

SLSA level, stated honestly

Provenance written by the same machine that ran the build is level 1: it records what happened, not that nothing could have tampered with it. Level 2 requires a hosted build platform the developer does not control, and this document says so rather than claiming it. Level 1 is still worth emitting — it is the entry requirement for most consumers, and it is the document a hosted builder later replaces rather than invents.


devrepro evidence — controls, and the ones this cannot answer

A questionnaire asks about Annex I Part II (1). This tool emits python/version-mismatch. Both describe the same world; neither name appears in the other's document. evidence is that translation table, for the CRA, NIST SSDF (which EO 14028 points at) and SLSA.

devrepro evidence --framework cra
devrepro evidence -o evidence-pack.json

Most controls are out of scope, and that is the feature. A compliance export that reports green across a whole framework hands somebody a document that is wrong, which is worse than shipping nothing. Every control is either evidenced by something in a scan report or marked out-of-scope with a reason — and the reason travels in the exported file, not just in this page.

Four statuses:

Status Means
evidenced This report contains the facts an assessor asked for
not-evidenced The scan found nothing for this control — an empty answer, not a passing one
out-of-scope A machine scan cannot speak to it; the reason is given
partial Some of what the control asks for

evidenced does not mean the control is satisfied. That judgement needs a person who knows what the product is, and no scan can make it. The exported file says this at the top, above the table, because that is the sentence the reader has to see first.

--licenses

devrepro evidence --licenses

An inventory of the toolchain's licenses — a question every SBOM tool skips, because the compiler is in no lockfile. The short answer, which the report leads with: compiling a program with a tool does not put you under that tool's license. GCC ships a Runtime Library Exception for exactly this.

Each row carries an obligation alongside the SPDX identifier, because GPL-2.0-only next to git with no further explanation tells a legal team something false:

  • none — permissive, or an exception covers the output.
  • redistribution-only — building creates no obligation; shipping the tool itself (inside a container image, say) does.
  • review — the answer depends on which distribution you have. An OpenJDK build and an Oracle JDK are not the same license question.

A tool absent from the table is reported as unknown rather than guessed at. An inventory whose rows were inferred is not an inventory, and the reader has no way to tell which rows were which.


devrepro advisories — the toolchain nobody scans

Every scanner walks the dependency tree. Almost none check what built it. A project can have a perfectly clean npm audit and be compiled by a git old enough that cloning a repository executes code from it.

devrepro advisories

It is also part of every doctor scan, as the advisories rule pack.

It is a seed, not a feed

The bundled set is small on purpose, and every entry carries a URL you can open. A large bundled set would be a stale bundled set; the mechanism for replacing it is the actual feature.

devrepro advisories --db /media/usb/advisories.json

An external bundle must be signed. This file decides which of your tools get called dangerous, and the interesting attack on it is not adding a false entry — it is quietly removing a true one, which no amount of reading the output would reveal.

export DEVREPRO_ADVISORY_KEY="…"          # never commit it
devrepro advisories --db advisories.json   # advisories.json.sig must be beside it

--trust-unsigned accepts one without a signature, in as many words.

Bundle format:

{
  "schema_version": "1.0",
  "published": "2026-08-08",
  "covers": ["git", "openssl"],
  "advisories": [
    {
      "id": "CVE-0000-00000",
      "tool": "git",
      "summary": "One sentence.",
      "fixed": ["2.45.1", "2.44.1", "2.43.4"],
      "introduced": "2.40.0",
      "reference": "https://example.org/advisory",
      "severity": "high"
    }
  ]
}

fixed is a list because backports are how these ship

A fix released as 2.45.1, 2.44.1 and 2.43.4 means 2.44.0 is affected and 2.44.1 is not. Comparing against the highest number gets that exactly backwards, and the wrong answer looks entirely reasonable. So the installed version's own major.minor branch is looked up first, and only if that branch has no listed fix does it compare against the highest one.

The failure mode, stated: a version on a branch old enough that it never received the fix compares against the highest fix and is reported affected. That is usually right, and it is the safe direction for a warning. It is the wrong direction for a gate — so nothing built on this blocks. devrepro advisories exits 1 (READY_WITH_WARNINGS) on a match, never 2.

The comparison is also offline, so it does not know whether your distribution backported the fix into the version string you have. Many do. Check the distribution changelog before treating a match as urgent.

Silence is reported as silence

A tool the bundle has no data for produced no answer, which is not a clean one. Both the command and the advisories/coverage finding name what the set covers and when it was reviewed, whether or not anything matched, because the whole failure mode of offline advisory data is being read as coverage.


devrepro history --verify — tamper-evident history

The snapshot history directory is a pile of files. Edit one, delete another, back-date a third, and afterwards it looks exactly like a directory where none of that happened.

Each stored snapshot is recorded in an append-only chain (chain.jsonl) where every entry commits to the digest of its snapshot and to the entry before it. Editing any record changes every record after it; deleting one breaks the link; reordering breaks it too.

devrepro history --verify

What this does not do. There is no secret involved, so anybody who can write to the history directory can rebuild the whole chain from any point forward as easily as it was built. A local hash chain is tamper-evident against everyone who does not think to recompute it — which covers accidents, sync conflicts and careless cleanup scripts, and does not cover a deliberate adversary with write access.

What upgrades it is anchoring. The chain head is one short digest, and --verify prints it. Record it anywhere the directory cannot reach — a commit message, a ticket, a chat message — and everything up to that point is fixed for good.