137 lines
5.2 KiB
JavaScript
137 lines
5.2 KiB
JavaScript
// network.test.mjs — Integration tests for the network-mapper
|
||
// Tests against the evil-project-health fixture which contains URLs to:
|
||
// - ngrok-free.app (SUSPICIOUS_DOMAINS — HIGH)
|
||
// - webhook.site (SUSPICIOUS_DOMAINS — HIGH)
|
||
// - requestbin.com (SUSPICIOUS_DOMAINS — HIGH)
|
||
// - pipedream.net (SUSPICIOUS_DOMAINS — HIGH)
|
||
// - pastebin.com (SUSPICIOUS_DOMAINS — HIGH)
|
||
// - bit.ly (SUSPICIOUS_DOMAINS — HIGH, URL shortener)
|
||
// - 192.168.x.x (private IP — MEDIUM)
|
||
// - 45.33.32.156 (public IP — HIGH, bypasses DNS)
|
||
//
|
||
// We do NOT assert on DNS resolution — it is network-dependent.
|
||
// Only URL pattern detection (Phase 1–2 of the scanner) is tested.
|
||
|
||
import { describe, it, beforeEach } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { resolve } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { resetCounter } from '../../scanners/lib/output.mjs';
|
||
import { discoverFiles } from '../../scanners/lib/file-discovery.mjs';
|
||
import { scan } from '../../scanners/network-mapper.mjs';
|
||
|
||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||
const FIXTURE = resolve(__dirname, '../../examples/malicious-skill-demo/evil-project-health');
|
||
|
||
describe('network-mapper integration', () => {
|
||
let discovery;
|
||
|
||
beforeEach(async () => {
|
||
resetCounter();
|
||
discovery = await discoverFiles(FIXTURE);
|
||
});
|
||
|
||
it('returns status ok', async () => {
|
||
const result = await scan(FIXTURE, discovery);
|
||
assert.equal(result.status, 'ok', `Expected status 'ok', got '${result.status}'`);
|
||
});
|
||
|
||
it('scans at least one file', async () => {
|
||
const result = await scan(FIXTURE, discovery);
|
||
assert.ok(result.files_scanned >= 1, `Expected files_scanned >= 1, got ${result.files_scanned}`);
|
||
});
|
||
|
||
it('detects at least 2 suspicious domain findings', async () => {
|
||
const result = await scan(FIXTURE, discovery);
|
||
const suspiciousFindings = result.findings.filter(f => f.severity === 'high');
|
||
assert.ok(
|
||
suspiciousFindings.length >= 2,
|
||
`Expected >= 2 HIGH severity network findings, got ${suspiciousFindings.length}. ` +
|
||
`All findings: ${result.findings.map(f => `${f.severity}: ${f.title}`).join('; ')}`
|
||
);
|
||
});
|
||
|
||
it('reports total findings >= 2', async () => {
|
||
const result = await scan(FIXTURE, discovery);
|
||
assert.ok(
|
||
result.findings.length >= 2,
|
||
`Expected >= 2 network findings, got ${result.findings.length}`
|
||
);
|
||
});
|
||
|
||
it('detects ngrok-free.app as suspicious endpoint', async () => {
|
||
const result = await scan(FIXTURE, discovery);
|
||
const ngrokFinding = result.findings.find(
|
||
f => f.title.toLowerCase().includes('ngrok') ||
|
||
(f.evidence && f.evidence.toLowerCase().includes('ngrok'))
|
||
);
|
||
assert.ok(
|
||
ngrokFinding,
|
||
`Should detect ngrok-free.app. All titles: ${result.findings.map(f => f.title).join('; ')}`
|
||
);
|
||
});
|
||
|
||
it('detects webhook.site as suspicious endpoint', async () => {
|
||
const result = await scan(FIXTURE, discovery);
|
||
const webhookFinding = result.findings.find(
|
||
f => f.title.toLowerCase().includes('webhook.site') ||
|
||
(f.evidence && f.evidence.toLowerCase().includes('webhook.site'))
|
||
);
|
||
assert.ok(
|
||
webhookFinding,
|
||
`Should detect webhook.site. All titles: ${result.findings.map(f => f.title).join('; ')}`
|
||
);
|
||
});
|
||
|
||
it('suspicious domain findings reference owasp LLM02', async () => {
|
||
const result = await scan(FIXTURE, discovery);
|
||
const suspiciousFindings = result.findings.filter(
|
||
f => f.severity === 'high' && f.title.toLowerCase().includes('suspicious')
|
||
);
|
||
for (const f of suspiciousFindings) {
|
||
assert.equal(
|
||
f.owasp, 'LLM02',
|
||
`Suspicious domain finding ${f.id} should be LLM02, got ${f.owasp}`
|
||
);
|
||
}
|
||
});
|
||
|
||
it('all findings have DS-NET- prefix', async () => {
|
||
const result = await scan(FIXTURE, discovery);
|
||
const wrongPrefix = result.findings.filter(f => !f.id.startsWith('DS-NET-'));
|
||
assert.equal(
|
||
wrongPrefix.length, 0,
|
||
`All network findings should have DS-NET- prefix. Wrong: ${wrongPrefix.map(f => f.id).join(', ')}`
|
||
);
|
||
});
|
||
|
||
it('finding IDs start from DS-NET-001 after reset', async () => {
|
||
const result = await scan(FIXTURE, discovery);
|
||
if (result.findings.length === 0) return;
|
||
assert.equal(result.findings[0].id, 'DS-NET-001');
|
||
});
|
||
|
||
it('counts total matches findings array length', async () => {
|
||
const result = await scan(FIXTURE, discovery);
|
||
const countTotal = Object.values(result.counts).reduce((s, n) => s + n, 0);
|
||
assert.equal(
|
||
countTotal, result.findings.length,
|
||
`counts total (${countTotal}) should match findings.length (${result.findings.length})`
|
||
);
|
||
});
|
||
|
||
it('does not emit findings for trusted domains (github.com, anthropic.com)', async () => {
|
||
const result = await scan(FIXTURE, discovery);
|
||
const trustedDomainFindings = result.findings.filter(
|
||
f => (f.evidence && (
|
||
f.evidence.includes('github.com') ||
|
||
f.evidence.includes('anthropic.com') ||
|
||
f.evidence.includes('npmjs.org')
|
||
))
|
||
);
|
||
assert.equal(
|
||
trustedDomainFindings.length, 0,
|
||
`Should not flag trusted domains. Found: ${trustedDomainFindings.map(f => f.evidence).join(', ')}`
|
||
);
|
||
});
|
||
});
|