Skip to content

Fix Security baseline gate: override brace-expansion in eslint's dependency tree - #473

Merged
richard-devbot merged 2 commits into
mainfrom
fix-eslint-security-advisory
Jul 30, 2026
Merged

Fix Security baseline gate: override brace-expansion in eslint's dependency tree#473
richard-devbot merged 2 commits into
mainfrom
fix-eslint-security-advisory

Conversation

@richard-devbot

@richard-devbot richard-devbot commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fixes Security baseline gate failing: eslint's transitive brace-expansion advisory #472 — CI's Security baseline job fails on main right now, independent of any PR. npm audit reports 5 high-severity advisories, all tracing to one root: a DoS/ReDoS bug in brace-expansion (GHSA-3jxr-9vmj-r5cp, GHSA-mh99-v99m-4gvg), reachable via eslint -> minimatch@3.1.5 -> brace-expansion@1.1.16.
  • npm audit's only reported fix was eslint@10 (isSemVerMajor: true) — tried it, but @eslint/js's ESLint-10 recommended config adds a new no-useless-assignment rule that flags 15 pre-existing dead-store patterns across src/. Too invasive for what's really a transitive-dependency-only issue (eslint is a devDependency, never installed by npm install rstack-agents).
  • Instead: added a root overrides entry (same mechanism this repo already uses for esbuild/protobufjs/ws) forcing brace-expansion to the patched 5.0.8 line. Confirmed this reaches eslint's own tree while — as the script's own comments document — leaving @earendil-works/pi-coding-agent's shrinkwrapped copy (5.0.6) untouched (npm honors a shrinkwrap absolutely; overrides can't reach it).
  • That shrinkwrapped copy is the only remaining high-severity finding, and it's exactly the node security-audit.mjs's BUNDLED_ROOTS/TOLERATED_BUNDLED_ADVISORIES allowlist already exists to tolerate. Added the second GHSA id to that allowlist — the gate is deliberately strict per-advisory-id (a newly-surfaced GHSA for an already-tolerated package still fails until consciously added), so this is a real, intentional decision, not a silent widening.

Test plan

  • npm run lint — unchanged (0 errors, same 4 pre-existing warnings), confirming minimatch@3.1.5 works fine against the newer brace-expansion at runtime
  • npm run typecheck — 0 errors
  • npm audit --json — only the already-tolerated bundled pi-coding-agent node remains; simulated the gate's own logic against the real audit output and confirmed 0 blocking
  • Full local test suite: same pre-existing failure count (locally, this Windows sandbox can't resolve bare npx/npm in spawn() without a shell — a known, unrelated environment limitation that also affects an unmodified main checkout identically; CI runs on Linux where this doesn't apply, same as the last two PRs)

Summary by CodeRabbit

  • Security

    • Updated security auditing to recognize an additional bundled brace-expansion advisory as tolerated.
    • Security checks now more reliably distinguish bundled vulnerabilities from issues requiring action.
  • Tests

    • Added automated coverage for advisory severity, dependency paths, allowlists, and blocking behavior.

…#472)

npm audit reported 5 high-severity advisories, all tracing to one root:
a DoS/ReDoS bug in brace-expansion (GHSA-3jxr-9vmj-r5cp,
GHSA-mh99-v99m-4gvg), reachable via eslint -> minimatch@3.1.5 ->
brace-expansion@1.1.16. npm's only reported fix was eslint@10
(isSemVerMajor), which pulls in a new no-useless-assignment rule
in @eslint/js's recommended config, flagging 15 pre-existing
dead-store patterns across src/ -- too invasive for a security-only
fix.

Instead: add a root `overrides` entry (this repo already uses the
same mechanism for esbuild/protobufjs/ws) forcing brace-expansion to
the patched 5.0.8 line. Verified this reaches eslint's own tree
(minimatch@3.1.5's brace-expansion now 5.0.8) while, as documented,
leaving @earendil-works/pi-coding-agent's shrinkwrapped copy (5.0.6)
untouched -- npm honors a shrinkwrap absolutely, so overrides can't
reach it regardless.

That shrinkwrapped copy is the only remaining high-severity finding,
and it's exactly the node this gate's BUNDLED_ROOTS/
TOLERATED_BUNDLED_ADVISORIES allowlist already exists to tolerate --
added the second GHSA id to that allowlist (the gate is deliberately
strict per-advisory-id, so a newly surfaced GHSA for an
already-tolerated package still fails until consciously added).

Verified: npm run lint unchanged (0 errors, same 4 pre-existing
warnings -- confirms minimatch@3.1.5 works fine with the newer
brace-expansion at runtime), npm run typecheck clean, gate logic
simulated against real npm audit output now reports 0 blocking.
@strix-security

strix-security Bot commented Jul 26, 2026

Copy link
Copy Markdown

Strix Security Review

No security issues found.

Updated for 780ed06.


Reviewed by Strix
Re-run review · Configure security review settings

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request adds a brace-expansion dependency override, refactors security-audit decision logic into exported functions, expands the tolerated advisory allowlist, guards script execution, and adds comprehensive tests.

Changes

Security audit baseline

Layer / File(s) Summary
Dependency and advisory configuration
package.json, scripts/security-audit.mjs
Adds the brace-expansion override and expands the exported bundled-advisory allowlist.
Reusable audit evaluation flow
scripts/security-audit.mjs
Exports bundled-path and advisory evaluation helpers, and moves audit execution into guarded main() logic.
Audit decision coverage
tests/security-audit.test.js
Tests bundled-path detection, advisory tolerance and blocking, severity filtering, advisory ID extraction, and the brace-expansion allowlist.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: richardsongunde

Poem

A rabbit checked the audit trail,
And found a brace within the warren.
“Bundled paths may pass,” it said,
“But wandering ones face the carrot.”
Tests now guard each burrow tight. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: pinning brace-expansion in ESLint's dependency tree to fix the security baseline gate.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-eslint-security-advisory

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix CI security baseline by overriding brace-expansion in ESLint tree

🐞 Bug fix ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Force patched brace-expansion version via npm overrides to unblock Security baseline gate.
• Update lockfile so ESLint’s minimatch chain resolves brace-expansion@5.0.8.
• Extend audit allowlist for shrinkwrapped bundled dependency to include newly surfaced GHSA.
Diagram

graph TD
  CI(["CI: Security baseline"]) --> AUD(["npm audit"]) --> GATE["scripts/security-audit.mjs"] --> OVR["package.json overrides"] --> LOCK["package-lock.json"]
  LOCK --> ESL["eslint → minimatch → brace-expansion@5.0.8"] --> OK["Gate passes"]
  LOCK --> BND["pi-coding-agent bundled brace-expansion@5.0.6"] --> ALW["Tolerated GHSA ids"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Upgrade ESLint major (eslint@10) and fix new lint violations
  • ➕ Eliminates the vulnerable transitive dependency without overrides
  • ➕ Aligns with upstream latest rules and ecosystem
  • ➖ Introduces new recommended rules causing non-trivial code churn (existing dead stores)
  • ➖ Higher risk/scope for what is effectively a devDependency-only audit issue
2. Update/replace the shrinkwrapped bundled dependency (pi-coding-agent)
  • ➕ Removes the last remaining high-severity advisory without allowlisting
  • ➕ Avoids relying on per-GHSA toleration over time
  • ➖ May require upstream release coordination or vendor changes
  • ➖ Potentially larger dependency and integration impact than a targeted override
3. Switch package manager/resolution mechanism (e.g., pnpm overrides/yarn resolutions)
  • ➕ Can offer more control/visibility over transitive dependency policies
  • ➕ May simplify future security remediation workflows
  • ➖ Tooling migration cost; impacts dev workflow and CI
  • ➖ Not necessary to address the immediate gate failure

Recommendation: The chosen approach (root-level npm overrides + explicit per-GHSA allowlisting for the unavoidable shrinkwrapped node) is the best short-term fix: it remediates the vulnerable ESLint dependency chain without a disruptive major ESLint upgrade, and keeps the security gate intentionally strict by requiring an explicit decision for each GHSA id. A follow-up should target the remaining shrinkwrapped brace-expansion by updating the bundled dependency upstream to eliminate the need for toleration.

Files changed (3) +24 / -27

Bug fix (1) +7 / -3
security-audit.mjsAllowlist additional brace-expansion GHSA for bundled shrinkwrap node +7/-3

Allowlist additional brace-expansion GHSA for bundled shrinkwrap node

• Extends the per-advisory-id allowlist for the bundled/shrinkwrapped brace-expansion instance to include GHSA-mh99-v99m-4gvg alongside the already tolerated GHSA-3jxr-9vmj-r5cp. Updates comments to document why the remaining finding is confined to the bundled root and why overrides cannot affect it.

scripts/security-audit.mjs

Other (2) +17 / -24
package-lock.jsonResolve brace-expansion to 5.0.8 and refresh dependent nodes +15/-23

Resolve brace-expansion to 5.0.8 and refresh dependent nodes

• Updates the lockfile so the eslint→minimatch chain no longer pulls brace-expansion@1.1.16, instead resolving brace-expansion@5.0.8. This also updates brace-expansion’s direct dependency (balanced-match) and removes concat-map from the resolved tree, reflecting the newer major line.

package-lock.json

package.jsonAdd npm override to pin brace-expansion to patched range +2/-1

Add npm override to pin brace-expansion to patched range

• Adds a top-level npm overrides rule forcing brace-expansion to >=5.0.8, consistent with existing overrides usage in the repo. This targets the transitive dependency causing the Security baseline gate failure without upgrading ESLint major.

package.json

@qodo-code-review

qodo-code-review Bot commented Jul 26, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 300 rules
✅ Skills: 17 invoked
  code-review-pr
  claude-api
  documentation-writing
  pptx
  docx
  performance-monitoring
  security-compliance
  cso
  plan-eng-review
  design-review
  prompt-engineering
  mcp-builder
  qa-testing
  code-patterns
  xlsx
  security-owasp
  testing-qa

Grey Divider


Remediation recommended

1. Node version mismatch 🐞 Bug ☼ Reliability
Description
The override pulls in brace-expansion@5.0.8 which declares Node "20 || >=22", but this repo declares
Node ">=18" and CI still runs a Node 18.x lane. This can cause install-time engine failures (in
stricter environments) or make the Node 18 lane brittle when the overridden dependency is exercised
(e.g., lint tooling).
Code

package-lock.json[R3569-3580]

    "node_modules/brace-expansion": {
-      "version": "1.1.16",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
-      "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
+      "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
-        "balanced-match": "^1.0.0",
-        "concat-map": "0.0.1"
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
      }
Relevance

●●● Strong

They’ve accepted Node version consistency fixes; engine mismatch could break Node 18 CI/install.

PR-#171

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The repo declares/support-tests Node 18, but the updated lockfile shows the overridden
brace-expansion now requires Node 20+.

package.json[41-43]
.github/workflows/ci.yml[13-17]
package-lock.json[3569-3580]
package.json[114-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`brace-expansion@5.0.8` is now forced into the root dependency tree, but it declares `engines.node = "20 || >=22"`, conflicting with this repo’s declared `engines.node >=18` and the CI matrix that still runs Node 18.

## Issue Context
Even if npm only warns by default, this becomes a hard failure in environments that enforce engines (or if the dependency starts using Node 20-only APIs).

## Fix Focus Areas
- package.json[41-43]
- package.json[114-119]
- .github/workflows/ci.yml[13-17]
- package-lock.json[3569-3580]

## What to change
Choose one:
1) If Node 18 is truly supported: use a patched `brace-expansion` version that supports Node 18, or scope the override so Node 18 paths don’t require Node 20-only packages.
2) If Node 18 is *not* supported in practice: update `package.json` engines and remove Node 18 from the CI matrix so the declared support matches reality.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Unbounded override range ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The override uses ">=5.0.8", which allows future lockfile regenerations to select newer (including
potentially breaking) major versions of brace-expansion. It also forces minimatch@3.1.5 to run
against brace-expansion 5.x even though minimatch declares a 1.x range, creating an ongoing
transitive-compatibility risk.
Code

package.json[R114-118]

  "overrides": {
    "esbuild": ">=0.28.1",
    "protobufjs": ">=7.6.4 <8",
-    "ws": ">=8.21.0"
+    "ws": ">=8.21.0",
+    "brace-expansion": ">=5.0.8"
Relevance

●● Moderate

No precedent on pinning overrides; repo already uses unbounded ">=" overrides, so change is
subjective.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new override is unbounded and the lockfile shows minimatch depends on brace-expansion ^1.1.7,
demonstrating the override is crossing a major range boundary.

package.json[114-119]
package-lock.json[4994-5005]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The override `"brace-expansion": ">=5.0.8"` is open-ended and can float to future major releases when the lockfile is regenerated. It also overrides `minimatch@3.1.5`’s declared dependency range (`^1.1.7`), so the repo is relying on compatibility that npm is no longer enforcing.

## Issue Context
Right now the lockfile pins `brace-expansion` to `5.0.8`, but the manifest range increases the chance of unexpected breakage on routine dependency maintenance.

## Fix Focus Areas
- package.json[114-119]
- package-lock.json[4994-5005]

## What to change
- Prefer pinning to an exact version (`"5.0.8"`) or a bounded range (`"^5.0.8"`).
- Consider scoping the override to only the eslint/minimatch subtree (npm overrides support nested overrides) so you don’t globally replace every consumer’s `brace-expansion` with a potentially incompatible major.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. security-audit.mjs change lacks tests ✓ Resolved 📘 Rule violation ▣ Testability
Description
This PR changes the Security baseline gate behavior by expanding TOLERATED_BUNDLED_ADVISORIES to
accept an additional GHSA ID, but no automated tests were added/updated in the same changeset to
cover this new allowlist behavior. Without tests, future refactors may unintentionally
broaden/narrow the gate logic and break CI or weaken security controls silently.
Code

scripts/security-audit.mjs[R31-42]

+// - brace-expansion GHSA-3jxr-9vmj-r5cp + GHSA-mh99-v99m-4gvg (both DoS):
+//   pinned 5.0.6 by pi's shrinkwrap, both advisories confined to that one
+//   bundled node (verified: eslint's own brace-expansion moved to the
+//   patched 5.0.8 after #472's eslint 9->10 bump, so only the shrinkwrapped
+//   copy remains on the vulnerable <=5.0.7 line); every non-shrinkwrapped
+//   path in our tree is on the fixed line.
// - protobufjs / ws: previously tolerated by name while their advisories were
//   live in the bundled subtree; currently dormant, so no ids are listed — if
//   one refires, the gate fails loudly and the specific GHSA gets re-added.
const TOLERATED_BUNDLED_ADVISORIES = new Map([
-  ['brace-expansion', new Set(['GHSA-3jxr-9vmj-r5cp'])],
+  ['brace-expansion', new Set(['GHSA-3jxr-9vmj-r5cp', 'GHSA-mh99-v99m-4gvg'])],
]);
Relevance

●● Moderate

They recently changed security-audit allowlist logic, but no evidence they require tests for
allowlist updates.

PR-#439

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1587034 requires that non-trivial behavior changes include accompanying automated
tests in the same changeset. The diff shows the gate behavior was changed by adding
GHSA-mh99-v99m-4gvg to the tolerated advisory IDs in scripts/security-audit.mjs, but no test
changes are present in the PR diff.

Rule 1587034: Require tests in the same changeset as behavior modifications
scripts/security-audit.mjs[31-42]
.github/workflows/ci.yml[97-101]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR updates `scripts/security-audit.mjs` to tolerate an additional `brace-expansion` advisory ID, which changes CI gate behavior, but there is no corresponding test change in this PR to lock in the intended behavior.

## Issue Context
The `security-baseline` CI job runs `node scripts/security-audit.mjs`, so changes to this script are executable behavior changes and should be covered by automated tests.

## Fix Focus Areas
- scripts/security-audit.mjs[31-42]
- tests/security-audit.test.js[1-200]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Incorrect audit script comment ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The updated comment claims eslint’s brace-expansion moved to 5.0.8 due to an “eslint 9->10 bump”,
but this PR keeps eslint at ^9 and makes the change via npm overrides. This mismatch can mislead
future debugging of the security baseline gate.
Code

scripts/security-audit.mjs[R31-36]

+// - brace-expansion GHSA-3jxr-9vmj-r5cp + GHSA-mh99-v99m-4gvg (both DoS):
+//   pinned 5.0.6 by pi's shrinkwrap, both advisories confined to that one
+//   bundled node (verified: eslint's own brace-expansion moved to the
+//   patched 5.0.8 after #472's eslint 9->10 bump, so only the shrinkwrapped
+//   copy remains on the vulnerable <=5.0.7 line); every non-shrinkwrapped
+//   path in our tree is on the fixed line.
Relevance

●●● Strong

Misleading comment is a straightforward maintainability fix; likely to be accepted when pointed out.

PR-#439

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The comment explicitly references an ESLint bump, but package.json shows ESLint is still ^9.0.0 and
the PR change is an override entry.

scripts/security-audit.mjs[31-36]
package.json[101-106]
package.json[114-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The comment in `scripts/security-audit.mjs` attributes the brace-expansion change to an ESLint 9→10 bump, but the repo is still on ESLint `^9.0.0` and the actual mechanism is a root `overrides` entry.

## Issue Context
This is documentation-only, but it directly affects maintainers’ ability to reason about why the vulnerability is (or isn’t) present.

## Fix Focus Areas
- scripts/security-audit.mjs[31-36]
- package.json[101-106]
- package.json[114-119]

## What to change
Rewrite the comment to say the non-shrinkwrapped tree is fixed due to the root `overrides` forcing `brace-expansion@5.0.8`, while the shrinkwrapped `@earendil-works/pi-coding-agent` copy remains pinned and tolerated by advisory id.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread scripts/security-audit.mjs
Comment thread package-lock.json
Comment on lines 3569 to 3580
"node_modules/brace-expansion": {
"version": "1.1.16",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
"balanced-match": "^4.0.2"
},
"engines": {
"node": "20 || >=22"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Node version mismatch 🐞 Bug ☼ Reliability

The override pulls in brace-expansion@5.0.8 which declares Node "20 || >=22", but this repo declares
Node ">=18" and CI still runs a Node 18.x lane. This can cause install-time engine failures (in
stricter environments) or make the Node 18 lane brittle when the overridden dependency is exercised
(e.g., lint tooling).
Agent Prompt
## Issue description
`brace-expansion@5.0.8` is now forced into the root dependency tree, but it declares `engines.node = "20 || >=22"`, conflicting with this repo’s declared `engines.node >=18` and the CI matrix that still runs Node 18.

## Issue Context
Even if npm only warns by default, this becomes a hard failure in environments that enforce engines (or if the dependency starts using Node 20-only APIs).

## Fix Focus Areas
- package.json[41-43]
- package.json[114-119]
- .github/workflows/ci.yml[13-17]
- package-lock.json[3569-3580]

## What to change
Choose one:
1) If Node 18 is truly supported: use a patched `brace-expansion` version that supports Node 18, or scope the override so Node 18 paths don’t require Node 20-only packages.
2) If Node 18 is *not* supported in practice: update `package.json` engines and remove Node 18 from the CI matrix so the declared support matches reality.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread package.json Outdated
Comment thread scripts/security-audit.mjs Outdated
Qodo flagged four issues on the eslint/brace-expansion security fix:
- no test coverage for the TOLERATED_BUNDLED_ADVISORIES allowlist change
- brace-expansion@5.0.8 declares engines.node "20 || >=22", excluding the
  Node 18 CI lane
- the ">=5.0.8" override is unbounded and can float to a future major
- the script comment mis-attributed the version bump to an eslint 9->10
  bump that never happened (the real mechanism is the root override)

Fix: bound the override to an exact "5.0.8" pin; export the pure
tolerate/block decision logic from security-audit.mjs (evaluateAdvisories,
isFullyBundled, advisoryIds) behind a main-guard so it's unit-testable
without shelling out to npm audit, and add tests/security-audit.test.js
covering the tolerate/block/allowlist-miss/severity-threshold paths, plus
a pinned-allowlist regression test; correct the stale comment to document
the real mechanism and the accepted Node 18 engines trade-off (no
brace-expansion release fixes both GHSAs while keeping Node 18 support;
it's a devDependency-only path and no engine-strict is set, so this
doesn't fail CI in practice).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/security-audit.mjs`:
- Around line 48-52: Update advisoryIds to fail closed for malformed advisory
objects: retain valid URL-derived IDs, but produce a non-tolerated result for
any object entry lacking a usable url instead of filtering it out. Ensure
callers of advisoryIds, including the additional referenced path, block the
report when such an unidentified entry is present.

In `@tests/security-audit.test.js`:
- Around line 59-72: Extend the security tests near the existing bundled
advisory case with a mixed-GHSA scenario for brace-expansion: include both the
allowlisted GHSA-3jxr-9vmj-r5cp and a new, non-allowlisted ID, then call
evaluateAdvisories with the same bundled configuration and assert the advisory
is blocking rather than tolerated. Keep the existing single-new-ID regression
intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8921db52-b91e-4e5f-adcd-b0207efd2798

📥 Commits

Reviewing files that changed from the base of the PR and between cce8808 and 41ba8f5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • package.json
  • scripts/security-audit.mjs
  • tests/security-audit.test.js

Comment on lines +48 to 52
export function advisoryIds(info) {
return (info.via ?? [])
.filter((entry) => typeof entry === 'object' && entry?.url)
.map((entry) => String(entry.url).split('/').pop());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed when an advisory entry cannot be identified.

advisoryIds drops object entries without url. A bundled report containing one allowlisted GHSA plus an unparseable advisory object is therefore tolerated. Treat malformed object entries as non-tolerated and block the advisory instead of filtering them out. As per coding guidelines, “validator restrictions must fail closed.”

Also applies to: 90-94

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/security-audit.mjs` around lines 48 - 52, Update advisoryIds to fail
closed for malformed advisory objects: retain valid URL-derived IDs, but produce
a non-tolerated result for any object entry lacking a usable url instead of
filtering it out. Ensure callers of advisoryIds, including the additional
referenced path, block the report when such an unidentified entry is present.

Source: Coding guidelines

Comment on lines +59 to +72
test('blocks a bundled advisory whose id is NOT on the allowlist — a new GHSA forces a conscious decision', () => {
const vulns = {
'brace-expansion': adv({
ids: ['GHSA-brand-new-id'],
nodes: [`${BUNDLED_ROOT}brace-expansion`],
}),
};
const { blocking, tolerated } = evaluateAdvisories(vulns, {
toleratedBundledAdvisories: new Map([['brace-expansion', new Set(['GHSA-3jxr-9vmj-r5cp'])]]),
bundledRoots: [BUNDLED_ROOT],
});
assert.equal(tolerated.length, 0);
assert.equal(blocking.length, 1);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Add a mixed-GHSA mutation regression.

Add a bundled brace-expansion case with one allowlisted ID and one new ID, asserting it blocks. The current suite would not catch an everysome mutation in evaluateAdvisories. As per coding guidelines, tests must include “mutation-oriented tests for security.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/security-audit.test.js` around lines 59 - 72, Extend the security tests
near the existing bundled advisory case with a mixed-GHSA scenario for
brace-expansion: include both the allowlisted GHSA-3jxr-9vmj-r5cp and a new,
non-allowlisted ID, then call evaluateAdvisories with the same bundled
configuration and assert the advisory is blocking rather than tolerated. Keep
the existing single-new-ID regression intact.

Source: Coding guidelines

richard-devbot added a commit that referenced this pull request Jul 30, 2026
…sses

Three independent reviewers (CodeRabbit, Strix, qodo) confirmed the same
class of bypass in isInterpreterExecCommand(): it only inspected the
FIRST whitespace token of a segment, so wrapping the interpreter behind a
launcher, a nested shell, or a version-suffixed binary evaded detection
entirely — reproduced live against the extension by CodeRabbit
(`bash -c "node -e ..."`) and Strix.

Concretely closes:
- Leading env-var assignments: `FOO=1 node -e ...`
- Launcher/wrapper commands: `env`/`sudo`/`timeout`/`nohup`/`command`/
  `nice`/`ionice`/`setsid`/`exec` node/python/etc, including stacked
  wrappers (`sudo env timeout 5 node -e ...`), each consuming its own
  flags/args before the real verb is found (bounded to 6 hops).
- Nested shells: `bash`/`sh`/`zsh`/`dash`/`ksh` are now themselves
  INTERPRETER_VERBS with `-c` as their eval flag — a shell is a
  general-purpose interpreter too, so `bash -c "node -e ..."` is caught
  on the OUTER bash invocation regardless of what's nested inside.
- Version-suffixed binaries: `python3.11`, `ruby3.2`, `perl5.34` (pyenv/
  asdf/system-package-manager convention) now normalize to their base
  interpreter name.
- Glued short flags: `-e'code'`, `-c"code"` (no space/`=` before the
  quoted value) are now recognized, scoped to <=2-char dash flags so a
  long `--flag` can't over-match as a prefix.
- Bare `deno` (no args): previously fell through to `args[0] undefined
  !== 'eval'` and returned false, unlike every other bare interpreter in
  this file (which correctly treat "no args at all" as stdin-fed code) —
  now consistent.

The validator sandbox reuses this exact detector, so every fix here
closes the same gap in both the builder approval-gate and the
deny-outright validator/reviewer/security path with one change.

Also: added @param/@returns to the exported isInterpreterExecCommand
JSDoc (qodo), and filled in docs/HARNESS.md's DESTRUCTIVE_CATEGORIES
list — it was missing interpreter-exec (this PR's own new category) plus
two pre-existing gaps (remote-exec, perm-destroy) found while fixing it.

Adversarial test coverage added for every bypass form above (wrapper
chains, nested shells, version suffixes, glued flags, bare deno) in both
the classifier and validator-sandbox suites, plus a false-positive check
confirming the fixes don't flag ordinary sudo/env/timeout/nohup usage.
1644/1644 tests, typecheck clean, lint clean.

Skipped as out of scope for this PR (all originate from the already-open
#473 branch merged in for CI, not from #477's own diff): security-audit.mjs
variable-naming/console.log/verb-noun-naming findings, and the Node-18-vs-
brace-expansion-5.0.8 engine note — belong to #473's review cycle.
Skipped as not applicable: "regression tests should be new files" (does
not match this codebase's actual, extensive convention of adding cases to
existing suite files) and `any`-typing in the claim gate (matches this
3000+-line file's pre-existing, pervasive typing convention; a scoped
retype was out of proportion to this fix).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
richard-devbot added a commit that referenced this pull request Jul 30, 2026
…sors

Two reviewers (qodo, Strix) found real correctness bugs in the claim
gate's #479 checks, all stemming from the same root cause: checking only
ONE item (via .find()) when the invariant actually requires checking
EVERY item.

1. (qodo, Action required) Active-stage check used .find() to grab a
   single IN_PROGRESS task and authorize the candidate against just that
   one. With overlapping parallel_groups (e.g. [A,B] and [A,C]), a
   candidate authorized against A (found first) could be wrongly allowed
   even though a DIFFERENT active task B was not authorized against it.
   Fixed: .filter() to get every active task, refuse if ANY of them is
   unauthorized with the candidate.

2. (qodo, Action required) Predecessor check only ever looked at the
   FIRST unmet predecessor (findUnmetPredecessorTask), and if that one
   happened to be exempt via parallel-group authorization, a second,
   non-exempt unmet predecessor elsewhere in the plan was never even
   checked. Fixed: new findUnmetPredecessorTasks (plural) returns every
   unmet predecessor; the claim gate finds the first one that ISN'T
   exempt, and refuses if any such predecessor exists. The singular
   function is now a thin wrapper (`[0]`) kept for its existing narrow
   callers/tests.

3. (Strix, HIGH, CWE-862) The predecessor exemption only checked
   parallel_groups CONFIG authorization, never whether the predecessor
   was actually running. Strix reproduced this live: validate an early
   stage into NEEDS_CONTEXT (its parallel_groups sibling is still
   configured as authorized), then call sdlc_build_next directly for
   that sibling — it claimed anyway, bypassing the runner's intended
   stop-for-human-context behavior. Fixed: exemption now requires
   `predecessor.status === "IN_PROGRESS"` in addition to authorization —
   a predecessor merely CONFIGURED into a group but not actually running
   (NEEDS_CONTEXT, BLOCKED, FAIL) is never exempt. Authorization only
   ever widens what may run alongside a genuinely active peer; it never
   excuses a predecessor that isn't running at all.

Also (qodo DRY finding): extracted the repeated "no task granted" return
object into one `refusal()` helper shared by the three return sites
(plain no-task, active-stage block, dependency block) instead of
duplicating the same seven-field literal three times.

Bonus fix found while writing the adversarial tests below: the
dependency_unmet tool response's `details` was missing `predecessor_status`
(present in the emitted event, but not in what the caller sees) — added.

Test coverage: findUnmetPredecessorTasks (plural) unit tests, plus three
new end-to-end tests reproducing each finding exactly — overlapping
active-task authorization, multiple unmet predecessors with only one
exempt, and the Strix NEEDS_CONTEXT bypass. 1645/1645 tests (1 known,
unrelated, pre-existing temp-dir-cleanup flake in dashboard-env-write.test.js
that passes in isolation), typecheck clean, lint clean.

Skipped as out of scope for this PR (originate from the already-open
#473 branch merged in for CI): security-audit.mjs advisoryIds naming,
console.log usage. Skipped as not applicable given this file's
pre-existing, pervasive convention: `any` typing in the claim gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
richard-devbot added a commit that referenced this pull request Jul 30, 2026
…re signal

Strix (HIGH, CWE-807) and qodo both found the same real gap in the
infra-failure heuristic: it inferred "the command never ran" from "stdout
is empty", but (a) a genuine test failure that only writes to stderr also
has empty stdout — a false positive that could silently downgrade a real
FAIL to a WARN — and (b) since the command runs via `sh -c` INSIDE the
container, repository-controlled test code could deliberately print one
of the known Docker/Podman error phrases to stderr and exit non-zero to
suppress its own real failure (the untrusted code influencing a trust
decision about itself).

Fixed with an unspoofable signal instead of an inference: buildSandboxArgv
now wraps the untrusted command so a TRUSTED start sentinel
(`__rstack_sandbox_started__`) prints to stdout before the command ever
runs. looksLikeInfraFailure now checks for the sentinel's presence in the
RAW (untruncated) stdout buffer — not "is stdout empty" — so:
- a real failure that writes only to stderr still shows the sentinel on
  stdout and correctly stays FAIL;
- untrusted code has no way to make the sentinel ABSENT, since the
  trusted wrapper's printf already ran and was already captured before
  the untrusted command got control — the only lever left to the
  untrusted code is printing the SAME sentinel too, which is harmless
  (still proves the command started, still a real FAIL either way).
The sentinel check runs on the full buffer before tail() truncates it, so
a long-output command whose sentinel would otherwise fall out of the
8KB tail window is still correctly recognized as started.

Also (qodo, real bug): validateSandboxConfig compared execution_policy
values case-sensitively while sandbox.js's resolveExecutionPolicy
normalizes via trim().toLowerCase() before comparing — an operator's
"Required" would validate as invalid even though it's accepted and
applied correctly at runtime. Added a matching normalizeExecutionPolicy
helper used by both the top-level and per-stage checks.

Also (qodo, minor): extracted the hardcoded `300` stderr-truncation length
in the infra-failure evidence string into a named
INFRA_FAILURE_EVIDENCE_MAX_CHARS constant.

Test coverage: updated the pinned argv test for the new wrapped command
format, two new tests proving a genuine stderr-only failure and a
deliberately daemon-phrase-mimicking real failure both stay FAIL, a
dedicated spoofing-attempt regression test, and case/whitespace
insensitivity coverage for the config validator matching the resolver.
1654/1654 tests, typecheck clean, lint clean.

Skipped as out of scope for this PR (originate from the already-open
#473 branch merged in for CI): PII-in-owner-comment finding (the
attribution line is this codebase's established, intentional convention,
naming the real project owner — not accidental PII), console.log usage
in security-audit.mjs. Skipped as not applicable given this file's
pre-existing convention: `any` typing on runValidationExecution.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@richard-devbot
richard-devbot merged commit 409d832 into main Jul 30, 2026
10 checks passed
@richard-devbot
richard-devbot deleted the fix-eslint-security-advisory branch July 30, 2026 18:02
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.

Security baseline gate failing: eslint's transitive brace-expansion advisory

2 participants