Skip to content

ci: make audit gate scope explicit#62

Open
mbeacom wants to merge 1 commit into
mainfrom
audit-gate-published-scope
Open

ci: make audit gate scope explicit#62
mbeacom wants to merge 1 commit into
mainfrom
audit-gate-published-scope

Conversation

@mbeacom

@mbeacom mbeacom commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Problem

The current audit gate was true but over-broad in the assurance it appeared to give. scripts/audit-gate.ts reads bun audit --json for this repository's resolved workspace tree after the root package.json overrides are applied. Those overrides are not published in @adrkit/* package manifests, so a clean workspace audit is not evidence that a consumer install of the published packages is clean.

I re-verified the consumer gap with a real install of the published MCP package:

$ mkdir .consumer-audit-probe && cd .consumer-audit-probe
$ printf '{"name":"adrkit-consumer-audit-probe","private":true}\n' > package.json
$ bun add @adrkit/mcp@0.2.0 >/dev/null
Resolving dependencies
Resolved, downloaded and extracted [86]
Saved lockfile
$ bun audit
bun audit v1.3.14 (0d9b296a)
@hono/node-server  <2.0.5
  @adrkit/mcp › @modelcontextprotocol/sdk › @hono/node-server
  moderate: Node.js Adapter for Hono: Path traversal in `serve-static` on Windows via encoded backslash (`%5C`) - https://github.com/advisories/GHSA-frvp-7c67-39w9

1 vulnerabilities (1 moderate)

fast-uri is genuinely closed for consumers because the SDK resolves 3.1.4. The remaining exposure is the moderate GHSA-frvp-7c67-39w9; @modelcontextprotocol/sdk@1.29.0 is latest and still ranges @hono/node-server as ^1.19.9, so adrkit cannot fix this by bumping the SDK today. The stdio MCP server does not use Hono serve-static or expose HTTP static files, so the advisory is low-consequence for adrkit, but it should not be invisible.

Design

  • Keep the PR CI gate as a workspace resolved dependency tree audit, but make that scope explicit in the output and workflow comment.
  • Preserve fail-closed parsing behavior for no output, unparseable JSON, and unexpected report shapes.
  • Record the known @adrkit/mcp@0.2.0 consumer exposure in audit-gate output with exact package, GHSA, published package/version, upstream path, why it is upstream-blocked, why it is low-consequence here, and the condition that resolves it.
  • Make the acceptance narrow and expiring: after 2026-10-31, evaluateAudit fails closed with consumer-advisory-acceptance-expired until the exposure is removed, evidence is refreshed, or a human deliberately renews it.
  • Add proposed ADR-0017 (not accepted) documenting why published-artifact consumer audits are release evidence rather than ordinary PR CI.

Alternatives rejected

  • Audit published package consumer installs on every PR. This needs network and fresh installs of all four published packages (@adrkit/core, @adrkit/cli, @adrkit/evaluator, @adrkit/mcp), and it audits the versions already on npm rather than the source tree under review. The repo is currently ahead of @adrkit/mcp@0.2.0, so this would be slow/flaky evidence about a different artifact.
  • Treat the root override audit as sufficient. That is the false-assurance failure ADR-0016 warns about: "clean" would mean only "workspace clean after unpublished overrides."
  • Fail every PR on this known moderate advisory. There is no adrkit-side dependency bump available today, and the stdio server does not exercise the vulnerable serve-static path.

ADR-0016 observed-failing output

Before implementing the fix, I added the new assertions and ran them against the unfixed code:

$ bun test scripts/audit-gate.test.ts
bun test v1.3.14 (0d9b296a)

scripts/audit-gate.test.ts:
(pass) audit-gate — evaluateAudit > rejects the pre-fix tree and names the exact high-severity advisories [0.54ms]
(pass) audit-gate — evaluateAudit > passes a clean audit ({} — audit ran, found nothing) [0.10ms]
48 | 
49 |   test('states that the live gate audits the workspace resolved tree, not published consumer installs', async () => {
50 |     const evaluation = evaluateAudit(await readFixture('clean.json'), { asOf: '2026-07-25' });
51 |     const rendered = formatEvaluation(evaluation);
52 | 
53 |     expect(rendered).toContain('scope: workspace-resolved-dependency-tree');
                          ^
error: expect(received).toContain(expected)

Expected to contain: "scope: workspace-resolved-dependency-tree"
Received: "audit-gate: examined 0 package(s) with advisories (critical=0, high=0, moderate=0, low=0, info=0).\naudit-gate: PASSED — no high- or critical-severity advisories."

      at <anonymous> (/Users/markbeacom/github/mbeacom/copilot-worktrees/adrkit/audit-gate-published-scope/scripts/audit-gate.test.ts:53:22)
(fail) audit-gate — evaluateAudit > states that the live gate audits the workspace resolved tree, not published consumer installs [0.15ms]
58 |   test('records the known upstream-blocked @adrkit/mcp consumer advisory without hiding the scope gap', async () => {
59 |     const evaluation = evaluateAudit(await readFixture('clean.json'), { asOf: '2026-07-25' });
60 |     const known = evaluation.knownConsumerAdvisories?.[0];
61 | 
62 |     expect(evaluation.ok).toBe(true);
63 |     expect(known?.package).toBe('@hono/node-server');
                                ^
error: expect(received).toBe(expected)

Expected: "@hono/node-server"
Received: undefined

      at <anonymous> (/Users/markbeacom/github/mbeacom/copilot-worktrees/adrkit/audit-gate-published-scope/scripts/audit-gate.test.ts:63:28)
(fail) audit-gate — evaluateAudit > records the known upstream-blocked @adrkit/mcp consumer advisory without hiding the scope gap [0.12ms]
75 |   });
76 | 
77 |   test('fails closed once the known consumer advisory acceptance expires', async () => {
78 |     const evaluation = evaluateAudit(await readFixture('clean.json'), { asOf: '2026-11-01' });
79 | 
80 |     expect(evaluation.ok).toBe(false);
                               ^
error: expect(received).toBe(expected)

Expected: false
Received: true

      at <anonymous> (/Users/markbeacom/github/mbeacom/copilot-worktrees/adrkit/audit-gate-published-scope/scripts/audit-gate.test.ts:80:27)
(fail) audit-gate — evaluateAudit > fails closed once the known consumer advisory acceptance expires [0.11ms]

 16 pass
 3 fail
 50 expect() calls
Ran 19 tests across 1 file. [14.00ms]

What this does and does not guarantee

This PR guarantees that the existing gate says what it actually audited, keeps blind bun audit states failing closed, and makes the known @adrkit/mcp@0.2.0 consumer exposure visible with an expiry.

It does not continuously discover new consumer-only advisories for published packages on every PR. That should be added as release-time evidence at the artifact boundary, where the real published package versions are the intended subject.

Validation

$ bun test scripts/audit-gate.test.ts packages/cli/test/lint.test.ts
22 pass
0 fail
73 expect() calls

$ bun run typecheck && bun test && bun run lint
784 pass
0 fail
3495 expect() calls

$ bun run adr lint
checked 17 records, 0 errors, 0 warnings

$ bun run audit:gate
# passed; output now states scope and records GHSA-frvp-7c67-39w9 as known published-consumer exposure

The audit gate was giving a true but incomplete result: it audited the
workspace resolved tree after root overrides, not the dependency tree consumers
get from published @adrkit/* manifests. That distinction matters because root
overrides are not published, and @adrkit/mcp@0.2.0 still resolves the upstream-
blocked moderate @hono/node-server advisory for consumers.

This keeps the PR gate focused on the workspace tree, makes that scope explicit
in the output and workflow comment, records the known @adrkit/mcp consumer
exposure with an expiring narrow acceptance, and adds proposed ADR-0017 to
capture why published-artifact consumer audits belong in release evidence rather
than ordinary PR CI.

ADR-0016 observed-failing output before the implementation:

```text
$ bun test scripts/audit-gate.test.ts
bun test v1.3.14 (0d9b296a)

scripts/audit-gate.test.ts:
(pass) audit-gate — evaluateAudit > rejects the pre-fix tree and names the exact high-severity advisories [0.54ms]
(pass) audit-gate — evaluateAudit > passes a clean audit ({} — audit ran, found nothing) [0.10ms]
48 |
49 |   test('states that the live gate audits the workspace resolved tree, not published consumer installs', async () => {
50 |     const evaluation = evaluateAudit(await readFixture('clean.json'), { asOf: '2026-07-25' });
51 |     const rendered = formatEvaluation(evaluation);
52 |
53 |     expect(rendered).toContain('scope: workspace-resolved-dependency-tree');
                          ^
error: expect(received).toContain(expected)

Expected to contain: "scope: workspace-resolved-dependency-tree"
Received: "audit-gate: examined 0 package(s) with advisories (critical=0, high=0, moderate=0, low=0, info=0).\naudit-gate: PASSED — no high- or critical-severity advisories."

      at <anonymous> (/Users/markbeacom/github/mbeacom/copilot-worktrees/adrkit/audit-gate-published-scope/scripts/audit-gate.test.ts:53:22)
(fail) audit-gate — evaluateAudit > states that the live gate audits the workspace resolved tree, not published consumer installs [0.15ms]
58 |   test('records the known upstream-blocked @adrkit/mcp consumer advisory without hiding the scope gap', async () => {
59 |     const evaluation = evaluateAudit(await readFixture('clean.json'), { asOf: '2026-07-25' });
60 |     const known = evaluation.knownConsumerAdvisories?.[0];
61 |
62 |     expect(evaluation.ok).toBe(true);
63 |     expect(known?.package).toBe('@hono/node-server');
                                ^
error: expect(received).toBe(expected)

Expected: "@hono/node-server"
Received: undefined

      at <anonymous> (/Users/markbeacom/github/mbeacom/copilot-worktrees/adrkit/audit-gate-published-scope/scripts/audit-gate.test.ts:63:28)
(fail) audit-gate — evaluateAudit > records the known upstream-blocked @adrkit/mcp consumer advisory without hiding the scope gap [0.12ms]
75 |   });
76 |
77 |   test('fails closed once the known consumer advisory acceptance expires', async () => {
78 |     const evaluation = evaluateAudit(await readFixture('clean.json'), { asOf: '2026-11-01' });
79 |
80 |     expect(evaluation.ok).toBe(false);
                               ^
error: expect(received).toBe(expected)

Expected: false
Received: true

      at <anonymous> (/Users/markbeacom/github/mbeacom/copilot-worktrees/adrkit/audit-gate-published-scope/scripts/audit-gate.test.ts:80:27)
(fail) audit-gate — evaluateAudit > fails closed once the known consumer advisory acceptance expires [0.11ms]

 16 pass
 3 fail
 50 expect() calls
Ran 19 tests across 1 file. [14.00ms]
```

Validation after the fix:

```text
bun run typecheck && bun test && bun run lint
784 pass
0 fail
3495 expect() calls
```

Also ran `bun run adr lint` (checked 17 records, 0 errors, 0 warnings) and a
real consumer probe for `bun add @adrkit/mcp@0.2.0 && bun audit`, which reported
only GHSA-frvp-7c67-39w9 as one moderate advisory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 26, 2026 03:19
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

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

Clarifies the audit gate’s workspace-only scope and records the known published-consumer advisory with an expiring acceptance.

Changes:

  • Adds explicit audit scope and consumer advisory reporting.
  • Fails the gate after the acceptance expires.
  • Documents the decision in proposed ADR-0017 and updates tests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
scripts/audit-gate.ts Implements scope reporting and expiring advisory acceptance.
scripts/audit-gate.test.ts Tests scope, advisory details, and expiry.
packages/cli/test/lint.test.ts Updates the ADR corpus count.
docs/adr/0017-keep-dependency-audit-scope-explicit-and-release-scoped.md Records the proposed audit policy.
.github/workflows/ci.yml Clarifies the audit job’s scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants