v8.1.0 S2. examples/poisoned-claude-md/fixture/ (CLAUDE.md with a base64 pipe-to-shell blob, plus an agent file) is now fixture.archive.json in the same format as the demo archive; run-memory-poisoning.mjs materializes it into a temp dir and deletes it on exit. README shows materialize-then-scan. payload-trees.test.mjs asserts byte identity for both archives. av-surface: b 6->5, d 1->0. Walkthrough output identical before/after (6 pass, 0 fail, 18 findings). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
63 lines
2.8 KiB
JavaScript
63 lines
2.8 KiB
JavaScript
// payload-trees.test.mjs — the materialized trees are byte-identical to the
|
|
// fixtures that used to sit on disk (sha256 measured before deletion, S1),
|
|
// and so are the encoded example archives (S2).
|
|
|
|
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
import { join, relative, basename, dirname, resolve } from 'node:path';
|
|
import { createHash } from 'node:crypto';
|
|
import { PAYLOAD_TREES, materializeTree } from './payload-trees.mjs';
|
|
import { DEMO_ARCHIVE, readArchive, materializeArchive } from '../../examples/malicious-skill-demo/materialize.mjs';
|
|
|
|
function walk(dir) {
|
|
return readdirSync(dir, { withFileTypes: true, recursive: true })
|
|
.filter(e => e.isFile())
|
|
.map(e => join(e.parentPath, e.name));
|
|
}
|
|
|
|
describe('payload-trees: materialized bytes match the retired on-disk fixtures', () => {
|
|
for (const name of Object.keys(PAYLOAD_TREES)) {
|
|
it(name, () => {
|
|
const { dir, cleanup } = materializeTree(name);
|
|
try {
|
|
const written = walk(dir).map(f => relative(dir, f)).sort();
|
|
assert.deepEqual(written, Object.keys(PAYLOAD_TREES[name]).sort(), 'file set changed');
|
|
for (const [rel, { sha256 }] of Object.entries(PAYLOAD_TREES[name])) {
|
|
const got = createHash('sha256').update(readFileSync(join(dir, rel))).digest('hex');
|
|
assert.equal(got, sha256, `${name}/${rel} differs from the retired fixture`);
|
|
}
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// S2 (2026-09-22): the example trees are encoded archives next to their
|
|
// examples; same check, sha256 measured from the retired on-disk files.
|
|
const EXAMPLE_ARCHIVES = {
|
|
'malicious-skill-demo/evil-project-health': DEMO_ARCHIVE,
|
|
'poisoned-claude-md/fixture': resolve(dirname(DEMO_ARCHIVE), '../poisoned-claude-md/fixture.archive.json'),
|
|
};
|
|
|
|
describe('example archives: materialized bytes match the retired on-disk trees', () => {
|
|
for (const [name, archivePath] of Object.entries(EXAMPLE_ARCHIVES)) {
|
|
it(name, () => {
|
|
const { files } = readArchive(archivePath);
|
|
const { dir, cleanup } = materializeArchive(archivePath);
|
|
try {
|
|
assert.equal(basename(dir), basename(name), 'leaf directory name changed');
|
|
const written = walk(dir).map(f => relative(dir, f)).sort();
|
|
assert.deepEqual(written, Object.keys(files).sort(), 'file set changed');
|
|
for (const [rel, { sha256 }] of Object.entries(files)) {
|
|
assert.match(sha256, /^[0-9a-f]{64}$/, `${name}/${rel} has no recorded sha256`);
|
|
const got = createHash('sha256').update(readFileSync(join(dir, rel))).digest('hex');
|
|
assert.equal(got, sha256, `${name}/${rel} differs from the retired fixture`);
|
|
}
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
});
|
|
}
|
|
});
|