mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-16 21:25:42 +02:00
142 lines
4.4 KiB
JavaScript
142 lines
4.4 KiB
JavaScript
/**
|
|
* Executes the workspace_path() example from the agent-security-hardening
|
|
* skill so the documented guard is checked as code, not only as prose.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const { spawnSync } = require("child_process");
|
|
|
|
const REPO_ROOT = path.join(__dirname, "..", "..");
|
|
const SKILL_PATH = path.join(REPO_ROOT, "skills", "agent-security-hardening", "SKILL.md");
|
|
|
|
function findPython() {
|
|
for (const candidate of ["python3", "python"]) {
|
|
const probe = spawnSync(candidate, ["-c", "import sys; print(sys.version_info >= (3, 9))"], {
|
|
encoding: "utf8",
|
|
});
|
|
if (probe.status === 0 && probe.stdout.trim() === "True") return candidate;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function workspacePathSnippet() {
|
|
const skill = fs.readFileSync(SKILL_PATH, "utf8");
|
|
const blocks = [...skill.matchAll(/```python\n([\s\S]*?)```/g)].map(match => match[1]);
|
|
const snippet = blocks.find(block => block.includes("def workspace_path("));
|
|
assert.ok(snippet, "workspace_path() python example missing from SKILL.md");
|
|
return snippet;
|
|
}
|
|
|
|
const HARNESS = `
|
|
import json, sys
|
|
from pathlib import Path
|
|
namespace = {}
|
|
exec(sys.stdin.read(), namespace)
|
|
workspace_path = namespace["workspace_path"]
|
|
workspace = Path(sys.argv[1])
|
|
results = {}
|
|
for requested in json.loads(sys.argv[2]):
|
|
try:
|
|
workspace_path(workspace, requested)
|
|
results[requested] = "accepted"
|
|
except ValueError:
|
|
results[requested] = "rejected"
|
|
print(json.dumps(results))
|
|
`;
|
|
|
|
function classify(python, requests) {
|
|
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-workspace-path-"));
|
|
try {
|
|
const result = spawnSync(python, ["-c", HARNESS, workspace, JSON.stringify(requests)], {
|
|
input: workspacePathSnippet(),
|
|
encoding: "utf8",
|
|
});
|
|
assert.strictEqual(result.status, 0, `python harness failed:\n${result.stderr}`);
|
|
return JSON.parse(result.stdout);
|
|
} finally {
|
|
fs.rmSync(workspace, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
const tests = [];
|
|
function test(name, fn) { tests.push([name, fn]); }
|
|
|
|
const python = findPython();
|
|
|
|
test("accepts ordinary nested and look-alike workspace paths", () => {
|
|
if (!python) return { skipped: "python >= 3.9 not available" };
|
|
const requests = ["reports/q1.txt", "reports\\q1.txt", "console.txt", "CONFIG", "com10.txt", "nullable.md", ".env"];
|
|
const results = classify(python, requests);
|
|
for (const requested of requests) {
|
|
assert.strictEqual(results[requested], "accepted", `${requested} should be accepted`);
|
|
}
|
|
});
|
|
|
|
test("rejects traversal and absolute paths", () => {
|
|
if (!python) return { skipped: "python >= 3.9 not available" };
|
|
const requests = ["../outside.txt", "nested/../../outside.txt", "..\\outside.txt", "nested\\..\\..\\outside.txt", "\\\\server\\share\\file.txt", path.resolve(os.tmpdir(), "abs.txt")];
|
|
const results = classify(python, requests);
|
|
for (const requested of requests) {
|
|
assert.strictEqual(results[requested], "rejected", `${requested} should be rejected`);
|
|
}
|
|
});
|
|
|
|
test("rejects Windows reserved device names, trailing dots or spaces, and stream syntax", () => {
|
|
if (!python) return { skipped: "python >= 3.9 not available" };
|
|
const requests = [
|
|
"CON",
|
|
"nul",
|
|
"nul.txt",
|
|
"NUL.tar.gz",
|
|
"aux",
|
|
"prn.log",
|
|
"com1",
|
|
"COM9.txt",
|
|
"lpt1",
|
|
"LPT9.log",
|
|
"COM¹",
|
|
"lpt³.txt",
|
|
"sub/CON/file.txt",
|
|
"sub\\CON\\file.txt",
|
|
"notes.",
|
|
"notes ",
|
|
"dir./file.txt",
|
|
"report.txt:ads",
|
|
"report.txt::$DATA",
|
|
"C:\\Windows\\system32",
|
|
"C:/Windows/system32",
|
|
];
|
|
const results = classify(python, requests);
|
|
for (const requested of requests) {
|
|
assert.strictEqual(results[requested], "rejected", `${JSON.stringify(requested)} should be rejected`);
|
|
}
|
|
});
|
|
|
|
let failed = 0;
|
|
let skipped = 0;
|
|
console.log("\n=== Testing agent-security-hardening skill ===\n");
|
|
for (const [name, fn] of tests) {
|
|
try {
|
|
const result = fn();
|
|
if (result?.skipped) {
|
|
skipped += 1;
|
|
console.log(` - SKIP ${name}: ${result.skipped}`);
|
|
continue;
|
|
}
|
|
console.log(` ✓ ${name}`);
|
|
} catch (error) {
|
|
failed += 1;
|
|
console.log(` ✗ ${name}`);
|
|
console.error(` ${error.message}`);
|
|
}
|
|
}
|
|
console.log(`\nPassed: ${tests.length - failed - skipped}`);
|
|
console.log(`Skipped: ${skipped}`);
|
|
console.log(`Failed: ${failed}`);
|
|
process.exitCode = failed ? 1 : 0;
|