v8.1.0 S2. examples/malicious-skill-demo/evil-project-health/ (7 files, 30 Unicode Tag chars, a base64 exfil blob) is now one archive, evil-project-health.archive.json: rot13 text, every codepoint above U+007E stored as a number, sha256 of each retired file recorded. materialize.mjs writes it to a temp dir (CLI prints the path); run-demo.sh materializes and deletes it itself; the six scanner tests that scanned the tree use it. payload-trees.test.mjs asserts byte identity (mutation-checked). av-surface: b 8->6, c 1->0, d 2->1. Demo 13/13 before and after. All scanners report identical findings except git-forensics: it used to scan this repository's own history (21 findings, none about the demo) and now reports skipped in a temp dir, which git.test.mjs already accepts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
99 lines
3.9 KiB
JavaScript
99 lines
3.9 KiB
JavaScript
// permission.test.mjs — Integration tests for the permission-mapper
|
|
// Tests against the evil-project-health fixture which has plugin.fixture.json.
|
|
//
|
|
// The scanner's isPlugin() checks:
|
|
// 1. .claude-plugin/plugin.json
|
|
// 2. plugin.json
|
|
// 3. plugin.fixture.json ← evil-project-health has this
|
|
//
|
|
// So the fixture IS detected as a plugin and the scanner should return status 'ok'.
|
|
|
|
import { describe, it, beforeEach, after } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { resolve } from 'node:path';
|
|
import { resetCounter } from '../../scanners/lib/output.mjs';
|
|
import { scan } from '../../scanners/permission-mapper.mjs';
|
|
import { materializeArchive } from '../../examples/malicious-skill-demo/materialize.mjs';
|
|
|
|
// v8.1.0 (S2): the demo tree is stored encoded and materialized at test time.
|
|
const { dir: FIXTURE, cleanup } = materializeArchive();
|
|
after(cleanup);
|
|
|
|
describe('permission-mapper integration', () => {
|
|
beforeEach(() => {
|
|
resetCounter();
|
|
});
|
|
|
|
it('returns status ok or skipped (graceful handling)', async () => {
|
|
// The fixture has plugin.fixture.json, which the scanner recognises.
|
|
// If the scanner detects it as a plugin → 'ok'.
|
|
// If the scanner does not recognise .fixture.json → 'skipped' (also valid).
|
|
const result = await scan(FIXTURE, { files: [] });
|
|
const validStatuses = ['ok', 'skipped'];
|
|
assert.ok(
|
|
validStatuses.includes(result.status),
|
|
`Expected status 'ok' or 'skipped', got '${result.status}'`
|
|
);
|
|
});
|
|
|
|
it('returns ok and finds permission issues when plugin is detected', async () => {
|
|
const result = await scan(FIXTURE, { files: [] });
|
|
|
|
if (result.status === 'skipped') {
|
|
// Scanner does not recognise .fixture.json suffix — acceptable, skip checks
|
|
assert.equal(result.findings.length, 0, 'skipped result should have 0 findings');
|
|
return;
|
|
}
|
|
|
|
// Status is 'ok' — fixture contains components with tool mismatches
|
|
assert.equal(result.status, 'ok');
|
|
// The SKILL.fixture.md has Read, Glob, Grep, Bash, Write, WebFetch with scan/audit intent
|
|
// Expect at least 1 finding (purpose-tools mismatch or dangerous combo)
|
|
assert.ok(
|
|
result.findings.length >= 1,
|
|
`Expected >= 1 permission finding, got ${result.findings.length}`
|
|
);
|
|
});
|
|
|
|
it('all findings have DS-PRM- prefix when present', async () => {
|
|
const result = await scan(FIXTURE, { files: [] });
|
|
if (result.status === 'skipped') return;
|
|
|
|
const wrongPrefix = result.findings.filter(f => !f.id.startsWith('DS-PRM-'));
|
|
assert.equal(
|
|
wrongPrefix.length, 0,
|
|
`All permission findings should have DS-PRM- prefix. Wrong: ${wrongPrefix.map(f => f.id).join(', ')}`
|
|
);
|
|
});
|
|
|
|
it('reports owasp LLM06 for permission findings', async () => {
|
|
const result = await scan(FIXTURE, { files: [] });
|
|
if (result.status === 'skipped') return;
|
|
|
|
for (const f of result.findings) {
|
|
assert.equal(f.owasp, 'LLM06', `Finding ${f.id} should be OWASP LLM06, got ${f.owasp}`);
|
|
}
|
|
});
|
|
|
|
it('scanner name is permission-mapper', async () => {
|
|
const result = await scan(FIXTURE, { files: [] });
|
|
assert.equal(result.scanner, 'PRM');
|
|
});
|
|
|
|
it('counts object keys contain critical, high, medium, low, info', async () => {
|
|
const result = await scan(FIXTURE, { files: [] });
|
|
const expectedKeys = ['critical', 'high', 'medium', 'low', 'info'];
|
|
for (const key of expectedKeys) {
|
|
assert.ok(key in result.counts, `counts should have key '${key}'`);
|
|
}
|
|
});
|
|
|
|
it('non-plugin directory returns skipped with no findings', async () => {
|
|
// Use the lib/ subdirectory which has no plugin structure
|
|
const libDir = resolve(FIXTURE, 'lib');
|
|
resetCounter();
|
|
const result = await scan(libDir, { files: [] });
|
|
assert.equal(result.status, 'skipped', `Expected skipped for non-plugin dir, got ${result.status}`);
|
|
assert.equal(result.findings.length, 0, 'Non-plugin dir should produce 0 findings');
|
|
});
|
|
});
|