test(llm-security): cover TRG/SIG/AST in e2e pipeline + SIG miner/hacktool/rot13 families (#58,#59)

#58 the e2e scan-pipeline suite omitted trg/sig/ast from EXPECTED_SCANNERS and no fixture carried trigger-abuse/known-signature/python-taint content, so the three v7.8.0 scanners' surfacing through the aggregate was never asserted; added a fixture (shadowing command name → TRG, webshell → SIG, tainted os.environ→requests.post → AST, python3-guarded) that asserts each appears in the envelope and the rolled-up counts/owasp_breakdown. #59 the SIG cryptominer (SIG-MINER-001/002) and hacktool (SIG-HACKTOOL-001) families and non-base64 decode variants had no test; added family-attribution tests and a rot13-decode assertion. Test-only, suite 2013/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
This commit is contained in:
Kjell Tore Guttormsen 2026-07-18 10:46:19 +02:00
commit 14070160ba
2 changed files with 227 additions and 6 deletions

View file

@ -13,6 +13,7 @@ import { tmpdir } from 'node:os';
import { resetCounter } from '../../scanners/lib/output.mjs';
import { discoverFiles } from '../../scanners/lib/file-discovery.mjs';
import { scan } from '../../scanners/signature-scanner.mjs';
import { rot13 } from '../../scanners/lib/string-utils.mjs';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const CLEAN_FIXTURE = resolve(__dirname, '../fixtures/signature-scan/clean');
@ -121,6 +122,96 @@ describe('signature-scanner: poisoned', () => {
});
});
// ---------------------------------------------------------------------------
// Cryptominer + hacktool families (#59) — the on-disk poisoned fixture only
// covers webshell + reverse_shell, so SIG-MINER-* and SIG-HACKTOOL-001 were
// never exercised. Non-base64 decode coverage (rot13) was also missing: the
// only decode-pipeline test used base64. Each temp fixture holds exactly one
// vector so the family attribution is unambiguous.
// ---------------------------------------------------------------------------
describe('signature-scanner: cryptominer + hacktool families', () => {
it('fires the cryptominer family on a known miner binary reference (SIG-MINER-002)', async () => {
const dir = mkdtempSync(join(tmpdir(), 'sig-miner-'));
try {
writeFileSync(join(dir, 'start.sh'), '#!/bin/sh\n./xmrig --coin monero -o pool.example:3333\n');
resetCounter();
const discovery = await discoverFiles(dir);
const result = await scan(dir, discovery);
assert.equal(result.status, 'ok');
const miner = result.findings.filter(f => /\[cryptominer\]/.test(f.evidence || ''));
assert.ok(miner.length >= 1, `expected a cryptominer finding, got: ${result.findings.map(f => f.evidence).join('; ')}`);
assert.ok(miner.some(f => /SIG-MINER-002/.test(f.evidence)), 'should attribute to SIG-MINER-002 (miner binary)');
assert.ok(miner.every(f => f.scanner === 'SIG'), 'miner findings must carry scanner SIG');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it('fires the cryptominer family on a stratum pool URL (SIG-MINER-001)', async () => {
const dir = mkdtempSync(join(tmpdir(), 'sig-stratum-'));
try {
writeFileSync(join(dir, 'config.txt'), 'pool = stratum+tcp://pool.example.org:4444\n');
resetCounter();
const discovery = await discoverFiles(dir);
const result = await scan(dir, discovery);
const miner = result.findings.filter(f => /SIG-MINER-001/.test(f.evidence || ''));
assert.ok(miner.length >= 1, `expected a SIG-MINER-001 stratum finding, got: ${result.findings.map(f => f.evidence).join('; ')}`);
assert.ok(miner.every(f => f.severity === 'high'), 'stratum finding is the high-severity miner rule');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it('fires the hacktool family on an offensive-tooling reference (SIG-HACKTOOL-001)', async () => {
const dir = mkdtempSync(join(tmpdir(), 'sig-hacktool-'));
try {
writeFileSync(join(dir, 'post.sh'), '#!/bin/sh\n# runs mimikatz sekurlsa::logonpasswords\n./mimikatz.exe\n');
resetCounter();
const discovery = await discoverFiles(dir);
const result = await scan(dir, discovery);
const ht = result.findings.filter(f => /\[hacktool\]/.test(f.evidence || ''));
assert.ok(ht.length >= 1, `expected a hacktool finding, got: ${result.findings.map(f => f.evidence).join('; ')}`);
assert.ok(ht.some(f => /SIG-HACKTOOL-001/.test(f.evidence)), 'should attribute to SIG-HACKTOOL-001');
assert.ok(ht.some(f => /mimikatz/i.test(`${f.title} ${f.description}`) || /hacktool/i.test(f.title)), 'finding should name the hacktool family');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
// ---------------------------------------------------------------------------
// Non-base64 decode variant (#59) — the SIG engine runs every rule against the
// rot13 variant too, so a rot13-obfuscated miner reference (invisible to a raw
// byte-matcher and to the base64 decode) must still be caught and flagged as
// recovered-from-obfuscation.
// ---------------------------------------------------------------------------
describe('signature-scanner: rot13 decode variant', () => {
it('catches a rot13-obfuscated cryptominer reference and marks it decoded', async () => {
const dir = mkdtempSync(join(tmpdir(), 'sig-rot13-'));
try {
// rot13 is its own inverse: writing rot13("...xmrig...") means the SIG
// rot13 variant decodes back to the plaintext miner reference. The raw
// bytes ("...kzevt...") match no signature.
const cipher = rot13('the xmrig payload is here');
assert.ok(!/xmrig/i.test(cipher), 'sanity: the raw ciphertext must not contain the plaintext token');
writeFileSync(join(dir, 'blob.txt'), cipher + '\n');
resetCounter();
const discovery = await discoverFiles(dir);
const result = await scan(dir, discovery);
const miner = result.findings.filter(f => /\[cryptominer\]/.test(f.evidence || ''));
assert.ok(miner.length >= 1, `expected a rot13-recovered cryptominer finding, got: ${result.findings.map(f => f.evidence).join('; ')}`);
assert.ok(
miner.some(f => /rot13/i.test(f.description) || /obfuscated/i.test(f.description)),
`finding should note it matched after rot13 decoding, got: ${miner.map(f => f.description).join(' | ')}`,
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
// ---------------------------------------------------------------------------
// Hygiene — must not flag its own ruleset / test / docs paths when scanning
// a project root that contains them