Skip to content
Merged
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
17 changes: 17 additions & 0 deletions .audit-allowlist.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"_comment": "Befristete Ausnahmen für scripts/audit-gate.mjs. Nur Findings ohne Upstream-Fix im Build-Tooling (nicht im App-Bundle). Nach Ablauf schlägt das Gate fehl -> neu bewerten (DrawFromMemory#348).",
"advisories": [
{
"id": "GHSA-86w9-cpqp-85rv",
"package": "node-forge",
"reason": "RSA PKCS#1 v1.5: kein gepatchter Release; nur via expo -> @expo/cli (Build-Tooling)",
"expires": "2026-12-31"
},
{
"id": "GHSA-vfj7-8cjw-p6xm",
"package": "braces",
"reason": "Stack-Exhaustion DoS: kein gepatchter Release; nur via micromatch/patch-package (Dev-Tooling)",
"expires": "2026-12-31"
}
]
}
5 changes: 3 additions & 2 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,9 @@ jobs:
with:
node-version: '20'

- name: Run npm audit
run: npm audit --audit-level=high
# Gate mit befristeter Allowlist (.audit-allowlist.json) für Findings ohne Upstream-Fix (#348).
- name: Run npm audit (Gate)
run: node scripts/audit-gate.mjs --level=high

- name: Check for sensitive data
run: |
Expand Down
91 changes: 91 additions & 0 deletions scripts/audit-gate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env node
/**
* Audit-Gate: `npm audit` mit expliziter, befristeter Allowlist.
*
* Schlägt fehl bei
* - hohen/kritischen Advisories, die NICHT in .audit-allowlist.json stehen
* - Allowlist-Einträgen, deren `expires` überschritten ist (Ausnahme muss neu bewertet werden)
* Nur Advisories (GHSA) werden verglichen, nicht Paketnamen: Ein neues Finding im selben
* Paket fällt dadurch weiterhin auf.
*
* Usage: node scripts/audit-gate.mjs [--level=high|critical]
*/
import { execFileSync } from 'node:child_process';
import { readFileSync, existsSync } from 'node:fs';

const level = (process.argv.find(a => a.startsWith('--level=')) || '--level=high').split('=')[1];
const RANK = { low: 1, moderate: 2, high: 3, critical: 4 };
const threshold = RANK[level];
if (!threshold) {
console.error(`Unbekanntes Level: ${level}`);
process.exit(2);
}

const allowlist = existsSync('.audit-allowlist.json')
? JSON.parse(readFileSync('.audit-allowlist.json', 'utf8')).advisories
: [];
const today = new Date().toISOString().slice(0, 10);

let raw;
try {
raw = execFileSync('npm', ['audit', '--json'], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
} catch (e) {
// npm audit endet bei Findings mit Exit-Code 1, die JSON-Ausgabe liegt trotzdem auf stdout.
raw = e.stdout;
if (!raw) {
console.error('npm audit lieferte keine Ausgabe:', e.message);
process.exit(2);
}
}
const report = JSON.parse(raw);
if (report.error) {
console.error('npm audit Fehler:', report.error.summary || report.error);
process.exit(2);
}

// Eindeutige Advisories sammeln (`via`-Objekte; Strings sind nur Verweise auf andere Pakete).
const advisories = new Map();
for (const [pkg, vuln] of Object.entries(report.vulnerabilities || {})) {
for (const via of vuln.via) {
if (typeof via !== 'object') continue;
const id = (via.url || '').split('/').pop() || via.title;
if (!advisories.has(id))
advisories.set(id, { id, pkg: via.name || pkg, title: via.title, severity: via.severity });
}
}

const allowed = new Map(allowlist.map(a => [a.id, a]));
const blocking = [];
const accepted = [];
for (const adv of advisories.values()) {
if (RANK[adv.severity] < threshold) continue;
const entry = allowed.get(adv.id);
(entry ? accepted : blocking).push({ ...adv, entry });
}
const expired = allowlist.filter(a => a.expires < today);
const unused = allowlist.filter(a => !advisories.has(a.id));

for (const a of accepted) {
console.log(`ℹ️ akzeptiert bis ${a.entry.expires}: ${a.id} (${a.pkg}) – ${a.entry.reason}`);
}
for (const a of unused) {
console.log(`ℹ️ Allowlist-Eintrag ohne Treffer, kann entfernt werden: ${a.id} (${a.package})`);
}
let failed = false;
for (const a of blocking) {
failed = true;
console.error(`❌ ${a.severity}: ${a.id} (${a.pkg}) – ${a.title}`);
}
for (const a of expired) {
failed = true;
console.error(
`❌ Ausnahme abgelaufen (${a.expires}): ${a.id} (${a.package}) – neu bewerten oder verlängern`,
);
}
if (failed) {
console.error('\nAudit-Gate: FEHLGESCHLAGEN');
process.exit(1);
}
console.log(
`✅ Audit-Gate bestanden (${accepted.length} akzeptierte, ${blocking.length} offene ${level}+-Findings)`,
);
Loading