Scoring methodology v2
This directory measures sscsb-control adoption — not general security. The rules below are versioned; every repo page names the version that scored it.
sscsb init before verifying —
which installs the very files many controls check for. So we snapshot the file
list first: evidence the scanner created never counts. And a
check that could not run is unverified
— nobody could answer this check — which is not the same as failing it. That is a third state: shown hatched, and left out of the
sums entirely. An unperformed check is never a verdict.The scan protocol
This directory measures sscsb-control adoption — how much of the supply-chain posture sscsb can bootstrap and verify a repository has actually committed to. It is not a general security audit. In methodology v2, a repository using an equivalent tool sscsb doesn't model scores a gap for that control — Dependabot in place of Renovate, say. Tool-equivalence mapping is a roadmap item for a future methodology version. Every version bump is recorded here and displayed on each repo's page.
- Shallow-clone the repository's default branch. The target's code is never executed.
- Snapshot the committed file list (
git ls-files). - Run
sscsb init, thensscsb verify --format jsonandsscsb report --format json. - Reclassify: any control whose passing evidence was created by init scores gap, per the class rules below.
- Delete the clone. A maintainer reviews every record before it publishes.
What the checks are for
Nine groups, drawn from the SLSA threat model, the CNCF supply-chain compromise catalog, and MITRE ATT&CK T1195. They answer "what did the attacker do". That is a different question from "where in the lifecycle", which the tool's own T1–T7 model answers.
Read the groups carefully. This lists defences the scan found, not weaknesses it found. A missing defence is not a break-in, and a full set of checks is not safety: nine groups and 54 checks do not cover everything.
Every incident below links to a primary source — a CVE record, a government alert, or the affected project's own write-up. The one exception is marked reported.
A1 Poisoned commit
Code nobody authorised or reviewed lands in the source.
Checks that defend it commit-signing, agent-signing, signing-model, branch-protection, gittuf, ai-trailers, pr-template, ai-receipts, binary-artifacts, scorecard
What this looks like when it happens
- xz / liblzma backdoor 2024-03-29 A contributor spent about two years earning maintainer trust, then hid a prebuilt object file inside a disguised test file so the build patched a backdoor into the shipped library.
- event-stream 2018-09 A new maintainer, handed the package by its worn-out original author, added a dependency whose payload only decrypted itself inside one bitcoin wallet.
SLSA A/B · CNCF source code, malicious maintainer
A2 Stolen publisher identity
The account or key that ships releases is used by someone who should not have it.
Checks that defend it commit-signing, agent-signing, signing-model, branch-protection, gittuf, sigstore-signing, provenance-verify, release-immutability, octo-sts, trusted-publishing, maintainer-mfa, publish-tokens, publish-provenance
What this looks like when it happens
- Shai-Hulud npm worm 2025-09 The worm authenticated to the registry as each developer it had compromised and republished their other packages as them.
- Ultralytics 2024-12 A second malicious release went out on an unused API token left over from before the project moved to short-lived publishing credentials.
SLSA F · CNCF phishing, trust and signing
A3 Look-alike or invented package
You install something that is not what you meant — a typo, a name collision, or a name an AI made up.
Checks that defend it ai-dep-gate, dependency-pinning, package-trust, socket-firewall, socket-firewall-ci
What this looks like when it happens
- PyTorch torchtriton 2022-12 Someone registered the same name on the public index, which takes precedence, and the resulting binary shipped the developer's SSH keys and git config off the machine.
- Package names invented by AI models 2025 Across 576,000 generated code samples, models named packages that do not exist 205,474 distinct times — every one of them a name an attacker can simply register.
SLSA D · ATT&CK T1195.001
A4 A real dependency turns hostile, or stays broken
The genuine package you depend on ships malware, or a known hole in it is left open.
Checks that defend it actions-audit, ai-dep-gate, sbom, vuln-scan, dependency-pinning, scorecard, renovate, package-trust, bumblebee, grype, socket-firewall, socket-firewall-ci, sbom-attestation, model-signing, dependency-track, guac, openvex, oras
What this looks like when it happens
- Shai-Hulud npm worm 2025-09 The first self-replicating supply-chain worm reached more than 500 packages, spreading itself through each one it reached.
- Log4Shell 2021-12-10 One bug in a logging library became an emergency for every project that shipped it.
SLSA D/H · CNCF outdated dependencies
A5 Hijacked build pipeline
The build system becomes the attacker's build machine, or hands over its credentials.
Checks that defend it secrets, actions-audit, webhooks, scorecard, renovate, socket-firewall-ci, slsa-provenance, github-attestations, provenance-verify, octo-sts, harden-runner, witness, workflow-audit-extended, wait-for-secrets, trusted-publishing
What this looks like when it happens
- tj-actions/changed-files 2025-03-15 A widely used build step's tags were repointed at malicious code that dumped every caller's secrets into logs anyone could read.
- Ultralytics 2024-12 A fork's branch name poisoned the build cache; the project's own legitimate publish job then restored that cache and built a wheel containing a crypto miner.
SLSA E · CNCF dev tooling, publishing infrastructure
A6 Compromised developer environment
The laptop, the editor extension, or the coding agent is the way in.
Checks that defend it agent-signing, signing-model, ai-trailers, ai-dep-gate, pr-template, ai-receipts, bumblebee, socket-firewall
What this looks like when it happens
- Malicious models on Hugging Face 2024-02-27 Around 100 models were built so that merely loading one opened a shell on the data scientist's own workstation.
- Shai-Hulud npm worm 2025-09 The worm installed a secret-scanning tool on the victim's own machine and used it to find the credentials it then spread with.
CNCF dev tooling · ATT&CK T1195.001
A7 Leaked credential
A key in the repository, the logs, or the build environment becomes the front door.
Checks that defend it secrets, webhooks, vuln-scan, octo-sts, harden-runner, workflow-audit-extended, wait-for-secrets, publish-tokens
What this looks like when it happens
- tj-actions/changed-files 2025-03-15 The record's own words: it let remote attackers discover secrets by reading build logs. No repository access needed.
- Shai-Hulud npm worm 2025-09 What it harvested was other people's keys — code-hosting tokens and cloud credentials from every machine it reached.
ATT&CK T1552.001 · CNCF negligence
A8 A flaw in the code you wrote
The hole is yours, and you ship it to everyone downstream.
Checks that defend it vuln-scan, sast, sighthound, codeql, fuzzing
What this looks like when it happens
- Log4Shell 2021-12-10 The same event as A4, seen from the other end: one project's own bug, everybody else's dependency crisis.
- Apache Struts remote code execution 2017-03-11 A file-upload parser mishandled its own error messages, so a crafted header ran commands on the server. The record says it was already being exploited in the wild.
CNCF negligence · the upstream half of SLSA D
A9 Untrustworthy delivery
What people download cannot be tied back to what you built.
Checks that defend it binary-artifacts, sbom, dependency-pinning, sigstore-signing, slsa-provenance, github-attestations, sbom-attestation, model-signing, provenance-verify, release-immutability, witness, guac, oras, publish-provenance, dist-manifests
What this looks like when it happens
- Ultralytics 2024-12 The second malicious release carried no build receipts at all — which is how it was spotted.
- polyfill.io 2024-06reported The domain that served a script to more than 100,000 sites changed hands, and then served something else. The code was never in anyone's repository.
SLSA F/G/H · CNCF trust and signing, publishing infrastructure
Every check, as a question
All 54, in the order they run, with the groups each one defends. A blank group means the check tells outsiders what a project does rather than stopping an attack.
| Check | The question it answers | Groups |
|---|---|---|
secrets | Are secrets blocked before they can be committed? | A7 A5 |
commit-signing | Are commits on protected branches signed by a person? | A1 A2 |
agent-signing | Do AI agents sign their commits with their own key? | A1 A2 A6 |
signing-model | Is signing set up the same way in every environment? | A1 A2 A6 |
branch-protection | Does the main branch require review before anything merges? | A1 A2 |
actions-audit | Are build steps pinned to an exact version, with narrow permissions? | A5 A4 |
gittuf | Is there a signed policy for who may change which branches? | A1 A2 |
ai-trailers | Do commits record which AI tool and model helped write them? | A1 A6 |
ai-dep-gate | Do AI commits that add dependencies get extra review? | A3 A4 A6 |
pr-template | Does the pull-request template ask what an AI generated? | A1 A6 |
ai-receipts | Is there a signed receipt linking a commit to its AI tool? | A1 A6 |
binary-artifacts | Is the source tree free of checked-in compiled programs? | A1 A9 |
webhooks | Does every webhook carry a shared secret? | A5 A7 |
sbom | Does the project publish a list of what it is made of? | A4 A9 |
vuln-scan | Is the project scanned for publicly known vulnerabilities? | A4 A7 A8 |
dependency-pinning | Are base images, lockfiles and downloads pinned to exact contents? | A4 A3 A9 |
scorecard | Does the project publish an OpenSSF Scorecard result? | A4 A1 A5 |
renovate | Are dependency updates automated and pinned to exact versions? | A4 A5 |
package-trust | Is a new dependency checked before anyone installs it? | A3 A4 |
bumblebee | Are installed tools and extensions checked against known compromises? | A6 A4 |
grype | Is that parts list itself scanned for known vulnerabilities? | A4 |
socket-firewall | Are malicious packages blocked at the moment of install? | A3 A4 A6 |
socket-firewall-ci | Does the build install its dependencies behind that block? | A3 A4 A5 |
sigstore-signing | Are released files signed so anyone can check them? | A9 A2 |
slsa-provenance | Does every build publish a receipt saying how it was made? | A9 A5 |
github-attestations | Does the build publish that receipt through GitHub itself? | A9 A5 |
sbom-attestation | Is the parts list signed and tied to the file it describes? | A9 A4 |
model-signing | Are machine-learning model files signed? | A9 A4 |
provenance-verify | Are build receipts checked before anything is published? | A9 A5 A2 |
release-immutability | Are release files attached before the release goes public? | A9 A2 |
octo-sts | Does the build use short-lived credentials instead of stored tokens? | A7 A5 A2 |
harden-runner | Is each build job watched for unexpected network traffic? | A5 A7 |
witness | Are the build steps themselves recorded and checked against a policy? | A5 A9 |
sast | Is the code checked for known-dangerous patterns before merging? | A8 |
sighthound | Is that check also run locally, before a commit lands? | A8 |
codeql | Is the code analysed in depth on every pull request? | A8 |
fuzzing | Is the code fed random input continuously to find crashes? | A8 |
workflow-audit-extended | Are risky build triggers and secret handling audited? | A5 A7 |
secure-repo | Has the repository been through a guided hardening setup? | — |
wait-for-secrets | Does releasing require a person to hand over the secret? | A7 A5 |
dependency-track | Are parts lists tracked over time, not just at build time? | A4 |
guac | Are receipts and parts lists linked into one searchable graph? | A4 A9 |
openvex | Does the project say which vulnerabilities actually apply to it? | A4 |
oras | Are parts lists and receipts stored where anyone can fetch them? | A9 A4 |
security-insights | Does the project publish a machine-readable security summary? | — |
best-practices-badge | Does the project hold an OpenSSF Best Practices badge? | — |
osps-baseline | Are the enabled checks mapped to the OSPS Baseline? | — |
compliance-map | Are the checks mapped to the frameworks an auditor asks about? | — |
publish-targets | Is it clear where this project publishes its releases? | — |
trusted-publishing | Does publishing use short-lived identity instead of a stored token? | A2 A5 |
maintainer-mfa | Is the account that can publish protected by strong two-factor? | A2 |
publish-tokens | Are publishing credentials kept out of the repository and short-lived? | A2 A7 |
publish-provenance | Can anyone check that a published package came from this project? | A2 A9 |
dist-manifests | Do the packaging files pin a checksum for what they install? | A9 |
And five checks that defend against none of them
These checks do not stop an attack. They let an outsider tell what a project already does, and where to report a problem.
These checks best-practices-badge, compliance-map, osps-baseline, publish-targets, secure-repo, security-insights
They are not weak checks. They are a different kind of thing, and saying so is more honest than inventing an attack for them. Three of the five currently move the grade; that is a scoring question, recorded here because publishing this map is what made it visible. See the formula.
Next to OpenSSF Scorecard
These are not rival tools. sscsb installs Scorecard: one of its 54 checks is whether Scorecard is running on your repository at all. Both are opt-in and both run in your own CI.
They differ in what they can see. Scorecard reads the repository and its GitHub settings, and scores each check out of ten. sscsb reads that too, plus your own machine — whether the hooks are really installed, how signing is set up, whether an agent's commits are gated. That is what the local lane exists to record.
They also differ in what they do. Scorecard rates. sscsb sets the controls up, then checks its own work, and says so when a tool is missing instead of scoring the gap as a low number you cannot tell from a real failure. Neither replaces the other.
All 20 Scorecard checks
Every check, including the 4 sscsb does not check at all. A comparison that dropped its own gaps would not be worth reading.
| Scorecard check | What it looks for | sscsb | Which checks, and how they differ |
|---|---|---|---|
| Binary-Artifacts High | Detects generated executable artifacts in source repository | Also checked | binary-artifactssscsb reads the bytes, not the name: an executable committed as logo.png is still flagged, and only tracked files count. |
| Branch-Protection High | Ensures default and release branches use protection settings or repository rules | Also checked | branch-protectionsscsb also verifies that protection actually blocks an unsigned commit, not only that a setting is on. |
| CI-Tests Low | Determines if project runs tests before pull requests merge | Not checked | — Out of scope by design: sscsb checks supply-chain posture, not whether a project tests itself. |
| CII-Best-Practices Low | Checks for OpenSSF Best Practices Badge at passing, silver, or gold level | Partly | best-practices-badgesscsb records that the self-assessment exists; earning the badge is an owner action it cannot perform. |
| Code-Review High | Verifies project requires human code review before merging pull requests | Partly | branch-protection pr-templateStructurally capped for a solo maintainer: Scorecard counts approved changesets, and one person merging their own pull requests scores zero however the branch is protected. |
| Contributors Low | Assesses if project has recent contributors from multiple organizations | Not checked | — Measures project social structure, which sscsb makes no claim about. |
| Dangerous-Workflow Critical | Identifies dangerous patterns in GitHub Action workflows | Also checked | workflow-audit-extended actions-auditsscsb parses committed workflows per job and reads shell bodies, so a signing step whose failure is swallowed does not count as present. |
| Dependency-Update-Tool High | Checks if project uses automated dependency update tools | Also checked | renovate |
| Fuzzing Medium | Determines if project uses fuzzing or property-based testing | Also checked | fuzzingOff by default in sscsb; enabling it is a deliberate choice, and off means the code does not run. |
| License Low | Verifies project has published a software license | Not checked | — Licensing is a legal property, not a supply-chain control. |
| Maintained High | Assesses whether project is actively maintained | Not checked | — Scores zero for any repository under 90 days old, which no action can change. sscsb makes no maintenance claim. |
| Packaging Medium | Checks if project is published as a downloadable package | Partly | release-immutabilitysscsb cares whether a published release can be altered after the fact, not whether one exists. |
| Pinned-Dependencies Medium | Verifies dependencies are pinned to specific versions or hashes | Also checked | actions-audit dependency-pinning renovatesscsb SHA-pins every action except the SLSA generator, which must stay tag-pinned because slsa-verifier validates the trusted builder's ref and rejects a digest. dependency-pinning covers what Scorecard's ecosystem list stops at: base-image digests, manifests without a lockfile, and downloads run before they are verified. |
| SAST Medium | Determines if project uses static application security testing | Also checked | sast codeqlsscsb runs SAST at pre-commit as well as in CI, so a finding is caught before it is ever pushed. |
| SBOM Medium | Checks for Software Bill of Materials in source or release artifacts | Also checked | sbom sbom-attestationsscsb additionally binds the SBOM to the artifact by attestation, so it cannot be swapped after the build. |
| Security-Policy Medium | Verifies project has published a security vulnerability reporting policy | Also checked | security-insights |
| Signed-Releases High | Checks if project cryptographically signs release artifacts | Also checked | sigstore-signing slsa-provenance provenance-verify github-attestationsOverlap is thinner than the names suggest. Scorecard scores this by FILENAME — it looks for a *.sig or *.intoto.jsonl beside the last five releases and its own docs say it does not verify them. sscsb runs cosign verify-blob and slsa-verifier against the release workflow's certificate identity before publishing, and fails the release closed. An empty file renamed to release.tar.gz.sig scores 8 there and fails here. |
| Token-Permissions High | Ensures automated workflow tokens follow least privilege principle | Also checked | actions-audit octo-sts harden-runnersscsb also replaces long-lived tokens with short-lived federated credentials rather than only narrowing scopes. |
| Vulnerabilities High | Identifies open, unfixed vulnerabilities in codebase or dependencies | Also checked | vuln-scan grype openvex dependency-tracksscsb can record an assessed VEX judgement, so a vulnerability that genuinely does not apply is suppressed visibly rather than silently. |
| Webhooks Critical | Verifies repository webhooks have token authentication configured | Also checked | webhooksScorecard's own Webhooks check is experimental and never runs for a default install; sscsb reads each hook's secret and TLS setting with any token that holds read:repo_hook, and says unverified when it cannot. |
What sscsb checks that Scorecard cannot
Scorecard reads the repository and its GitHub settings. Everything below happens somewhere else — on a maintainer's machine, before a commit exists, or inside a release pipeline — so no amount of reading the repository will show it.
| What | Why it matters | Checks |
|---|---|---|
| An AI agent cannot land a commit | Signing identities are classified, and only a human-class key satisfies the protected-branch policy. An agent can draft anything; it cannot land it. | commit-signing agent-signing signing-model |
| AI-authored changes are declared and gated | Commits record which model and tool produced them, and an AI commit that adds a dependency or a shell command hits an extra gate. | ai-trailers ai-dep-gate ai-receipts |
| Secrets are blocked before they exist | Two scanners run at pre-commit and pre-push, so a credential never reaches the remote in the first place. Scorecard can only observe what was already pushed. | secrets |
| A new dependency needs approval | Checks that a package exists, is not a look-alike of a popular name, and was approved by a human before it entered the tree. | package-trust |
| Build provenance is verified, not just produced | The release is gated on verifying its own provenance against a pinned trusted builder before anything is published. | provenance-verify slsa-provenance octo-sts |
| Releases cannot be altered after publication | Assets and tags are frozen once a release is published. | release-immutability |
| Ref history itself is protected | Signed, forge-independent policy over who may change which refs — protection that survives the forge being wrong. | gittuf |
| Every control maps to a framework | Each control is mapped to SLSA, SSDF, the CRA and the OpenSSF Baseline, so the posture can be read as compliance evidence. | compliance-map osps-baseline security-insights |
| A missing tool degrades loudly | If a scanner is not installed, sscsb says so and reports DEGRADED. It never scores the absence of a check as a pass, and never as a low number you cannot tell apart from a real failure. | — |
Scorecard check names, risk levels and descriptions are taken from the project's own published check list. sscsb check names are the control ids this site already uses on every repository page.
Evidence classes
Not every check can be answered from the same place. These five groups say who could see what, and each repository page names the group beside every check.
| Class | Controls | Rule |
|---|---|---|
| A — committed artifacts | secrets, gittuf, pr-template, binary-artifacts, sbom, vuln-scan, dependency-pinning, renovate, sigstore-signing, slsa-provenance, github-attestations, sbom-attestation, model-signing, provenance-verify, release-immutability, octo-sts, sast, codeql, fuzzing, wait-for-secrets, dependency-track, security-insights, best-practices-badge, osps-baseline, publish-targets, dist-manifests | The control's evidence is files committed to the repository. The scanner snapshots that file list before it runs sscsb init. A registered artifact missing from that snapshot was installed by the scanner itself, so the control scores gap. Evidence the scanner installed seconds earlier is never the repository's evidence. A pre-existing artifact that fails sscsb's shape checks is a real fail. Five controls depend on tools the scanning machine may lack: secrets, sbom, vuln-scan, sast and provenance-verify. When the tool was absent, the committed artifacts decide instead. When the tool ran and could not verify, nothing decides, and the row is unverified. |
| A′ — static audits of committed workflows | actions-audit, socket-firewall-ci, harden-runner, workflow-audit-extended, trusted-publishing, publish-tokens | actions-audit, workflow-audit-extended, harden-runner and socket-firewall-ci parse every workflow file. With zero pre-existing workflows the verdict would be vacuous, so it scores unverified. Otherwise the raw verdict maps directly. sscsb's own installed templates pass its audit by construction, so init can only push a verdict toward pass. A fail therefore always implicates the repository's own workflows. |
| B — live remote checks | branch-protection, webhooks, scorecard, publish-provenance | branch-protection, webhooks and Scorecard query GitHub itself. Init cannot influence them, so raw verdicts map directly. Scorecard's live alert feed and a repository's webhook settings need permissions a cross-repo scan does not have. Those rows are recorded as unverified, never guessed. |
| C — local environment | commit-signing, agent-signing, signing-model, ai-trailers, ai-dep-gate, ai-receipts, package-trust, bumblebee, grype, socket-firewall, witness, sighthound, guac, openvex, oras, maintainer-mfa | Commit signing, signing-model posture, AI trailers and package-trust hooks describe the development machine. No repository scan can observe that machine — not from inside CI, not from outside. A repository scan therefore records these as unverified. An unperformed check is a third state, never a pass or a fail. Class C is the one class a signed local record can settle by itself. There, the maintainer's signed word is the best evidence that can exist. It is not the only class a local record may resolve. A local record votes on every control it holds. Its verdict on a class A, A′ or B row becomes countable as soon as an independent record agrees with it. If one disagrees, the row scores a gap. See “the local lane” below. |
| M — meta / informational | secure-repo, compliance-map | compliance-map (about sscsb itself) and secure-repo (an external service pointer) are excluded from scoring entirely. |
Scope
A control is in scope when it is enabled by sscsb's defaults or by the
repository's own committed .sscsb/config.toml. Disabling a default-on
control scores a gap — the denominator cannot be shrunk. Enabling an optional
control puts it in scope against real evidence — never free points.
countable = pass + fail + gap
phase % = 100 · pass / countable
overall = Σ pass / Σ countable
coverage = Σ countable / |scope|
Read it in words: answered checks — the checks that produced a pass, a fail, or a missing-defence result. A gap — the defence was looked for and not found. Checks nobody could answer are never in any sum, and a phase where nothing was answered reads "no evidence" — not 0%.
A+ = exactly 100% · A ≥ 90 · B ≥ 80 · C ≥ 70 · D ≥ 60 · F below. Then evidence coverage — how many of the checks produced a yes-or-no answer at all: under 50% there is no letter at all (NA). Under 75% the letter is provisional — the grade stands, but too much went unchecked to treat it as settled.
| Grade | Overall |
|---|---|
| A+ | exactly 100% |
| A | ≥ 90%, < 100% |
| B | ≥ 80%, < 90% |
| C | ≥ 70%, < 80% |
| D | ≥ 60%, < 70% |
| F | < 60% |
Evidence coverage below 50% yields NA — insufficient evidence for any letter. Coverage between 50% and 75% marks the letter provisional. This inherits sscsb's own doctrine: exit code 0 is not a clean bill of health, and an unperformed check is never converted into a verdict.
Authenticated records: the trust chain
Three things can produce a record, and they see different amounts. That is the evidence source — who ran the scan, and therefore how much of the project they could see, shown as a badge on every listing. It decides nothing about the score. It tells you how far the scanner could see. Where a build leaves a signed receipt for what it produced, that receipt is an attestation — a signed receipt saying which build produced which file.
The checks that matter most — branch protection, Actions token permissions, security-feature enablement — are repository settings, readable only with repository credentials. The only place a complete scan can run is the repository's own CI, which raises the obvious question: when a repository hands in its own report card, why believe it? The answer is OpenSSF Scorecard's, adopted deliberately.
- Verified scanner. The action installs an sscsb release only after checking its Sigstore bundle. The bundle is checked against the tool repository's own release-workflow identity, at that exact tag. The scanner proves its own provenance before assessing anyone else's.
- Signed record. With
id-token: write, the action signsscan-record.jsonusing keyless signing — signing with a short-lived certificate tied to the build, so there is no long-lived key to steal. Fulcio issues a short-lived certificate whose identity is the producing workflow: repository, workflow path, and branch ref. GitHub's OIDC issuer burns those in; the record does not assert them. The signature is then logged to Rekor. - Pinned verification. Ingest runs
cosign verify-blobpinned tohttps://github.com/OWNER/REPO/.github/workflows/sscsb-scan.yml@refs/heads/<default branch>— the default branch fetched live from GitHub, never read from the record — and to the commit the record claims. A third party cannot mint that identity. A feature-branch or renamed-workflow signature does not verify. A record whose bundle fails verification is rejected outright. And the directory publishes the bundle beside every verified record, so anyone can re-run the check. - Human gate, still. Verified or not, nothing lists without a
maintainer's
publishlabel. An action-lane record that arrives unsigned is listed only as an unverified claim.
What this deliberately does not prove: that the workflow which
signed the record was unmodified. A maintainer who edits their own
sscsb-scan.yml can sign whatever it emits. OpenSSF Scorecard closes
that gap. It fetches the producing workflow at the certificate's commit and
rule-checks it: allow-listed steps only, no environment redirection, hosted runners.
That is this directory's ingestion-hardening milestone. Until then, the human publish
gate is the backstop.
Local records: the third lane
About a dozen checks describe a developer's own machine, where no scan can look. A maintainer answers them by running the scan there and signing the result. The key they sign with is one the repository already publishes, in its signer list — the file a project commits naming the keys it accepts scans from.
Ten or eleven controls per repository are class C: they describe the development environment — commit signing configuration, AI-assistant trailers, dependency gates, package-trust hooks. No repository scan can observe them, from inside CI or out, so they score unverified and sit outside every denominator. For a repository that has actually adopted them, that is a permanent ceiling on evidence coverage. It is why a well-run repository can still read provisional.
The local lane closes exactly that gap. A maintainer runs
sscsb scan --local --submit
which scans the working tree, writes a record, and signs it with the maintainer's
git signing key. That is the same user.signingkey /
gpg.format=ssh / gpg.ssh.program configuration git itself
uses, so a 1Password- or hardware-backed key works untouched.
The contract
The tool and this directory are two programs that have to agree exactly — down to
a namespace string and a file path. Otherwise every submission fails in a way neither
side can see. So the agreement is written down once, in the tool's
docs/local-scan.md, and mirrored here byte for byte. Both trees test
their own copy against these values, and both pin the same digest over the block.
An edit on one side that the other does not mirror fails a test, instead of shipping
a lane that does not work.
sscsb local-lane contract v1
command sscsb scan --local --submit
sshsig-namespace sscsb-scan-record
record-path .sscsb/scan-record.local.json
signature-path .sscsb/scan-record.local.json.sig
anchor-path .sscsb/policy/allowed_signers
anchor-namespaces git,sscsb-scan-record
signed-bytes the bytes of .sscsb/scan-record.local.json, verbatim
record-shape ScanRecord
schema-version 1
methodology-version 2
record-fields schema_version methodology_version repo scanned_at scanner request_issue controls score
repo-fields owner name url default_branch commit description
control-fields id phase in_scope raw_outcome scan_outcome reclassified reason messages
score-fields grade provisional overall_percent evidence_coverage_percent phases
submission-label local-scan-result
Two lines carry most of the weight. record-path and
signature-path are committed paths. The submission is a
pointer: the directory reads the record, the signature and the trust anchor
out of the public repository. Nothing a submitter types reaches the bytes that get
verified. And record-shape is ScanRecord — the
directory's own schema, the same one every other lane produces, with every required
field including methodology_version. A signature makes bytes
unreshapeable, so the shape has to be right at signing time.
The trust anchor
A local scan has no OIDC identity: there is no CI run to issue a certificate
against. What it does have is a file the repository already commits.
.sscsb/policy/allowed_signers — generated from
.sscsb/policy/signers.toml — is the anchor the commit-signing control
itself uses. So ingest verifies the record with
ssh-keygen -Y verify -f allowed_signers \
-I "<signer>" -n sscsb-scan-record \
-s .sscsb/scan-record.local.json.sig < .sscsb/scan-record.local.json
against an allowed_signers fetched from the public repository
at the scanned commit. That is committed content, read through the GitHub
contents API at that ref, never anything the submitter hands us. A record whose
signature does not verify is refused outright — exactly as a Sigstore bundle that
fails cosign verify-blob is refused.
Only a human-class approved signer may assert a local record
sscsb grants the sscsb-scan-record namespace to
class = "human" signers only. A ci or
ai entry in .sscsb/policy/signers.toml keeps
namespaces="git" and nothing else, so its commits still verify and its
scan records cannot.
Three reasons, none of them tidiness. A local record is a person's attested word about a machine nobody can inspect. It is the one lane whose local-environment verdicts count with no corroboration. What makes that acceptable is that a named human put their key behind it. CI does not need the grant: it has the authenticated lane, which proves strictly more. And an AI-class signer asserting one would contradict the invariant the signing policy is built on: an agent may draft anything and signs nothing.
The refusal is structural, not advisory. Because the namespace is
simply absent from the anchor line, the ssh-keygen -Y verify above fails:
the tool refuses to produce the record, and this directory refuses to ingest one,
without either of them re-implementing the rule.
The same fact has a consequence. A repository anchored before this lane existed cannot submit yet. A listing in that state is told to regenerate and commit its anchor first, rather than promised a one-line fix that would refuse.
What this proves — and what it does not
Proves: a holder of a key this repository commits as an approved signer asserts this result at commit X. That is attributable, auditable, and re-checkable by anyone: the directory publishes the local record and its signature beside the listing, and the command above is the whole verification.
Does not prove: that the scan ran on a clean machine, that the tools were the ones claimed, or that the working tree matched the commit. Nobody can check a workstation. This is weaker than the authenticated lane, which proves the repository's own CI produced the record under an identity GitHub's OIDC issuer burned in. It never earns the ✓ verified mark.
The scoring rule
A repository's grade takes account of its GitHub Actions-emitted results and its local scan-emitted results. These are not mutually exclusive lanes, and neither one is a second-class citizen. For each control the directory collects a verdict from every evidence source it holds. There are three of them: the newest action-lane record whose signature verified, the newest local-lane record whose signature verified, and the external record the directory produced itself. Then:
- Two or more sources give different countable verdicts (pass, fail or gap) → the control scores gap. It also carries a contradiction flag naming each source and the verdict it gave. The flag appears on the record, on the listing row and on the detail page. Scoring a disagreement down while saying nothing about it would hide the most interesting fact the directory holds about that repository.
- Exactly one distinct countable verdict across sources → that
verdict, whichever lane produced it. A local
passcounts; so does a localfail. - No countable verdict →
unverifiedorinfo, outside every denominator, exactly as before.
A contradiction therefore costs the repository — a gap sits in the denominator without passing. That is deliberate. Erring on the side of caution removes any incentive to submit a flattering local scan: the flattering answer never wins, it only ever converts a row into a gap.
…and where a lane could not have looked, its verdict is not evidence
The same principle runs backwards. A repository-observable lane cannot observe class C: the development environment is not in the checkout. So a class-C verdict carried by an action-lane or external record is dropped before anything is counted, whatever it says. It is not countable evidence. It does not satisfy the independence requirement above for other rows. And it cannot contradict a genuine local verdict into a gap. A verdict a source could not have made is not a verdict.
The record's own score is the submitter's, not ours
A local record is a complete, self-describing ScanRecord, so it carries a
top-level score block: a grade, a percentage, a coverage figure. Those were
computed on the maintainer's machine, over the controls that machine had in scope. This
directory republishes the record byte for byte. The signature covers
those exact bytes, and rewriting them would destroy the only thing that makes the record
evidence.
So two scores can be reached from one listing, and only one of them is ours. The grade
on the listing and on the detail page is the directory's, computed here
from every evidence source under the published methodology. The number inside
scan-record.local.json is the submitter's
self-report. Every listing carrying a local record says which is which, in both
places, and names both numbers. Quoting the embedded grade as a directory grade should
take work, not inattention.
Where someone else could have checked, we require that someone else
One requirement makes that union safe, and it is not "local counts less".
Classes A, A′ and B are by definition observable from a repository
scan — a committed artifact, a committed workflow, a live GitHub setting. For those
rows a maintainer's self-report alone is not countable. With no
independent source the control stays unverified, outside the
denominator. It becomes countable the moment a CI or external record exists to agree
or disagree with it.
Class C is by definition not independently observable — it lives on the workstation and nowhere else. There the maintainer's signed word is the best evidence that can exist, and it counts on its own.
The practical consequence, and the reason the requirement exists: a repository
whose only evidence is a local record publishes with its class-C
rows scored and everything else unverified. Its evidence coverage is
low, so it reads NA — insufficient evidence, never A+. A local record lifts
a real score only alongside a scan somebody else could run. The scope such a listing
is measured against is the directory's control set, not the
record's: a record cannot shrink its own denominator by declining to mention a
control.
Gates
A local submission passes through everything an authenticated one does. The
record must be a valid v1 ScanRecord for the repository the request
names. The signature must verify against the repository's own committed
allowed_signers. And a maintainer must still apply the
publish label. The authenticated lane's auto-publish gate is not
available to it. A listing that used the local lane says so, on the listing and on
the detail page. It names the controls the local record actually resolved. It says so
again when that record describes a different commit than the
repository scan it sits beside. A local record has no expiry. A stale one quietly
filling holes in a much newer scan is something a reader has to be told about.
Coverage below 75% still reads provisional after a local scan if the remaining holes are elsewhere. Where that is the case the listing says so rather than promising a fix the command cannot deliver.
Changelog
- v1 — initial methodology: diff-based init reclassification, five evidence classes, and an academic grade scale with A+ reserved for exactly 100%.
- v1, 2026-09 — authenticated records are signed and verified against the producing workflow's identity. Scoring is unchanged: provenance is displayed, not scored.
- v1, 2026-09 — the local lane. A maintainer-signed workstation record, verified against the repository's own committed
allowed_signers, joins the action and external records as an evidence source. Verdicts are merged per control. Sources that disagree score a gap with a named contradiction. A local assertion about a control a repository scan could observe is not counted until an independent record agrees with it. Class rules, the formula and the grade scale are unchanged. - v2, 2026-09 — three controls join the registry: binary-artifacts, webhooks and dependency-pinning. All three are on by default, so every score has more checks in it. The sbom and vuln-scan gates now run their tools instead of checking that a tool exists. A scanner that ran and could not verify is recorded as unverified; only an absent tool lets the committed artifacts stand in. A record made under v1 says so until its next scan.