fix(ci): redact all retained dependency evidence at one boundary

This commit is contained in:
gensokyo-dev
2026-10-23 13:46:09 +00:00
parent 95eb175457
commit 08e8501d7b
2 changed files with 61 additions and 20 deletions
+19 -19
View File
@@ -63,6 +63,13 @@ export async function checkDependencySecurity(
directory = resolve(directory);
await mkdir(evidenceDirectory, { recursive: true, mode: 0o700 });
const evidence = await mkdtemp(join(resolve(evidenceDirectory), "run-"));
// Keep parsed facts intact. Every retained representation crosses this one
// authentication-redaction boundary, including partial/failed graph facts.
const retain = async (name, source) => {
const rendered = redactTransportAuthentication(source);
await writeFile(join(evidence, name), rendered);
return digest(rendered);
};
const report = {
schema_version: 1,
checks_passed: false,
@@ -93,10 +100,7 @@ export async function checkDependencySecurity(
};
const finish = async () => {
report.completed_at = new Date().toISOString();
await writeFile(
join(evidence, "report.json"),
JSON.stringify(report, null, 2) + "\n",
);
await retain("report.json", JSON.stringify(report, null, 2) + "\n");
return report;
};
let reference;
@@ -122,11 +126,10 @@ export async function checkDependencySecurity(
selectedLockSha = digest(selectedBytes);
installedPackages = lockedPackages(JSON.parse(selectedBytes));
report.selected_lock_sha256 = selectedLockSha;
const retainedLock = redactTransportAuthentication(
report.retained_lock_sha256 = await retain(
"selected-package-lock.json",
selectedBytes.toString("utf8"),
);
await writeFile(join(evidence, "selected-package-lock.json"), retainedLock);
report.retained_lock_sha256 = digest(retainedLock);
const product = JSON.parse(
await readFile(
join(
@@ -180,17 +183,17 @@ export async function checkDependencySecurity(
: Buffer.from(value)
.subarray(0, SECURITY_OUTPUT_BYTES)
.toString("utf8");
const stdout = redactTransportAuthentication(bounded(result.stdout)),
stderr = redactTransportAuthentication(bounded(result.stderr));
await writeFile(join(evidence, `${name}.${json ? "json" : "txt"}`), stdout);
await writeFile(join(evidence, `${name}.stderr.txt`), stderr);
const stdout = bounded(result.stdout),
stderr = bounded(result.stderr);
const stdoutHash = await retain(`${name}.${json ? "json" : "txt"}`, stdout);
const stderrHash = await retain(`${name}.stderr.txt`, stderr);
report.commands.push({
name,
exit_code: result.exitCode,
elapsed_ms: result.elapsedMs,
failure: result.failure,
stdout_sha256: digest(stdout),
stderr_sha256: digest(stderr),
stdout_sha256: stdoutHash,
stderr_sha256: stderrHash,
});
let value = null;
try {
@@ -290,12 +293,9 @@ export async function checkDependencySecurity(
reference,
mode === "consumer",
);
await writeFile(
join(evidence, "tree-facts.json"),
JSON.stringify(graph, null, 2) + "\n",
);
await writeFile(
join(evidence, "source-identity-differences.json"),
await retain("tree-facts.json", JSON.stringify(graph, null, 2) + "\n");
await retain(
"source-identity-differences.json",
JSON.stringify(differences, null, 2) + "\n",
);
const byteDifferences = differences.filter(
@@ -236,6 +236,7 @@ async function scanFixture(
changedVersion?: boolean;
changedIntegrity?: boolean;
mirror?: boolean;
authenticated?: boolean;
tarball?: boolean;
} = {},
) {
@@ -272,6 +273,8 @@ async function scanFixture(
if (options.changedIntegrity)
a.integrity = `sha512-${Buffer.alloc(64, 2).toString("base64")}`;
if (options.mirror) a.resolved = "https://mirror.example/a.tgz";
if (options.authenticated)
a.resolved = `HTTPS://user:fixture-secret@registry.npmjs.org/a/-/a-${a.version}.tgz`;
const dependencies: Record<string, unknown> = { a };
let qualifiedTarball: string | undefined;
if (options.consumer) {
@@ -303,6 +306,12 @@ async function scanFixture(
),
}),
);
const reported = structuredClone(dependencies);
if (options.authenticated) {
const selected = reported.a as { resolved?: string; integrity?: string };
delete selected.resolved;
delete selected.integrity;
}
const results = new Map([
[
"npm-version",
@@ -322,7 +331,7 @@ async function scanFixture(
name: "rea-agents",
version: "6.7.0",
path: directory,
dependencies,
dependencies: reported,
}),
],
]);
@@ -453,6 +462,38 @@ it("binds the local consumer exception to the selected archive's SHA-512 bytes",
expect((await fixture.scan()).checks_passed).toBe(false);
});
it("redacts raw lock credentials from every retained partial consumer representation", async () => {
const fixture = await scanFixture({
consumer: true,
authenticated: true,
changedVersion: true,
});
const report = await fixture.scan();
expect(report.checks_passed).toBe(false);
expect(report.tree.status).toBe("unavailable");
for (const name of [
"selected-package-lock.json",
"tree-facts.json",
"source-identity-differences.json",
"report.json",
"installed-tree.json",
]) {
const retained = await readFile(
join(report.evidence_directory, name),
"utf8",
);
expect(retained).not.toContain("fixture-secret");
if (
[
"selected-package-lock.json",
"tree-facts.json",
"source-identity-differences.json",
].includes(name)
)
expect(retained).toContain("HTTPS://registry.npmjs.org/a/-/a-2.0.0.tgz");
}
});
it("uses the selected lock for declared integrity when npm reads uncached package metadata", () => {
const directory = "/tmp/project";
const complete = packageNode(directory, "a");