Skip to main content

Supply chain

Release artefacts here are meant to be auditable by somebody who does not trust this repository: a wheel, its hash, its bill of materials, and a signature chaining all three back to the commit and the workflow run that produced them. This page says how that is arranged, and — the part that is easier to leave out — what each layer does not cover.

The convention is the specification's: MUST, SHOULD, MAY.

1. The chain, and the job that realises each arrow​

.github/workflows/rc-build.yml is one dispatchable workflow, three jobs, in this order. Each arrow below is a needs: edge or a step, not a description of intent:

arrowrealised by
source commit → CIqualify, checked out at the dispatched ref with fetch-depth: 0
CI → testsqualify, step The suite
tests → conformancequalify, steps Conformance Suite v2 and Evidence records
conformance → buildbuild declares needs: qualify — a red suite builds nothing
build → SBOMbuild, syft over the clean install created from the wheel that was just gated
SBOM → artefact hashbuild, one sha256sum table over dist/ and sbom/
artefact hash → attestationattest declares needs: build and re-checks SHA256SUMS first
attestation → proofattest, gh attestation verify on every artefact

The build happens once. gates/wheel_install.py --export-dist exports the artefacts that gate judged, and nothing downstream rebuilds them. The alternative — a workflow that runs python -m build and then runs the gate — produces two files that are not byte-identical, and that gate's own docstring carries the two digests that proved it. Every hash, SBOM and attestation in this workflow is over the bytes the thirteen checks passed.

Two SBOM formats, and one of the two producers is a cross-check. The release artefacts are syft's: synapse_cdm.spdx.json (SPDX 2.3, JSON) and synapse_cdm.cdx.json (CycloneDX, JSON), both taken over the clean-install environment — a venv that has never seen the source tree, holding the gated wheel and its runtime closure and nothing else — rather than over the wheel file or the runner's own environment. (Until 2026-09-16 this page and rc-build.yml said "read off the gated wheel", and syft over a wheel file catalogues one SPDX package, the filename, and zero CycloneDX components; publish.yml has taken the venv as its subject since round PS.) Beside them, cyclonedx-py environment reads the same venv and writes its document outside the artefact set — a second tool asking the interpreter what is importable, where syft catalogues the directory. The library components of the two are compared and the difference is printed, never asserted empty: syft's document also carries file and application components, which cyclonedx-py has no notion of. Four refusals then stop the build on an SBOM that describes nothing — no packages, no components, no synapse-cdm entry, or a version that is not PACKAGE_VERSION.

Nothing here publishes. rc-build.yml has no PyPI step, creates no release and moves no tag. Publishing is .github/workflows/publish.yml, which a v* tag starts.

2. Six scanners, and what each one is blind to​

Stated as a table because the failure mode is mistaking one green for another. (Five rows until 2026-09-16: the npm audit row was missing, although the job had run on every push since round PB on 2026-09-08 and both image-size exception files named it as their enforcing mitigation.)

layerwherewhat it readsblind to
Dependabot alerts + security updatesrepository settingsthe dependency graph GitHub builds from the committed manifestsanything not in a manifest; a vulnerability with no advisory yet
Dependabot version updates.github/dependabot.ymlpip in /packages/cdm, github-actions in /, npm in /docsa dependency reachable only at runtime
Dependency review.github/workflows/dependency-review.ymlwhat a pull request addsevery commit pushed directly, which is how this campaign is developed — see below
pip-auditci.yml, job supply-chainthe installed environment, and the wheel's own frozen closure in a clean venvJavaScript, which is the row below; anything outside Python packaging
npm auditci.yml, job docs-auditthe committed docs/package-lock.json, installed with npm ci, at high, with the exception allowlist derived from security/exceptions/Python; a moderate advisory, which is reported and never blocks; anything outside the docs toolchain's lock
CodeQL.github/workflows/codeql.ymlpython and javascript-typescript sources, security-extendeddependencies (that is the four rows above)

The dependency-review row is the one worth reading twice. That action compares the dependency graph of a pull request's base and head refs, so it exercises only on pull requests. The SOIF Part 1 campaign pushes directly to its branch, which means dependency review did not run on the commits that built this package. The layer that covers the dependency set as it actually stands, on every push, is pip-audit in ci.yml for the Python distribution and npm audit in the docs-audit job for the documentation toolchain (this sentence named pip-audit alone until 2026-09-16). The Python readings are two, because they answer different questions:

  • the installed environment: the package plus its test extra plus everything those drag in;
  • the wheel's frozen closure: the wheel installed alone into a clean virtual environment and pip freezed. This is what pip install synapse-cdm actually creates, and it is the smaller and more relevant of the two.

The npm reading is one: npm ci from the committed docs/package-lock.json, then npm audit --audit-level=high verbatim for the log, then a step that fails on any high or critical advisory not derived from security/exceptions/ — and, last in the same job, the site's own npm run ci, so a lock that clears an advisory and breaks the build is not green. Where an advisory sits in a package reachable only under @docusaurus/*, the repair is a floor in docs/package.json's overrides (fifteen today — six until 2026-09-17, each for a high advisory, then a seventh for the one moderate advisory this job had reported on every push and by its own rule never blocked, uuid's, since 2026-10-05 three more, brace-expansion, http-cache-semantics and joi, for four high advisories that had fixed versions, and since 2026-10-06 four more, compression, proxy-addr, source-map-js and tinypool, for five high or critical advisories the job named on the 3.2.0 release commit, each with a fixed version, and later that day a fifteenth, shell-quote, for the critical advisory the database published after that reading; ten until 2026-10-06 and fourteen until the shell-quote floor), never npm audit fix, which downgraded qs into its vulnerable range when it was tried; the dated reason for each pin is in packages/cdm/synapse_cdm/MIGRATIONS.md, and tests/test_cdm_security_policy.py holds this count and SECURITY.md's row to the block itself.

3. What blocks, and the one way past it​

HIGH and CRITICAL findings block. A CVSS security-severity of 7.0 or above fails the gate; 9.0 and above is reported as critical rather than used as a second threshold.

Precision is printed for every finding and is not part of the blocking test. A medium-precision CVSS 9.8 result that passed a gate claiming to block critical findings would be the gate's whole failure mode. A query that is genuinely imprecise is a query problem — narrow it or suppress it at the source — and an exception is deliberately not the tool for that.

Findings below 7.0, and results whose rule carries no security-severity at all (CodeQL sets that property on security queries only, and security-extended runs quality queries too), are reported and counted, never dropped. The gate prints each one.

The one way past a blocking finding is a documented, time-bounded exception:

security/exceptions/<identifier>.json

with an identifier, affected_package, version_range, risk, reason, mitigation, upstream_status, owner, expiry, created and at least one reference — the four prose fields separated by what they answer: the impact here, why the exception is granted, the compensating controls in force now, and the upstream state with the event that ends the exception before its expiry. Two properties make it a mechanism rather than a note:

  1. One source, three consumers. gates/codeql_gate.py reads the directory, and the pip-audit step and the docs-audit job both run python gates/codeql_gate.py --emit-pip-audit-ignores to derive their allowlists. No workflow carries a list, and a test asserts that none ever will — two lists diverge silently in the direction that suppresses a live finding.
  2. Expiry is enforced by the suite, unconditionally. tests/test_cdm_security_exceptions.py fails the whole suite on the day an expiry passes. No flag, no grace period, no skip. The repair is to fix the finding or to write a new exception saying what changed — never to move the date.

There is one exception today, GHSA-vfj7-8cjw-p6xm: braces's stack-exhaustion denial of service, high, in a package the documentation toolchain pulls in under @docusaurus/core and the Python distribution does not carry, with no fixed release to move to — written on 2026-10-05 on the maintainer's ruling of that date, the maintainer its owner, expiry 2026-12-04, sixty days. (Corrected 2026-10-05: until that date this paragraph opened "There are no exceptions today, and that is a reading rather than a first state"; the rest of it is kept as it stood.) Two were in force from 2026-09-08 to 2026-09-16 — GHSA-w3rx-r6r6-pgpr and GHSA-5p2g-fcmc-qvqq, image-size's ICNS and JXL/HEIF denial-of-service advisories, high, reachable only when Docusaurus measures an image at documentation build time, granted because no fixed release existed and bounded at sixty days — and both were deleted, as their own upstream_status required, when image-size 2.0.3 (published 2026-09-14) turned out to carry the fix for exactly those three parsers. docs/package.json now pins image-size at ^2.0.4 in overrides, and npm audit over the committed lock read zero high or critical advisories — and, from 2026-09-17, when uuid took the seventh floor, zero advisories at any severity. On 2026-10-05 it read eight advisories, five of them high; after the three floors of that date it reads three: braces's, excepted, a moderate one and a low one. On 2026-10-06, on the 3.2.0 release commit, it read nine, five of them high or critical and not excepted, in compression, proxy-addr, source-map-js and tinypool; after the four floors of that date it reads four: braces's, excepted, two moderate ones and a low one. pip-audit --strict reported No known vulnerabilities found on 2026-09-08, and no Python finding and no CodeQL finding has ever been excepted. (Until 2026-09-16 this paragraph still read as it had on 2026-09-08, and was false for the eight days the two files existed.)

4. The gate runs the same way on your machine​

gates/codeql_gate.py is a file in the tree rather than an expression inside a workflow, so a downloaded SARIF gets the same verdict CI gave:

gh run download <run-id> --name codeql-sarif-python
python gates/codeql_gate.py *.sarif

Exit 0 is clean, 1 is a blocking finding, 2 is a refusal — a missing or malformed SARIF, which is deliberately not the same thing as a clean one. A gate that treated an absent SARIF as an empty one would print a green for an analysis that never ran.

5. Signing: OIDC, and no key anywhere​

Attestation is actions/attest-build-provenance, which is Sigstore-backed and authenticated by the workflow run's own OIDC token. There is no signing key in this repository, in its secrets, or on anybody's disk — nothing to rotate, nothing to leak, and nothing that outlives the run that used it.

attestations: write is declared on the two attest jobs — rc-build.yml's and publish.yml's — and nowhere else; id-token: write is declared on those two and on publish.yml's publish job, which mints the PyPI Trusted Publishing token with it. (This paragraph said "the attest job of rc-build.yml and nowhere else" until 2026-09-16, which had been false since the release pipeline gained its own attestation stage.) Every other write is on the job that needs it and names its reason: contents: write on publish.yml's release and witness jobs, for the Release and its assets; security-events: write on CodeQL's analysis job, for the SARIF upload; and pull-requests: write on dependency review, for its summary comment. Everything else is contents: read.

Every uses: in every workflow is pinned to a full commit SHA with the tag it corresponded to in a trailing comment. A tag is a moving target on somebody else's account; github-actions is in Dependabot's config precisely because a SHA pin is the kind of staleness nobody notices by reading.

6. What this does not claim​

  • Not a claim about dependencies' own code. pip-audit reports known advisories. It is not an audit, and "no known vulnerabilities" is a statement about what is published.
  • Not reproducible builds. The wheel is built once and hashed; two runs of this workflow are not asserted to produce identical bytes. The attestation says this run produced these bytes from this commit, which is a weaker and checkable claim.
  • Not a claim about the runner. GitHub-hosted runners are trusted here. An attacker with control of the runner image is outside the boundary (spec/sc-oes/14-security-considerations.md is where the trust boundaries are written down).
  • No CodeQL finding has been evaluated yet. Advanced setup landed in round P6 and its first run is round P7's to record. The gate is in place and tested against synthetic SARIF; it has not yet been handed a real analysis of this repository. (Dated correction, 2026-09-16: this bullet had been false since round P7 read the first analysis on 2026-09-08 — run 34212170555, RED on four findings at or above 7.0, none in the shipped package, all four fixed at their sites by round PC with no exception written, and the next run GREEN. SECURITY.md's CodeQL row is the record, and the gate has since become a stage of the release pipeline as well.)