feat(llm-security-copilot): port llm-security v5.1.0 to GitHub Copilot CLI

Full port of llm-security plugin for internal use on Windows with GitHub
Copilot CLI. Protocol translation layer (copilot-hook-runner.mjs)
normalizes Copilot camelCase I/O to Claude Code snake_case format — all
original hook scripts run unmodified.

- 8 hooks with protocol translation (stdin/stdout/exit code)
- 18 SKILL.md skills (Agent Skills Open Standard)
- 6 .agent.md agent definitions
- 20 scanners + 14 scanner lib modules (unchanged)
- 14 knowledge files (unchanged)
- 39 test files including copilot-port-verify.mjs (17 tests)
- Windows-ready: node:path, os.tmpdir(), process.execPath, no bash

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-04-09 21:56:10 +02:00
commit f418a8fe08
169 changed files with 37631 additions and 0 deletions

View file

@ -0,0 +1,137 @@
// 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(', ')}`
);
});
});