ktg-plugin-marketplace/plugins/llm-security/tests/scanners/network.test.mjs

137 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 12 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(', ')}`
);
});
});