Skip to content

chore: add security release gates - #113

Merged
goanpeca merged 7 commits into
mainfrom
feat/issue-62-security-gate
Aug 6, 2026
Merged

goanpeca merged 7 commits into
mainfrom
feat/issue-62-security-gate

Conversation

@goanpeca

@goanpeca goanpeca commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Replaced the duplicated npm production audit/SBOM sequence with scripts/production-security-gate.mjs, exposed through pnpm run audit:production and pnpm run release:sbom; the script derives a production-only audit lock and CycloneDX SBOM from the committed pnpm-lock.yaml, uses bounded npm audit retries, and rejects path traversal plus symlinked .audit/ targets before delete/write operations.
  • Made the production npm audit evaluate JSON results through the same scoped, expiring audit-policy.json exception model as the full pnpm audit, with negative tests for unaccepted production advisories and direct runtime deps emitted as non-dev.
  • Added a CI production-audit matrix on Node 22.3.0, 24, and 26 that runs the repo-owned audit script directly, avoiding pnpm's newer engine floor on the Node 22.3.0 runtime boundary.
  • Made publishing wait on the protected live B2 contract suite for the exact resolved publish ref, added bounded retry for transient live-contract failures, and split durable SBOM upload into a separate contents: write job before npm trusted publishing, with explicit actions: read only where release artifacts are downloaded.
  • Moved workflow security analysis into CI so mark-green cannot advance without it, using a digest-pinned zizmor 1.29.0 container with no GitHub token, no online audits, and container networking disabled.
  • Configured weekly Dependabot npm/GitHub Actions updates with zizmor-compliant default cooldowns, grouped minor/patch development updates, and restored runtime-sensitive major holds for opossum, @types/node, and @toon-format/toon.
  • Superseded dependency PRs chore(deps): Bump the aws-sdk group across 1 directory with 3 updates #44, chore(deps): Bump body-parser from 2.2.2 to 2.3.0 #47, and chore(deps): Bump fast-uri from 3.1.2 to 3.1.4 #53 on current main: AWS SDK is reviewed at 3.1103.0, Axios/body-parser are absent, and fast-uri is dev-only at 3.1.5.
  • Updated release/testing docs for production audit commands, live B2 secret prerequisites, the required pre-created GitHub Release, and the SBOM upload/publish permission split.

Linked issue

Closes #62

Tests run

  • pnpm run format
  • pnpm run format:check
  • pnpm run lint
  • pnpm run lint:docs
  • pnpm run typecheck
  • node scripts/run-vitest-layer.mjs unit -- tests/unit/supply-chain-policy.unit.test.ts tests/unit/workflow-yaml.unit.test.ts
  • node scripts/run-vitest-layer.mjs contract -- tests/contract/ci-workflow-policy.contract.test.ts tests/contract/security-remediation.contract.test.ts
  • pnpm run audit:production
  • pnpm run release:sbom
  • pnpm run test:unit
  • pnpm run test:contract
  • pnpm run audit:supply-chain
  • uvx 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 -- .
  • Earlier implementation pass: 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

  • The live contract suite was not run locally because it requires the protected live-b2-contract environment secrets. The publish workflow now runs that suite on Node 22.23.1, 24, and 26 with bounded retry before npm publish can start.
  • The publish workflow expects the GitHub Release for the publish tag to exist so the SBOM can be attached durably before npm publishing.

Copilot AI lite review requested due to automatic review settings August 6, 2026 02:45
@goanpeca goanpeca added this to the v0.1.0 milestone Aug 6, 2026
@goanpeca goanpeca added the bug Something isn't working label Aug 6, 2026
@goanpeca goanpeca self-assigned this Aug 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 runs npm audit --omit=dev --audit-level=moderate in 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.

Comment thread scripts/prepare-production-npm-audit.mjs Outdated
Copilot AI review requested due to automatic review settings August 6, 2026 03:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 CycloneDX bom-ref for every lockfile entry. The converted npm-style lock can contain multiple packages entries with the same name+version (e.g. peer-context duplicates under .pnpm/...), which will produce duplicate bom-ref values and an invalid/ambiguous SBOM. Deduplicate components by name@version before generating refByNameVersion/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 with scripts/production-security-gate.mjs exposed via pnpm 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`.

Comment thread scripts/production-security-gate.mjs
Copilot AI review requested due to automatic review settings August 6, 2026 03:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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@...)). refByNameVersion is keyed by plain name@<baseVersion>, so the current lookup drops most dependsOn edges 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.mjs flow. It should point to the new audit:production script (or at least scripts/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`.

Copilot AI review requested due to automatic review settings August 6, 2026 03:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 by scripts/production-security-gate.mjs and exposed via npm/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`.

Comment thread .github/workflows/publish.yml
Copilot AI review requested due to automatic review settings August 6, 2026 03:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.cmd on Windows, which won't execute under cmd.exe. Use the same npm.js + npm.cmd wrapper approach as fakeNpmAudit so 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/pathStrictlyInsideRoot treat 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

  • fakeNpmAudit writes a Node shebang script to npm.cmd on Windows. .cmd files are executed by cmd.exe (not Node), so this helper won't work for Windows contributors. Write a npm.js Node script plus a small npm.cmd wrapper 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",

Copilot AI review requested due to automatic review settings August 6, 2026 03:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.cmd on Windows by writing a Node script to it, but .cmd files must be batch scripts. If this test is ever run on Windows, the retry behavior will fail to execute. Write the Node script to npm.js and generate an npm.cmd wrapper 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.cmd will not execute under cmd.exe, so the production gate tests will fail if run on win32. Either skip these tests on Windows or emit a real npm.cmd wrapper that invokes a .js file with Node (matching how npm.cmd works).

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;
  }

Copilot AI review requested due to automatic review settings August 6, 2026 03:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 20 changed files in this pull request and generated no new comments.

@goanpeca
goanpeca marked this pull request as ready for review August 6, 2026 04:00
@goanpeca
goanpeca merged commit 21f1fb0 into main Aug 6, 2026
21 checks passed
@sophiecarreras

Copy link
Copy Markdown
Contributor

Sophie/QK status note

Confirmed GitHub linkage: this merged PR is connected to #62, [b2-mcp] Remediate production dependencies and add a security gate.

QK/QA position:

  • This PR is already merged and [b2-mcp] Remediate production dependencies and add a security gate #62 is closed, so I would not treat this PR as an active review gate now.
  • For merged work, the right QA/QK path is not a retroactive PR review unless there is a concrete defect. The right path is a follow-up ticket if evidence is missing or QK cannot represent the evidence correctly.
  • For now, this is okay: the security-gate work has landed, and we can evaluate the current repo evidence from main.
  • QK may need to improve how it recognizes security-gate/release-gate evidence for MCP handoff repositories. If that gap appears in assessment, we should create a separate QK ticket.

Follow-up should happen only if one of these is true:

  • QK cannot classify the security gate evidence;
  • the security gate is not visible in current repo-owned checks;
  • the production dependency/audit evidence is missing, stale, or not runnable;
  • the security-team advice cannot be traced to a current check, doc, or protected workflow.

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.

@goanpeca
goanpeca deleted the feat/issue-62-security-gate branch September 1, 2026 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[b2-mcp] Remediate production dependencies and add a security gate

3 participants