Supply chain#
A domain pack is a dependency that writes files into your repository. That makes it the most interesting attack surface this tool has, and the reason the guarantees below exist. This page covers what is enforced, what is opt-in, and how to run any of it on a network that cannot reach GitHub.
For reporting a vulnerability, see SECURITY.md.
What a pack can do#
csda specops add clones a pack repository at a pinned tag and renders its
templates into your project. Rendering writes files. It does not execute pack
code — there is no install hook, no lifecycle script, no plugin entry point —
but a malicious pack can still write a .feature file, an ADR, or a CI config
your pipeline then runs.
Treat installing a pack the way you treat adding a dependency: from a source you trust, at a version you pinned.
Pinning and integrity#
Every installed pack is recorded in .specops.lock with its repository,
version and a content digest: a sha256 over the pack's file tree, taken
from sorted POSIX-relative paths and file contents, so it is stable across
operating systems and clone order.
Re-installing the same (repo, pack, version) recomputes the digest and fails
loudly when it differs:
Pack integrity check failed for payments@v1.2.0:
locked digest: sha256:9f2c…
fetched digest: sha256:41ab…That is the signature of a moved tag, a rewritten history, or a poisoned cache — the three ways a version number stops meaning one thing. A version tag is a label, not a promise; the digest is the promise.
The fix is deliberately manual. Investigate the pack repository first. If the
change is legitimate, re-pin to a new version, or delete the cached copy and
re-run specops add to accept the new digest explicitly. Nothing auto-heals,
because auto-healing here means silently accepting substituted content.
Drift in the other direction#
The digest catches the pack changing under a fixed version. validate --against-lock catches the opposite: rendered files in your repository no
longer matching the locked pack.
csda validate . --against-lockcsda ci init now emits this step for all four providers, guarded on
.specops.lock existing so a project without packs is unaffected. If you
generated your gate before this existed, re-run ci init --stdout and copy the
Check pack drift step across.
Signed packs#
Signature verification uses GPG through git — git verify-tag, falling back to
git verify-commit for workflows without annotated tags. No new dependency:
organisations that sign already distribute keys through git tooling.
It is opt-in, per project:
# specops.config.yaml
require_signed_packs: trueWith that set, an unsigned or unverifiable pack is a hard error.
It is off by default, and that is deliberate. Turning it on by default would break every project installing a pack whose tags are not signed — including the ten curated packs in this repository. A default that fails on correct usage teaches people to disable the check, which is worse than not having it. Turn it on once your organisation signs its own packs; that is when the check starts distinguishing anything.
Air-gapped and offline networks#
Two independent mechanisms, for two different situations.
A machine that has the pack cached already. Packs are cached under
~/.cache/csda/packs/<sha256-of-repo>/<version>/. Setting CSDA_OFFLINE=1
makes resolution use the cache and refuse to reach the network:
CSDA_OFFLINE=1 csda specops add --pack-repo <url> --pack-version v1.2.0 --pack paymentsA cache miss is an error naming the exact directory it looked in, rather than a silent fetch. Use this on CI runners that must not egress.
A network that has never seen the pack. Export the pack repository as a git
bundle on a connected machine, carry the single file across, and install from
it — specops add accepts a bundle path anywhere it accepts a URL:
# connected side
csda pack bundle --repo https://github.com/acme/packs.git --out acme-packs.bundle
# air-gapped side
csda specops add --pack-repo ./acme-packs.bundle --pack-version v1.2.0 --pack paymentsThe bundle carries full history and tags, so version pinning, digests and signature verification all still work. Nothing about the security model is weakened by the transport.
Both paths are worth rehearsing before you need them. A restore procedure that has never been run is a hope, not a procedure.
Dependencies of this tool#
The published package has zero runtime dependencies. Everything in
package.json is a devDependency, so nothing in the dependency tree ships to
users.
That is not the same as harmless. A compromised devDependency executes on CI
with a publish token in scope, which is why npm audit, CodeQL and Dependabot
all gate the build, and why npm releases carry
provenance.
SBOM#
A CycloneDX SBOM is generated on every push, pull request and weekly schedule,
and retained for 90 days as the sbom-cyclonedx artifact — so an audit can ask
"what was in the tree that day" without reconstructing a historical dependency
graph.
npm run sbom # writes sbom.cyclonedx.json
npm run licenses # SBOM + the licence policy gateIt comes from npm sbom, not a third-party generator. Adding a dependency to
document having few dependencies would be a poor trade.
Licence policy#
scripts/license_check.ts fails the build when any component carries a licence
outside an allow-list of permissive terms. Dual licences (MIT OR CC0-1.0)
pass when either side is allowed; AND expressions require every term.
The tree today — 377 components, all permissive, no copyleft:
| Licence | Components |
|---|---|
| MIT | 288 |
| Apache-2.0 | 33 |
| ISC | 27 |
| BSD-3-Clause | 9 |
| BSD-2-Clause | 7 |
| BlueOak-1.0.0 | 5 |
| MIT OR CC0-1.0 | 3 |
| Python-2.0 · CC-BY-4.0 · CC-BY-3.0 · CC0-1.0 · 0BSD | 1 each |
The unusual four are worth naming, because "we allow CC-BY" reads alarming
without them: argparse is Python-2.0, caniuse-lite is CC-BY-4.0 over a
browser-support dataset, and spdx-exceptions / spdx-license-ids are
CC-BY-3.0 and CC0-1.0 over lists of licence identifiers. All four are data or
tooling, none imposes an obligation on distributing this CLI.
A copyleft licence is not banned so much as undecided: allowing one silently
would skip the review of what it obliges. If you need to add a licence, add it
to ALLOWED in scripts/license_check.ts and say why here — the allow-list and
its justification stay in the same change.
What this does not do#
csda validate is a specification gate, not a security scanner. It checks that
requirements have scenarios, tests and traceability rows. It will not tell you
your application has an injection flaw, and it never claims to.