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:
parent
901bf0ae12
commit
f418a8fe08
169 changed files with 37631 additions and 0 deletions
108
plugins/llm-security-copilot/tests/scanners/unicode.test.mjs
Normal file
108
plugins/llm-security-copilot/tests/scanners/unicode.test.mjs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// unicode.test.mjs — Integration tests for the unicode-scanner
|
||||
// Tests against the evil-project-health fixture which contains:
|
||||
// - Zero-width characters in SKILL.fixture.md
|
||||
// - Unicode Tag block codepoints (steganographic hidden message) in SKILL.fixture.md
|
||||
// - BIDI override characters in SKILL.fixture.md
|
||||
|
||||
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/unicode-scanner.mjs';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const FIXTURE = resolve(__dirname, '../../examples/malicious-skill-demo/evil-project-health');
|
||||
|
||||
describe('unicode-scanner 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 zero-width characters (CRITICAL or HIGH)', async () => {
|
||||
const result = await scan(FIXTURE, discovery);
|
||||
const zeroWidthFindings = result.findings.filter(f =>
|
||||
(f.severity === 'critical' || f.severity === 'high') &&
|
||||
(
|
||||
f.title.toLowerCase().includes('zero-width') ||
|
||||
f.title.toLowerCase().includes('zero width') ||
|
||||
(f.evidence && f.evidence.toLowerCase().includes('u+200'))
|
||||
)
|
||||
);
|
||||
assert.ok(
|
||||
zeroWidthFindings.length >= 1,
|
||||
`Expected at least 1 zero-width finding, got ${zeroWidthFindings.length}. ` +
|
||||
`All findings: ${result.findings.map(f => f.title).join('; ')}`
|
||||
);
|
||||
});
|
||||
|
||||
it('detects Unicode Tag block codepoints (CRITICAL) — steganographic hidden message', async () => {
|
||||
const result = await scan(FIXTURE, discovery);
|
||||
const tagFindings = result.findings.filter(f =>
|
||||
f.severity === 'critical' &&
|
||||
f.title.toLowerCase().includes('unicode tag')
|
||||
);
|
||||
assert.ok(
|
||||
tagFindings.length >= 1,
|
||||
`Expected at least 1 Unicode Tag finding (CRITICAL), got ${tagFindings.length}. ` +
|
||||
`All findings: ${result.findings.map(f => f.title).join('; ')}`
|
||||
);
|
||||
});
|
||||
|
||||
it('reports at least 3 total findings across all categories', async () => {
|
||||
const result = await scan(FIXTURE, discovery);
|
||||
assert.ok(
|
||||
result.findings.length >= 3,
|
||||
`Expected >= 3 total unicode findings, got ${result.findings.length}`
|
||||
);
|
||||
});
|
||||
|
||||
it('assigns correct scanner prefix UNI to all findings', async () => {
|
||||
const result = await scan(FIXTURE, discovery);
|
||||
const wrongPrefix = result.findings.filter(f => !f.id.startsWith('DS-UNI-'));
|
||||
assert.equal(
|
||||
wrongPrefix.length, 0,
|
||||
`All findings should have DS-UNI- prefix. Wrong: ${wrongPrefix.map(f => f.id).join(', ')}`
|
||||
);
|
||||
});
|
||||
|
||||
it('finding IDs are sequential starting from DS-UNI-001', async () => {
|
||||
const result = await scan(FIXTURE, discovery);
|
||||
if (result.findings.length === 0) return;
|
||||
assert.equal(result.findings[0].id, 'DS-UNI-001');
|
||||
});
|
||||
|
||||
it('all findings have required fields', async () => {
|
||||
const result = await scan(FIXTURE, discovery);
|
||||
for (const f of result.findings) {
|
||||
assert.ok(f.id, `Finding missing id`);
|
||||
assert.ok(f.scanner, `Finding ${f.id} missing scanner`);
|
||||
assert.ok(f.severity, `Finding ${f.id} missing severity`);
|
||||
assert.ok(f.title, `Finding ${f.id} missing title`);
|
||||
assert.ok(f.owasp, `Finding ${f.id} missing owasp`);
|
||||
}
|
||||
});
|
||||
|
||||
it('counts object reflects actual findings array', 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 equal findings.length (${result.findings.length})`
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue