Why Zuke's security requirements are met: the threat model, the trust
boundaries, and the argument that secure design principles are applied and
common implementation weaknesses countered. SECURITY.md
states the policy (how to report, how releases are verified); this document
justifies it. Both are maintained together — a change that moves a trust
boundary updates this file in the same pull request.
Zuke is a build-automation framework: it runs inside other projects' CI pipelines with their credentials in scope, and it publishes 50+ packages to a public registry. From that, its security requirements are:
- What consumers install is what this repository contains. No tampering between the merged source and the published package (supply-chain integrity).
- Zuke must not become an injection vector. Values a build interpolates into commands — parameters, environment values, file contents — must never be reinterpreted as shell syntax or extra arguments.
- The project's own pipeline must contain a compromise. A malicious pull request, a compromised third-party action, or a leaked token must be limited in what it can read, write, or exfiltrate.
- Secrets must not leak — not into the repository history, not into logs, not to unintended network destinations.
Adversaries considered, and the assets they target:
| Adversary | Target | Primary counter |
|---|---|---|
| Malicious contributor (fork PR) | CI secrets, write token | Fork PRs run with a read-only token and no secrets; pull_request (never pull_request_target) triggers. The one job that runs with secrets for a fork PR — the AI review a maintainer's @zuke-build review comment starts — runs master's code and fetches the PR as data, never executing it |
| Compromised third-party action | CI secrets, published artifacts | Every action pinned to a full commit SHA; Dependabot bumps pins; zizmor audits workflows |
| Compromised build-time dependency | Release credentials, egress | --frozen lockfile; zero runtime dependencies; egress blocked to an allowlist on every job holding a write-scoped token |
| Registry-level attacker | Consumers of @zuke/* |
OIDC trusted publishing; Sigstore provenance per version; no long-lived registry tokens exist |
| Network attacker (MITM) on bootstrap | Developer/CI machines | Launcher downloads the pinned Deno release over HTTPS and verifies a per-platform SHA-256 baked into the launcher; DENO_VERSION=latest is refused |
| Careless or compromised insider | Repository history | gitleaks scans full history on schedule and pushes; PR-scoped scans on every pull request; secret parameters are redacted from output |
| Contributor with write access (same-repo PR) | CI secrets, write token | Not defended against, by design: the lint fixer pushes fixes and Codecov uploads from pull-request runs, so same-repo PRs carry OPENAI_API_KEY, CODECOV_TOKEN and a write-scoped token, and the AI review job carries the zuke-build App's credentials so the token it mints can resolve the review threads it answers, which the Actions token is refused. Write access is held only by maintainers, whose own secrets these are, and every repository secret is readable from any branch a maintainer pushes regardless of which job names it; the residual is a compromised maintainer account yielding an OpenAI key, an upload-only Codecov token, and the App key, whose blast radius egress blocking on that job bounds to GitHub and the model provider |
Out of scope: vulnerabilities in GitHub, JSR, or Deno themselves (they are the trusted computing base — see the boundaries below), and denial of service against public CI.
- Pull request → gate. Code in a PR is untrusted until the required CI gate and review pass. Fork PRs cross this boundary with a read-only token and no secrets. The PR body is also untrusted: it reaches the build only through an environment variable, never interpolated into a shell line.
- Build layer → published packages. The build layer (
build/,zuke.ts) may use dev dependencies; the published packages underpackages/may not depend on anything third-party. The boundary is enforced by each package'sdeno.jsondeclaring no dependencies, so nothing the build layer trusts is shipped to consumers. - CI jobs → each other. Job token scopes are disjoint by design: the release job (repo write) never holds the JSR OIDC credential, and the publish job (OIDC) never holds a write-scoped repo token. A compromise of one job does not yield the other's authority.
- CI jobs → network. Every job that holds a write-scoped token runs with egress blocked to a named allowlist. That bounds which third parties a job can reach; it does not stop a secret leaving. GitHub is on the allowlist by necessity — the lint fixer pushes and comments through it — and a public repository's job log is world-readable, so masking is defeated by any encoding. The counter this boundary provides is against a compromised build-time dependency phoning home, not against code that is already trusted with the token.
- Repository → third-party code. Actions cross the boundary only at pinned
commit SHAs; scanner binaries only with verified checksums
(
build/scanners.ts); modules only through the committed, frozendeno.lock. - Trusted computing base. GitHub (repository, Actions, OIDC issuer), JSR (registry, provenance), and the pinned Deno runtime are trusted. Attacks on them are acknowledged as out of scope above.
- Least privilege. The default workflow token is
contents: read; each job opts into exactly the scopes it needs (see boundary 3). The one deliberate elevation — the lint fixer'scontents: writeon thecijob — is documented as a trade-off inSECURITY.md, and is inert for fork PRs. - Fail-safe defaults. The lockfile is enforced with
--frozen, so a missing resolution fails the run instead of being healed silently. The launcher refuses a Deno version it cannot verify against a pinned checksum. The coverage gate fails the build below threshold rather than warning. - Complete mediation. One gate (
./zuke ci) stands in front of every merge — the same gate locally and in CI, so there is no unchecked path tomaster; releases are cut only frommaster. - Economy of mechanism. Zero runtime dependencies and a single toolchain (Deno) keep the attack surface enumerable: what ships is only this repository's source.
- Open design. The build, the workflows (generated from
build/workflows.ts), the scanners, and this assurance case are all public; nothing relies on secrecy. - Separation of privilege. Publishing requires the release pipeline's OIDC identity — a stolen personal token cannot produce a provenance-valid release, and provenance names the workflow and commit for anyone to check.
- Command injection (the classic build-tool weakness): the
$shell tokenises interpolated values into discrete argv entries and all process execution goes throughDeno.Command— there is no shell-string concatenation anywhere to inject into. - Type confusion / unsafe casts: strict TypeScript with
any, forcedascasts, and non-null!assertions banned by policy and lint; runtime guards are exercised by tests. - A guard hardened in one copy and not the other: a security check whose
value is being applied everywhere must have exactly one implementation, so
fixing it fixes every call site. Duplicated logic is a review-blocking defect
rather than a style note, because copies drift: a second zip-slip guard, a
Markdown escaper missing a sibling's newline collapse, two loopback checks
disagreeing about
[::1], and aJSON.parseguard present in one of two twin stores were all found and consolidated this way. Policy inAGENTS.md; both AI reviewers read it from the diff base and flag retyped helpers. - Regression and logic errors: a 95% line-and-branch coverage gate, three test layers (unit, in-process integration, cross-process e2e on three OSes), and an adversarial review pass on every feature.
- Vulnerable patterns in new code: CodeQL's security suite analyzes every
pull request and push (including the
actionspack over the workflow YAML); zizmor and actionlint audit the workflows in the required gate. - Secret leakage: gitleaks in the gate and on schedule; secret parameters
are redacted from build output; no credentials are persisted into
.git/configin release jobs. - Dependency confusion / typosquatting: consumers install from the
@zukeJSR scope with per-version provenance; the npm-side@zuke-buildorg is reserved to prevent squatting.
The known, accepted trade-offs — the dirty-tree publish backstop, the lint
fixer's write scope, and the bootstrap-download design — are documented with
their mitigations in
SECURITY.md § Known trade-offs. The
project's largest non-technical risk, a bus factor of one, is tracked in
GOVERNANCE.md and ROADMAP.md.