Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/scripts/check-audit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env node
// Evaluates `npm audit --json` output and fails the build only on high/critical
// advisories that are NOT explicitly allowlisted.
//
// Why this exists:
// 1. The project stubs out `@posthog/cli` via an aliased override
// ("@posthog/cli": "npm:empty-npm-package@1.0.0"). npm 10 (bundled with
// Node 20) crashes on aliased overrides during audit with
// "Invalid comparator", so the workflow runs audit with npm 11.
// 2. Some high-severity advisories have no safe upstream fix yet and cannot be
// patched without breaking changes. Those are documented below and allowed
// until an upstream release resolves them.
//
// Usage: node check-audit.mjs <path-to-npm-audit-json>

import { readFileSync } from "node:fs";

// Advisories that are known, reviewed, and intentionally allowed because no
// non-breaking fix is available yet. Keep this list short and time-bound.
const ALLOWLIST = [
{
url: "https://github.com/advisories/GHSA-34r5-q4jw-r36m",
package: "samlify",
reason:
"Surfaced transitively via @better-auth/sso, which pins samlify '~2.10.2'. " +
"The patched samlify (>=2.13.0) is outside that range, so forcing it risks " +
"breaking SAML/SSO. No @better-auth/sso release bumps samlify yet (latest 1.6.11). " +
"Re-evaluate when @better-auth/sso ships a patched samlify.",
},
];

const path = process.argv[2];
if (!path) {
console.error("Usage: check-audit.mjs <audit-json-file>");
process.exit(2);
}

let report;
try {
report = JSON.parse(readFileSync(path, "utf8"));
} catch (err) {
console.error(`Could not read/parse audit JSON at ${path}: ${err.message}`);
process.exit(2);
}

// npm prints an `error` object when audit itself failed to run (e.g. the
// "Invalid comparator" crash on older npm). Treat that as a hard failure so we
// never silently skip the gate.
if (report.error) {
console.error(`npm audit failed to run: ${report.error.summary || JSON.stringify(report.error)}`);
process.exit(2);
}

const allowedUrls = new Set(ALLOWLIST.map((a) => a.url));
const vulns = report.vulnerabilities || {};

// Collect concrete advisory objects (high/critical) from every vulnerability's
// `via` chain. Transitive-only entries (whose `via` is just a package name)
// carry no advisory object and are covered once their root advisory is allowed.
const advisories = new Map(); // url -> { url, title, severity }
for (const vuln of Object.values(vulns)) {
for (const via of vuln.via || []) {
if (typeof via !== "object") continue;
if (via.severity !== "high" && via.severity !== "critical") continue;
if (via.url) advisories.set(via.url, { url: via.url, title: via.title, severity: via.severity });
}
}
Comment on lines +61 to +67
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

High/critical advisories without a url are silently dropped from the gate.

A via advisory object is only recorded when via.url is truthy (Line 65). Any high/critical advisory object lacking a url is skipped entirely, so it neither blocks nor gets reported. It also can't be allowlisted (the allowlist keys on url), so the safe default for such an entry is to block. In practice npm/GHSA advisories carry a url, hence the low priority, but for a security gate it's worth failing closed.

🛡️ Fail closed when a high/critical advisory has no url
 for (const vuln of Object.values(vulns)) {
   for (const via of vuln.via || []) {
     if (typeof via !== "object") continue;
     if (via.severity !== "high" && via.severity !== "critical") continue;
-    if (via.url) advisories.set(via.url, { url: via.url, title: via.title, severity: via.severity });
+    // Use the url as the dedup/allowlist key; fall back to a synthetic key so
+    // an advisory without a url still surfaces and blocks (it can't be allowlisted).
+    const key = via.url || `${via.source ?? via.title ?? "unknown"}`;
+    advisories.set(key, { url: via.url || "(no url)", title: via.title, severity: via.severity });
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const vuln of Object.values(vulns)) {
for (const via of vuln.via || []) {
if (typeof via !== "object") continue;
if (via.severity !== "high" && via.severity !== "critical") continue;
if (via.url) advisories.set(via.url, { url: via.url, title: via.title, severity: via.severity });
}
}
for (const vuln of Object.values(vulns)) {
for (const via of vuln.via || []) {
if (typeof via !== "object") continue;
if (via.severity !== "high" && via.severity !== "critical") continue;
// Use the url as the dedup/allowlist key; fall back to a synthetic key so
// an advisory without a url still surfaces and blocks (it can't be allowlisted).
const key = via.url || `${via.source ?? via.title ?? "unknown"}`;
advisories.set(key, { url: via.url || "(no url)", title: via.title, severity: via.severity });
}
}
🤖 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 @.github/scripts/check-audit.mjs around lines 61 - 67, The loop currently
ignores high/critical advisories that lack via.url; change it so all
high/critical via entries are recorded into the advisories Map even when via.url
is falsy: in the block iterating vuln.via (symbols: advisories, via, vuln,
vuln.via) remove the guard that skips when via.url is falsy and instead compute
a stable key (e.g. via.url ||
`${vuln.name||vuln.title||'advisory'}:${via.severity}:${via.id||i}`) and call
advisories.set(key, { url: via.url || null, title: via.title, severity:
via.severity, id: via.id || null }) so entries without a url are captured (with
url null) and will block by default and can be matched by an allowlist
separately.


const blocking = [];
const allowed = [];
for (const adv of advisories.values()) {
(allowedUrls.has(adv.url) ? allowed : blocking).push(adv);
}

if (allowed.length) {
console.log("Allowlisted high/critical advisories (not blocking):");
for (const a of allowed) console.log(` - [${a.severity}] ${a.title} (${a.url})`);
}

if (blocking.length) {
console.error("\nBlocking high/critical advisories:");
for (const a of blocking) console.error(` - [${a.severity}] ${a.title} (${a.url})`);
console.error(
"\nResolve these (bump/override the affected package) or, if there is no safe fix, " +
"add the advisory to the ALLOWLIST in .github/scripts/check-audit.mjs with justification.",
);
process.exit(1);
}

console.log(`\nNo blocking high/critical advisories (${allowed.length} allowlisted).`);
process.exit(0);
10 changes: 8 additions & 2 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,14 @@ jobs:
id: audit
shell: bash
run: |
set -euo pipefail
npm audit --audit-level=high 2>&1 | tee audit-output.txt
set -uo pipefail
# npm 10 (bundled with Node 20) crashes on aliased overrides
# ("npm:...") during audit with "Invalid comparator"; npm 11 handles
# them. Run audit with npm 11 and evaluate the result via a script
# that fails on high/critical advisories except documented allowlisted
# ones (see .github/scripts/check-audit.mjs).
npx -y npm@11 audit --json > audit-output.json 2> audit-error.txt || true
node .github/scripts/check-audit.mjs audit-output.json 2>&1 | tee audit-output.txt
Comment on lines +55 to +56
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the latest npm 11 version and its required Node.js engines range?

💡 Result:

As of May 29, 2026, the latest version of npm 11 is 11.16.0 [1]. The required Node.js engines range for npm 11 is defined as ^20.17.0 || >=22.9.0 [2]. This range was established to support newer Node.js releases while ensuring compatibility with supported LTS versions [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Show the workflow section around the referenced lines and any setup-node config nearby.
FILE=".github/workflows/pr-validation.yml"
if [ -f "$FILE" ]; then
  echo "== $FILE =="
  nl -ba "$FILE" | sed -n '1,140p'
else
  echo "Missing $FILE" >&2
  exit 1
fi

# Also show the check-audit script for context on what error outputs it consumes.
SCRIPT=".github/scripts/check-audit.mjs"
if [ -f "$SCRIPT" ]; then
  echo "== $SCRIPT =="
  nl -ba "$SCRIPT" | sed -n '1,200p'
else
  echo "Missing $SCRIPT" >&2
  exit 1
fi

Repository: reqcore-inc/reqcore

Length of output: 166


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=".github/workflows/pr-validation.yml"
if [ -f "$FILE" ]; then
  echo "== $FILE =="
  awk 'NR>=1 && NR<=160 {printf "%d:%s\n", NR, $0}' "$FILE"
else
  echo "Missing $FILE" >&2
  exit 1
fi

echo
echo "== setup-node occurrences =="
rg -n "setup-node|node-version" .github/workflows/pr-validation.yml .github/workflows -S || true

echo
SCRIPT=".github/scripts/check-audit.mjs"
if [ -f "$SCRIPT" ]; then
  echo "== $SCRIPT =="
  awk 'NR>=1 && NR<=220 {printf "%d:%s\n", NR, $0}' "$SCRIPT"
else
  echo "Missing $SCRIPT" >&2
  exit 1
fi

Repository: reqcore-inc/reqcore

Length of output: 8324


Pin npm@11 (and surface audit-error.txt)—Node 20 is compatible

  • actions/setup-node@v6 uses node-version: 20; npm 11’s engines are ^20.17.0 || >=22.9.0, so the current Node 20 setup should satisfy the requirement.
  • audit-error.txt is still never surfaced in the PR summary; if npm audit fails before producing useful JSON, the stderr context won’t be visible (only audit-output.txt is).
  • npx -y npm@11 is floating; pinning to a specific npm 11.x version (e.g., npm@11.16.0) would make the audit gate reproducible over time.
🤖 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 @.github/workflows/pr-validation.yml around lines 55 - 56, Pin the floating
npm invocation and surface the stderr file so audit failures are reproducible
and visible: change the npx call (currently "npx -y npm@11 audit ...") to a
pinned release such as "npx -y npm@11.16.0 audit --json > audit-output.json 2>
audit-error.txt || true" and update the downstream invocation of the audit
checker (node .github/scripts/check-audit.mjs) to also consume or print
audit-error.txt (for example pass audit-error.txt as an additional argument to
check-audit.mjs or cat audit-error.txt into the piped output) so the contents of
audit-error.txt are included in audit-output.txt and the PR summary; ensure
references to "npx -y npm@11", "audit-error.txt", and "node
.github/scripts/check-audit.mjs" are updated accordingly.


- name: Build
id: build
Expand Down
Loading