diff --git a/scanner/package.json b/scanner/package.json index a71b58fe..9ebe124a 100644 --- a/scanner/package.json +++ b/scanner/package.json @@ -6,6 +6,7 @@ "type": "module", "scripts": { "build": "tsc", + "test": "node --import tsx --test src/websocket-patterns.test.ts", "start": "node dist/index.js", "dev": "tsx watch src/index.ts", "scan": "tsx src/cli.ts" diff --git a/scanner/src/patterns.ts b/scanner/src/patterns.ts index f2e61e23..f39b8e62 100644 --- a/scanner/src/patterns.ts +++ b/scanner/src/patterns.ts @@ -141,6 +141,30 @@ export const DANGEROUS_PATTERNS: Pattern[] = [ pattern: /dns\.resolve|dns\.lookup.*[+`$]/gi, category: 'network' }, + { + id: 'NET_WEBSOCKET_INSECURE', + name: 'Insecure or Suspicious WebSocket Endpoint', + description: 'WebSocket connection uses plaintext transport or a commonly abused tunnel endpoint', + severity: 'high', + pattern: /(?:new\s+)?WebSocket\s*\(\s*['"`](?:ws:\/\/|wss:\/\/[^'"`]*(?:ngrok(?:-free)?\.app|ngrok\.io|requestbin|pipedream|webhook\.site))/gi, + category: 'network' + }, + { + id: 'EXFIL_WEBSOCKET_SEND', + name: 'Sensitive Data Sent over WebSocket', + description: 'WebSocket send includes credentials, environment variables, or file contents', + severity: 'critical', + pattern: /\.\s*send\s*\(\s*(?:JSON\.stringify\s*\([^)]*)?(?:process\.env|readFile(?:Sync)?\s*\(|credentials?|private[_-]?key|secret|token)/gi, + category: 'exfiltration' + }, + { + id: 'EXEC_WEBSOCKET_MESSAGE', + name: 'Command Execution from WebSocket Message', + description: 'WebSocket message handler passes remote data to a command execution function', + severity: 'critical', + pattern: /\.on\s*\(\s*['"`]message['"`]\s*,[\s\S]{0,300}\b(?:exec|execSync|spawn|spawnSync)\s*\(/gi, + category: 'code_execution' + }, // === MEDIUM: Obfuscation === { diff --git a/scanner/src/websocket-patterns.test.ts b/scanner/src/websocket-patterns.test.ts new file mode 100644 index 00000000..55908883 --- /dev/null +++ b/scanner/src/websocket-patterns.test.ts @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { analyzeContent } from './analyzer.js'; + +function findingIds(content: string): string[] { + return analyzeContent(content).findings.map(finding => finding.patternId); +} + +test('detects plaintext WebSocket endpoints', () => { + assert.ok(findingIds("const socket = new WebSocket('ws://example.com/control')").includes('NET_WEBSOCKET_INSECURE')); +}); + +test('detects commonly abused WebSocket tunnel endpoints', () => { + assert.ok(findingIds("const socket = new WebSocket('wss://agent.ngrok-free.app/ws')").includes('NET_WEBSOCKET_INSECURE')); +}); + +test('allows ordinary secure WebSocket endpoints', () => { + assert.ok(!findingIds("const socket = new WebSocket('wss://example.com/events')").includes('NET_WEBSOCKET_INSECURE')); +}); + +test('detects environment variable exfiltration over WebSocket', () => { + assert.ok(findingIds('socket.send(process.env.API_TOKEN)').includes('EXFIL_WEBSOCKET_SEND')); +}); + +test('detects file content exfiltration over WebSocket', () => { + assert.ok(findingIds("socket.send(readFileSync('/tmp/data'))").includes('EXFIL_WEBSOCKET_SEND')); +}); + +test('detects command execution in a WebSocket message handler', () => { + const content = "socket.on('message', command => { exec(command) })"; + assert.ok(findingIds(content).includes('EXEC_WEBSOCKET_MESSAGE')); +}); + +test('allows WebSocket message handlers without command execution', () => { + const content = "socket.on('message', message => { console.log(message) })"; + assert.ok(!findingIds(content).includes('EXEC_WEBSOCKET_MESSAGE')); +});