mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-11 23:27:25 +08:00
fix: harden CI validators
Ports personal-path validator hardening and quoted checkout detection onto current main.
This commit is contained in:
211
tests/ci/no-personal-paths.test.js
Normal file
211
tests/ci/no-personal-paths.test.js
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Tests for scripts/ci/validate-no-personal-paths.js.
|
||||
*
|
||||
* Run with: node tests/ci/no-personal-paths.test.js
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const repoRoot = path.join(__dirname, '..', '..');
|
||||
const validatorPath = path.join(repoRoot, 'scripts', 'ci', 'validate-no-personal-paths.js');
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` \u2713 ${name}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(` \u2717 ${name}`);
|
||||
console.log(` Error: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createTestDir() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'no-personal-paths-test-'));
|
||||
}
|
||||
|
||||
function cleanupTestDir(testDir) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function writeFile(filePath, content) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, content);
|
||||
}
|
||||
|
||||
function stripShebang(source) {
|
||||
let result = source;
|
||||
if (result.charCodeAt(0) === 0xFEFF) result = result.slice(1);
|
||||
if (result.startsWith('#!')) {
|
||||
const newline = result.indexOf('\n');
|
||||
result = newline === -1 ? '' : result.slice(newline + 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function runValidatorAgainst(testDir) {
|
||||
let source = fs.readFileSync(validatorPath, 'utf8');
|
||||
source = stripShebang(source);
|
||||
source = source.replace(
|
||||
/const ROOT = .*?;/,
|
||||
`const ROOT = ${JSON.stringify(testDir)};`,
|
||||
);
|
||||
|
||||
const tmpFile = path.join(
|
||||
os.tmpdir(),
|
||||
`no-personal-paths-${Date.now()}-${Math.random().toString(36).slice(2)}.js`,
|
||||
);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(tmpFile, source, 'utf8');
|
||||
const stdout = execFileSync('node', [tmpFile], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 10000,
|
||||
cwd: repoRoot,
|
||||
});
|
||||
return { code: 0, stdout, stderr: '' };
|
||||
} catch (err) {
|
||||
return {
|
||||
code: err.status || 1,
|
||||
stdout: err.stdout || '',
|
||||
stderr: err.stderr || '',
|
||||
};
|
||||
} finally {
|
||||
try { fs.unlinkSync(tmpFile); } catch (_) { /* ignore cleanup errors */ }
|
||||
}
|
||||
}
|
||||
|
||||
function runValidatorAgainstRealRepo() {
|
||||
try {
|
||||
const stdout = execFileSync('node', [validatorPath], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 10000,
|
||||
cwd: repoRoot,
|
||||
});
|
||||
return { code: 0, stdout, stderr: '' };
|
||||
} catch (err) {
|
||||
return {
|
||||
code: err.status || 1,
|
||||
stdout: err.stdout || '',
|
||||
stderr: err.stderr || '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Testing validate-no-personal-paths.js ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function record(ok) {
|
||||
if (ok) passed += 1;
|
||||
else failed += 1;
|
||||
}
|
||||
|
||||
record(test('passes against the real repository', () => {
|
||||
const result = runValidatorAgainstRealRepo();
|
||||
assert.strictEqual(result.code, 0, `expected exit 0; stderr: ${result.stderr}`);
|
||||
assert.ok(result.stdout.includes('Validated:'), 'expected success line in stdout');
|
||||
}));
|
||||
|
||||
record(test('flags a leaked /Users/<name> path', () => {
|
||||
const testDir = createTestDir();
|
||||
try {
|
||||
writeFile(path.join(testDir, 'skills', 'leaky', 'SKILL.md'), 'See /Users/sugig/.claude/settings.json\n');
|
||||
const result = runValidatorAgainst(testDir);
|
||||
assert.strictEqual(result.code, 1, 'expected non-zero exit on leak');
|
||||
assert.ok(result.stderr.includes('/Users/sugig'), `expected stderr to mention leaked path; got: ${result.stderr}`);
|
||||
assert.ok(result.stderr.includes('skills/leaky/SKILL.md'), `expected normalized file path; got: ${result.stderr}`);
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
}));
|
||||
|
||||
record(test('flags a leaked C:\\Users\\<name> path case-insensitively', () => {
|
||||
const testDir = createTestDir();
|
||||
try {
|
||||
writeFile(path.join(testDir, 'docs', 'guide.md'), 'See C:\\Users\\Affaan\\projects\\thing\n');
|
||||
const result = runValidatorAgainst(testDir);
|
||||
assert.strictEqual(result.code, 1, 'expected non-zero exit on leak');
|
||||
assert.ok(result.stderr.includes('C:\\Users\\Affaan'), `expected stderr to mention leaked path; got: ${result.stderr}`);
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
}));
|
||||
|
||||
record(test('allows /Users/<placeholder> templates', () => {
|
||||
const testDir = createTestDir();
|
||||
try {
|
||||
writeFile(path.join(testDir, 'commands', 'demo.md'), [
|
||||
'/Users/you/.claude/session.json',
|
||||
'/Users/example/.claude/rules/foo.md',
|
||||
'/Users/yourname/projects/app',
|
||||
'/Users/your-username/.claude/settings.json',
|
||||
'C:\\Users\\USER\\.claude\\settings.json',
|
||||
].join('\n'));
|
||||
const result = runValidatorAgainst(testDir);
|
||||
assert.strictEqual(result.code, 0, `expected exit 0 for placeholders; stderr: ${result.stderr}`);
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
}));
|
||||
|
||||
record(test('exempts docs/fixes forensic reports', () => {
|
||||
const testDir = createTestDir();
|
||||
try {
|
||||
writeFile(
|
||||
path.join(testDir, 'docs', 'fixes', 'HOOK-FIX-EXAMPLE.md'),
|
||||
'Reporter ran: C:\\Users\\sugig\\.claude\\settings.local.json\n',
|
||||
);
|
||||
const result = runValidatorAgainst(testDir);
|
||||
assert.strictEqual(result.code, 0, `expected exit 0 for docs/fixes; stderr: ${result.stderr}`);
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
}));
|
||||
|
||||
record(test('only scans configured file extensions', () => {
|
||||
const testDir = createTestDir();
|
||||
try {
|
||||
writeFile(path.join(testDir, 'skills', 'demo', 'image.png'), 'binary /Users/sugig/secret');
|
||||
const result = runValidatorAgainst(testDir);
|
||||
assert.strictEqual(result.code, 0, `expected non-text extensions to be skipped; stderr: ${result.stderr}`);
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
}));
|
||||
|
||||
record(test('reports every leak on a single offending file', () => {
|
||||
const testDir = createTestDir();
|
||||
try {
|
||||
writeFile(path.join(testDir, 'skills', 'multi', 'SKILL.md'), [
|
||||
'/Users/sugig/.claude/a.json',
|
||||
'/Users/sugig/.claude/b.json',
|
||||
'C:\\Users\\foo\\bar',
|
||||
].join('\n'));
|
||||
const result = runValidatorAgainst(testDir);
|
||||
assert.strictEqual(result.code, 1, 'expected non-zero exit on leak');
|
||||
const sugigCount = (result.stderr.match(/\/Users\/sugig/g) || []).length;
|
||||
const fooCount = (result.stderr.match(/C:\\Users\\foo/g) || []).length;
|
||||
assert.strictEqual(sugigCount, 2, `expected both /Users/sugig occurrences reported; got: ${result.stderr}`);
|
||||
assert.strictEqual(fooCount, 1, `expected C:\\Users\\foo reported once; got: ${result.stderr}`);
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
}));
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
console.log(`Failed: ${failed}\n`);
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -81,6 +81,24 @@ function run() {
|
||||
assert.match(result.stderr, /pull_request\.head\.sha/);
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Quoted action names are valid YAML. The checkout-step filter must still
|
||||
// inspect their `with.ref` values in privileged workflows.
|
||||
if (test('rejects pull_request_target checkout when uses is double-quoted', () => {
|
||||
const result = runValidator({
|
||||
'unsafe-double-quoted.yml': `name: Unsafe\non:\n pull_request_target:\n branches: [main]\njobs:\n inspect:\n runs-on: ubuntu-latest\n steps:\n - uses: "actions/checkout@v4"\n with:\n ref: \${{ github.event.pull_request.head.sha }}\n`,
|
||||
});
|
||||
assert.notStrictEqual(result.status, 0, 'Expected validator to fail on double-quoted uses:');
|
||||
assert.match(result.stderr, /pull_request\.head\.sha/);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects pull_request_target checkout when uses is single-quoted', () => {
|
||||
const result = runValidator({
|
||||
'unsafe-single-quoted.yml': `name: Unsafe\non:\n pull_request_target:\n branches: [main]\njobs:\n inspect:\n runs-on: ubuntu-latest\n steps:\n - uses: 'actions/checkout@v4'\n with:\n ref: \${{ github.event.pull_request.head.sha }}\n`,
|
||||
});
|
||||
assert.notStrictEqual(result.status, 0, 'Expected validator to fail on single-quoted uses:');
|
||||
assert.match(result.stderr, /pull_request\.head\.sha/);
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user