ci: require npm audit signature checks

Require npm registry signature verification wherever workflow npm audit checks run.

- add npm audit signatures to CI Security Scan and maintenance security audit jobs
- teach the workflow security validator to reject npm audit without signature verification
- keep the repair and Copilot prompt tests portable across Windows path/case and CRLF frontmatter behavior

Validation:
- node tests/run-all.js (2376 passed, 0 failed)
- CI current-head matrix green on #1846
This commit is contained in:
Affaan Mustafa
2026-05-12 23:48:56 -04:00
committed by GitHub
parent 766f4ee1d8
commit 797f283036
6 changed files with 44 additions and 3 deletions

View File

@@ -243,7 +243,9 @@ jobs:
node-version: '20.x'
- name: Run npm audit
run: npm audit --audit-level=high
run: |
npm audit signatures
npm audit --audit-level=high
continue-on-error: true # Allows PR to proceed, but marks job as failed if vulnerabilities found
lint:

View File

@@ -34,6 +34,7 @@ jobs:
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts
npm audit signatures
npm audit --audit-level=high
else
echo "No package-lock.json found; skipping npm audit"

View File

@@ -26,6 +26,8 @@ const RULES = [
const WRITE_PERMISSION_PATTERN = /^\s*(?:contents|issues|pull-requests|actions|checks|deployments|discussions|id-token|packages|pages|repository-projects|security-events|statuses):\s*write\b/m;
const NPM_CI_PATTERN = /\bnpm\s+ci\b(?![^\n]*--ignore-scripts)/g;
const NPM_AUDIT_PATTERN = /\bnpm\s+audit\b(?!\s+signatures\b)/;
const NPM_AUDIT_SIGNATURES_PATTERN = /\bnpm\s+audit\s+signatures\b/;
const ACTIONS_CACHE_PATTERN = /uses:\s*['"]?actions\/cache@/m;
const ID_TOKEN_WRITE_PATTERN = /^\s*id-token:\s*write\b/m;
@@ -127,6 +129,16 @@ function findViolations(filePath, source) {
});
}
if (NPM_AUDIT_PATTERN.test(source) && !NPM_AUDIT_SIGNATURES_PATTERN.test(source)) {
violations.push({
filePath,
event: 'npm audit signatures',
description: 'workflows that run npm audit must also verify registry signatures',
expression: 'npm audit without npm audit signatures',
line: getLineNumber(source, source.search(NPM_AUDIT_PATTERN)),
});
}
return violations;
}

View File

@@ -122,6 +122,21 @@ function run() {
assert.match(result.stderr, /id-token: write must not restore or save shared dependency caches/);
})) passed++; else failed++;
if (test('rejects npm audit without registry signature verification', () => {
const result = runValidator({
'unsafe-audit.yml': `name: Unsafe\non:\n push:\njobs:\n audit:\n runs-on: ubuntu-latest\n steps:\n - run: npm audit --audit-level=high\n`,
});
assert.notStrictEqual(result.status, 0, 'Expected validator to fail when npm audit signatures is missing');
assert.match(result.stderr, /npm audit must also verify registry signatures/);
})) passed++; else failed++;
if (test('allows npm audit when registry signatures are verified', () => {
const result = runValidator({
'safe-audit.yml': `name: Safe\non:\n push:\njobs:\n audit:\n runs-on: ubuntu-latest\n steps:\n - run: |\n npm audit signatures\n npm audit --audit-level=high\n`,
});
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
})) passed++; else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);

View File

@@ -27,7 +27,8 @@ function read(relativePath) {
}
function parseSimpleFrontmatter(source, relativePath) {
const match = source.match(/^---\n([\s\S]*?)\n---\n/);
const normalizedSource = source.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
const match = normalizedSource.match(/^---\n([\s\S]*?)\n---\n/);
assert.ok(match, `${relativePath} must start with YAML frontmatter`);
const fields = {};

View File

@@ -64,6 +64,16 @@ function runNode(scriptPath, args = [], options = {}) {
}
}
function normalizeComparablePath(filePath) {
const normalized = path.normalize(filePath);
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
}
function pathListIncludes(paths, expectedPath) {
const normalizedExpected = normalizeComparablePath(expectedPath);
return paths.some(filePath => normalizeComparablePath(filePath) === normalizedExpected);
}
function test(name, fn) {
try {
fn();
@@ -117,7 +127,7 @@ function runTests() {
const parsed = JSON.parse(repairResult.stdout);
assert.strictEqual(parsed.results[0].status, 'repaired');
assert.ok(parsed.results[0].repairedPaths.includes(managedPath));
assert.ok(pathListIncludes(parsed.results[0].repairedPaths, managedPath));
assert.strictEqual(fs.readFileSync(managedPath, 'utf8'), expectedContent);
assert.ok(fs.existsSync(statePath));
} finally {