Repository navigation
chore: add security release gates - #113
Conversation
There was a problem hiding this comment.
Pull request overview
Adds CI/release security gating around production dependency advisories and workflow hardening, aligning the repository’s “clean production deps” claims with enforceable checks and release artifacts.
Changes:
- Adds an isolated “production-only” npm audit root under
.audit/and runsnpm audit --omit=dev --audit-level=moderatein CI and publish. - Generates a CycloneDX SBOM during release, uploads it with the tarball, and verifies both SHA-256 values before publish.
- Introduces a pinned zizmor workflow scan and tightens Dependabot grouping/cooldowns to reduce update noise while keeping majors separate.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/supply-chain-policy.unit.test.ts | Extends supply-chain policy tests to cover the new production audit root and enforce pinned marketplace actions across all workflows. |
| tests/contract/security-remediation.contract.test.ts | Adds contract assertions for Dependabot cooldown/grouping rules (majors remain ungrouped). |
| tests/contract/ci-workflow-policy.contract.test.ts | Updates workflow allowlist and asserts the new pinned zizmor-based security workflow policy. |
| scripts/prepare-production-npm-audit.mjs | New script to generate an isolated npm production manifest used for advisory gating and SBOM generation. |
| RELEASE.md | Documents the new production audit and SBOM steps as part of the release checklist. |
| docs/TESTING.md | Explains why production audit is separate from pnpm lock audit and how CI prepares the isolated audit root. |
| docs/SUPPLY_CHAIN_SECURITY.md | Documents the production audit commands and release SBOM verification behavior. |
| docs/SDK_ADOPTION_CONTRACT.md | Tightens SDK bump requirements to include the full no-credential contract gates. |
| CONTRIBUTING.md | Updates contributor guidance to include the production npm audit gate and SDK bump requirements. |
| CHANGELOG.md | Notes the addition of production audit gating, SBOM generation, zizmor scanning, and Dependabot changes. |
| .gitignore | Ignores the generated .audit/ workspace. |
| .github/workflows/test.yml | Adds production audit-root preparation and npm audit gating to CI audit job. |
| .github/workflows/smoke.yml | Pins core GitHub Actions refs (checkout/setup-node) to immutable SHAs. |
| .github/workflows/security.yml | New workflow running pinned zizmor workflow analysis with minimal permissions. |
| .github/workflows/publish.yml | Adds production npm audit, SBOM generation/upload, and SBOM SHA verification to release pipeline. |
| .github/workflows/contract.yml | Pins additional GitHub Actions refs to immutable SHAs. |
| .github/dependabot.yml | Adds cooldown settings and grouping update-types rules to keep majors separate and reduce PR churn. |
| .cspell/project-words.txt | Adds “CycloneDX” and zizmor-related words to spellcheck allowlist. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
scripts/production-security-gate.mjs:220
writeCycloneDxSbom()uses${name}@${version}as the CycloneDXbom-reffor every lockfile entry. The converted npm-style lock can contain multiplepackagesentries with the same name+version (e.g. peer-context duplicates under.pnpm/...), which will produce duplicatebom-refvalues and an invalid/ambiguous SBOM. Deduplicate components byname@versionbefore generatingrefByNameVersion/components.
console.log(
`production-security-gate: prepared ${path.relative(root, target)} from pnpm-lock.yaml`,
);
}
RELEASE.md:39
- This release checklist still references
scripts/prepare-production-npm-audit.mjs, but that script no longer exists and the PR replaces the flow withscripts/production-security-gate.mjsexposed viapnpm run audit:production. Update these steps to avoid broken release instructions.
`npm run audit:supply-chain`. Also generate the ephemeral production npm
audit root with `scripts/prepare-production-npm-audit.mjs` and run
`npm audit --omit=dev --audit-level=moderate`.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated no new comments.
Suppressed comments (2)
scripts/production-security-gate.mjs:298
- In the SBOM dependency graph, dependency version strings coming from the converted pnpm snapshots can include peer-context suffixes (e.g.
8.66.0(eslint@...)).refByNameVersionis keyed by plainname@<baseVersion>, so the current lookup drops mostdependsOnedges and produces an incomplete/inaccurate dependency graph. Strip the peer-context suffix (keep the leading semver) before looking up refs.
dependencyRows.push({
ref: refByNameVersion.get(`${entry.name}@${entry.version}`),
dependsOn: Object.entries(dependencies)
.map(([name, version]) => refByNameVersion.get(`${name}@${version}`))
.filter(Boolean)
RELEASE.md:39
- This release checklist still references the removed
scripts/prepare-production-npm-audit.mjsflow. It should point to the newaudit:productionscript (or at leastscripts/production-security-gate.mjs) so the documented steps match the current gating behavior.
`npm run audit:supply-chain`. Also generate the ephemeral production npm
audit root with `scripts/prepare-production-npm-audit.mjs` and run
`npm audit --omit=dev --audit-level=moderate`.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
RELEASE.md:39
- RELEASE.md still references
scripts/prepare-production-npm-audit.mjs, but that script no longer exists (the production audit is now implemented byscripts/production-security-gate.mjsand exposed vianpm/pnpm run audit:production). This makes the release checklist incorrect.
`npm run audit:supply-chain`. Also generate the ephemeral production npm
audit root with `scripts/prepare-production-npm-audit.mjs` and run
`npm audit --omit=dev --audit-level=moderate`.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated no new comments.
Suppressed comments (3)
tests/unit/supply-chain-policy.unit.test.ts:952
- This test also writes a Node shebang script to
npm.cmdon Windows, which won't execute undercmd.exe. Use the samenpm.js+npm.cmdwrapper approach asfakeNpmAuditso the retry behavior test remains runnable on win32.
it("retries transient npm production audit registry failures", () => {
const dir = mkdtempSync(join(tmpdir(), "b2-mcp-production-audit-"));
const target = ".audit/test-production-retry";
const state = join(dir, "attempts");
const fakeNpm = join(dir, process.platform === "win32" ? "npm.cmd" : "npm");
scripts/production-security-gate.mjs:112
pathInsideRoot/pathStrictlyInsideRoottreat any relative path starting with ".." as outside the root. That incorrectly rejects legitimate children whose first segment starts with ".." (e.g..audit/.../foo), because"...".startsWith("..")is true. Use a segment-aware check (relative === ".."or starts with".." + path.sep) instead of a raw prefix check.
function pathInsideRoot(rootDir, target) {
const relative = path.relative(rootDir, target);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
tests/unit/supply-chain-policy.unit.test.ts:316
fakeNpmAuditwrites a Node shebang script tonpm.cmdon Windows..cmdfiles are executed bycmd.exe(not Node), so this helper won't work for Windows contributors. Write anpm.jsNode script plus a smallnpm.cmdwrapper on win32.
This issue also appears on line 946 of the same file.
function fakeNpmAudit(dir: string, report: unknown, exitCode: number) {
const fakeNpm = join(dir, process.platform === "win32" ? "npm.cmd" : "npm");
writeFileSync(
fakeNpm,
[
"#!/usr/bin/env node",
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated no new comments.
Suppressed comments (2)
tests/unit/supply-chain-policy.unit.test.ts:975
- This test creates a fake
npm.cmdon Windows by writing a Node script to it, but.cmdfiles must be batch scripts. If this test is ever run on Windows, the retry behavior will fail to execute. Write the Node script tonpm.jsand generate annpm.cmdwrapper that calls Node, or skip the test on win32.
it("retries transient npm production audit registry failures", () => {
const dir = mkdtempSync(join(tmpdir(), "b2-mcp-production-audit-"));
const target = ".audit/test-production-retry";
const state = join(dir, "attempts");
const fakeNpm = join(dir, process.platform === "win32" ? "npm.cmd" : "npm");
writeFileSync(
fakeNpm,
[
"#!/usr/bin/env node",
'const fs = require("node:fs");',
"const state = process.env.B2_MCP_FAKE_NPM_STATE;",
"let attempt = 0;",
'try { attempt = Number(fs.readFileSync(state, "utf8")); } catch {}',
"attempt += 1;",
"fs.writeFileSync(state, String(attempt));",
"const args = process.argv.slice(2);",
"if (args[0] !== 'audit' || !args.includes('--json')) process.exit(2);",
"if (attempt === 1) {",
' console.error("npm ERR! code EAI_AGAIN");',
' console.error("npm ERR! advisory endpoint timed out");',
" process.exit(1);",
"}",
`console.log(JSON.stringify(${JSON.stringify(emptyNpmAuditReport())}));`,
"process.exit(0);",
tests/unit/supply-chain-policy.unit.test.ts:316
- On Windows, writing a JavaScript payload directly to
npm.cmdwill not execute undercmd.exe, so the production gate tests will fail if run on win32. Either skip these tests on Windows or emit a realnpm.cmdwrapper that invokes a.jsfile with Node (matching hownpm.cmdworks).
This issue also appears on line 951 of the same file.
function fakeNpmAudit(dir: string, report: unknown, exitCode: number) {
const fakeNpm = join(dir, process.platform === "win32" ? "npm.cmd" : "npm");
writeFileSync(
fakeNpm,
[
"#!/usr/bin/env node",
"const args = process.argv.slice(2);",
"if (args[0] !== 'audit' || !args.includes('--json')) {",
" console.error(`unexpected npm args: ${args.join(' ')}`);",
" process.exit(2);",
"}",
`console.log(JSON.stringify(${JSON.stringify(report)}));`,
`process.exit(${exitCode});`,
].join("\n"),
);
chmodSync(fakeNpm, 0o755);
return fakeNpm;
}
Sophie/QK status noteConfirmed GitHub linkage: this merged PR is connected to #62, QK/QA position:
Follow-up should happen only if one of these is true:
No extra QK ticket is required from this comment alone. We will create one only if the current QK model cannot represent the evidence we need. |
Summary
scripts/production-security-gate.mjs, exposed throughpnpm run audit:productionandpnpm run release:sbom; the script derives a production-only audit lock and CycloneDX SBOM from the committedpnpm-lock.yaml, uses bounded npm audit retries, and rejects path traversal plus symlinked.audit/targets before delete/write operations.audit-policy.jsonexception model as the full pnpm audit, with negative tests for unaccepted production advisories and direct runtime deps emitted as non-dev.contents: writejob before npm trusted publishing, with explicitactions: readonly where release artifacts are downloaded.mark-greencannot advance without it, using a digest-pinned zizmor 1.29.0 container with no GitHub token, no online audits, and container networking disabled.opossum,@types/node, and@toon-format/toon.Linked issue
Closes #62
Tests run
pnpm run formatpnpm run format:checkpnpm run lintpnpm run lint:docspnpm run typechecknode scripts/run-vitest-layer.mjs unit -- tests/unit/supply-chain-policy.unit.test.ts tests/unit/workflow-yaml.unit.test.tsnode scripts/run-vitest-layer.mjs contract -- tests/contract/ci-workflow-policy.contract.test.ts tests/contract/security-remediation.contract.test.tspnpm run audit:productionpnpm run release:sbompnpm run test:unitpnpm run test:contractpnpm run audit:supply-chainuvx zizmor==1.29.0 --offline --min-severity medium --min-confidence medium .env -u GH_TOKEN uvx zizmor==1.29.0 --persona=regular --format=github --collect=default --no-online-audits --min-severity=medium --min-confidence=medium --color=never -- .pnpm run build,pnpm run spell,pnpm run check:package-budget,pnpm run test:package,pnpm run smoke:package,pnpm run test:protocol,pnpm run verify, Node 22.3.0 packed consumer smoke, and Node 24/26 package-budget checks.Follow-up notes
live-b2-contractenvironment secrets. The publish workflow now runs that suite on Node 22.23.1, 24, and 26 with bounded retry before npm publish can start.