fix: complete P1 integration audit trail
This commit is contained in:
@@ -7,6 +7,7 @@ import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createServer, connect } from "node:net";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
CHECK_IDS,
|
||||
buildSafeEnvironment,
|
||||
installExternalFetchGuard,
|
||||
installNetworkGuard,
|
||||
negativeRequestEvidence,
|
||||
cleanupOwnedRun,
|
||||
createOwnedRun,
|
||||
@@ -164,6 +166,28 @@ test("injected failure executes once, retains a complete ordered diagnostic repo
|
||||
assert.equal(report.checks[4].error, "Not executed after earlier failure.");
|
||||
});
|
||||
|
||||
test("failed scenario retains partial request and response evidence with observed commands", async () => {
|
||||
const partial = {
|
||||
commands: ["git"],
|
||||
artifacts: [
|
||||
{ path: "requests/partial.json", sha256: "a".repeat(64) },
|
||||
{ path: "responses/partial.json", sha256: "b".repeat(64) },
|
||||
],
|
||||
};
|
||||
const checks = exactScenarios(async (id) => {
|
||||
if (id === CHECK_IDS[4]) {
|
||||
const error = new Error("HTTP scenario failed after response persistence");
|
||||
error.acceptancePartial = partial;
|
||||
throw error;
|
||||
}
|
||||
return {};
|
||||
});
|
||||
const results = await executeChecks({ checks });
|
||||
assert.deepEqual(results[4].commands, partial.commands);
|
||||
assert.deepEqual(results[4].artifacts, partial.artifacts);
|
||||
assert.equal(results[4].error, "Acceptance scenario failed safely.");
|
||||
});
|
||||
|
||||
test("executeChecks never repeats or executes after first failure but emits the exact check set", async () => {
|
||||
const calls = new Map();
|
||||
const result = await executeChecks({
|
||||
@@ -212,7 +236,7 @@ test("secret scanner excludes only the direct fixture-secrets subtree", async ()
|
||||
await mkdir(dirname(join(run.root, path)), { recursive: true });
|
||||
await writeFile(join(run.root, path), `prefix ${canary} suffix`);
|
||||
}
|
||||
const findings = await scanSecrets({ runRoot: run.root, forbiddenValues: [canary] });
|
||||
const findings = await scanSecrets({ runRoot: run.root, forbiddenValues: [canary], expectedGitRepositories: [] });
|
||||
assert.deepEqual(new Set(findings.map((finding) => finding.path)), new Set(paths));
|
||||
});
|
||||
|
||||
@@ -230,7 +254,7 @@ test("secret scanner examines reachable Git blobs, not just loose file bytes", a
|
||||
await execFileAsync("git", ["commit", "-m", "secret blob"], { cwd: gitRoot });
|
||||
await execFileAsync("git", ["rm", "secret.txt"], { cwd: gitRoot });
|
||||
await execFileAsync("git", ["commit", "-m", "remove worktree copy"], { cwd: gitRoot });
|
||||
const findings = await scanSecrets({ runRoot: run.root, forbiddenValues: [canary] });
|
||||
const findings = await scanSecrets({ runRoot: run.root, forbiddenValues: [canary], expectedGitRepositories: ["author"] });
|
||||
assert.equal(findings.some((finding) => finding.path.startsWith("git-object:")), true);
|
||||
});
|
||||
|
||||
@@ -250,12 +274,15 @@ test("command helper accepts only executable plus separate argv", async () => {
|
||||
await assert.rejects(runCommand({ executable: "/bin/echo", argv: "hello" }));
|
||||
await assert.rejects(runCommand({ executable: "/bin/echo", argv: [], shell: true }));
|
||||
await assert.rejects(runCommand({ executable: "git status; rm -rf /", argv: [] }));
|
||||
await assert.rejects(runCommand({ executable: "/tmp/git", argv: ["--version"] }), /command executable is not allowlisted/);
|
||||
await assert.rejects(runCommand({ executable: "tht", argv: ["config", "check"] }), /command executable is not allowlisted/);
|
||||
const repositoryRoot = await fakeRepository();
|
||||
const executable = join(repositoryRoot, "executable with spaces");
|
||||
await writeFile(executable, "#!/bin/sh\nprintf '%s' \"$1\"\n", { mode: 0o700 });
|
||||
await chmod(executable, 0o700);
|
||||
const result = await runCommand({ executable, argv: ["literal;not-a-shell"] });
|
||||
assert.equal(result.stdout, "literal;not-a-shell");
|
||||
await assert.rejects(runCommand({ executable, argv: ["literal;not-a-shell"] }), /command executable is not allowlisted/);
|
||||
const result = await runCommand({ executable: "git", argv: ["--version"] });
|
||||
assert.match(result.stdout, /^git version /);
|
||||
assert.equal(result.code, 0);
|
||||
});
|
||||
|
||||
@@ -320,3 +347,90 @@ test("public wrapper replaces ambient environment before invoking the runner", a
|
||||
assert.match(wrapper, /P1_ACCEPTANCE_FAIL_AT/);
|
||||
assert.doesNotMatch(wrapper, /export THT_BIN/);
|
||||
});
|
||||
|
||||
|
||||
test("network guard is installed globally, rejects non-loopback sockets, and permits one owned listener", async () => {
|
||||
const server = createServer((socket) => socket.end("ok"));
|
||||
await new Promise((resolvePromise, reject) => server.listen(0, "127.0.0.1", (error) => error ? reject(error) : resolvePromise()));
|
||||
const address = server.address();
|
||||
assert(address && typeof address === "object");
|
||||
const guard = installNetworkGuard();
|
||||
try {
|
||||
guard.addOwnedOrigin(`http://127.0.0.1:${address.port}`);
|
||||
const contents = await new Promise((resolvePromise, reject) => {
|
||||
const socket = connect({ host: "127.0.0.1", port: address.port });
|
||||
let value = "";
|
||||
socket.setEncoding("utf8");
|
||||
socket.on("data", (chunk) => { value += chunk; });
|
||||
socket.on("end", () => resolvePromise(value));
|
||||
socket.on("error", reject);
|
||||
});
|
||||
assert.equal(contents, "ok");
|
||||
assert.throws(() => connect({ host: "example.com", port: 80 }), /external network connection prohibited/);
|
||||
await assert.rejects(globalThis.fetch("https://example.com/"), /external network connection prohibited/);
|
||||
assert.equal(guard.externalAttempts.length, 2);
|
||||
} finally {
|
||||
guard.restore();
|
||||
await new Promise((resolvePromise) => server.close(resolvePromise));
|
||||
}
|
||||
});
|
||||
|
||||
test("report validation rejects duplicate artifact paths across checks", () => {
|
||||
const report = validReport();
|
||||
report.checks[1].artifacts[0].path = report.checks[0].artifacts[0].path;
|
||||
assert.throws(() => validateReport(report), /report artifact path is duplicated/);
|
||||
});
|
||||
|
||||
test("virtual report leakage yields a minimal sanitized exact-15 FAIL report", async () => {
|
||||
const repositoryRoot = await fakeRepository();
|
||||
const canary = "VIRTUAL-CANARY-12345678";
|
||||
const checks = exactScenarios(async (id) => ({
|
||||
commands: [],
|
||||
artifacts: id === CHECK_IDS[0] ? [{ path: `logs/${canary}.json`, sha256: "a".repeat(64) }] : [],
|
||||
}));
|
||||
const result = await runIntegration({
|
||||
repositoryRoot,
|
||||
checks,
|
||||
setup: async (_run, _repositoryRoot, _env, ctx) => {
|
||||
ctx.forbiddenValues = [canary];
|
||||
return ctx;
|
||||
},
|
||||
});
|
||||
assert.equal(result.exitCode, 1);
|
||||
const bytes = await readFile(join(result.runRoot, "report.json"));
|
||||
assert.equal(bytes.includes(Buffer.from(canary)), false);
|
||||
const report = JSON.parse(bytes);
|
||||
assert.deepEqual(report.checks.map(({ id }) => id), CHECK_IDS);
|
||||
assert(report.checks.every(({ status, commands, artifacts }) => status === "FAIL" && commands.length === 0 && artifacts.length === 0));
|
||||
});
|
||||
|
||||
test("partial setup preserves forbidden values and never writes secret-bearing report bytes", async () => {
|
||||
const repositoryRoot = await fakeRepository();
|
||||
const canary = "PARTIAL-SETUP-CANARY-12345678";
|
||||
const result = await runIntegration({
|
||||
repositoryRoot,
|
||||
setup: async (run, _repositoryRoot, _env, ctx) => {
|
||||
ctx.forbiddenValues = [canary];
|
||||
await mkdir(join(run.root, "logs"), { recursive: true });
|
||||
await writeFile(join(run.root, "logs", "partial-setup.log"), canary);
|
||||
throw new Error(`unsafe ${canary}`);
|
||||
},
|
||||
});
|
||||
assert.equal(result.exitCode, 1);
|
||||
const bytes = await readFile(join(result.runRoot, "report.json"));
|
||||
assert.equal(bytes.includes(Buffer.from(canary)), false);
|
||||
const report = JSON.parse(bytes);
|
||||
assert.equal(report.checks.length, 15);
|
||||
assert(report.checks.every(({ status }) => status === "FAIL"));
|
||||
});
|
||||
|
||||
test("secret scan fails closed when either expected Git repository is missing", async () => {
|
||||
for (const missing of ["remote.git", "author"]) {
|
||||
const repositoryRoot = await fakeRepository();
|
||||
const run = await createOwnedRun({ repositoryRoot });
|
||||
const present = missing === "remote.git" ? "author" : "remote.git";
|
||||
await mkdir(join(run.root, present));
|
||||
await execFileAsync("git", present === "remote.git" ? ["init", "--bare", join(run.root, present)] : ["init", join(run.root, present)]);
|
||||
await assert.rejects(scanSecrets({ runRoot: run.root, forbiddenValues: ["CANARY-value-123"] }), new RegExp(`missing expected Git repository: ${missing.replace(".", "\\.")}`));
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user