From 08e8501d7b411f45666d5b8064bc2e71e5116be2 Mon Sep 17 00:00:00 2001 From: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:46:09 +0000 Subject: [PATCH] fix(ci): redact all retained dependency evidence at one boundary --- scripts/lib/dependency-security.mjs | 38 ++++++++-------- .../filesystem/dependencySecurity.test.ts | 43 ++++++++++++++++++- 2 files changed, 61 insertions(+), 20 deletions(-) diff --git a/scripts/lib/dependency-security.mjs b/scripts/lib/dependency-security.mjs index 5906adeb08..97582402d8 100644 --- a/scripts/lib/dependency-security.mjs +++ b/scripts/lib/dependency-security.mjs @@ -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( diff --git a/tests/boundary/filesystem/dependencySecurity.test.ts b/tests/boundary/filesystem/dependencySecurity.test.ts index 9dfe9fbbe6..9f4a4ca414 100644 --- a/tests/boundary/filesystem/dependencySecurity.test.ts +++ b/tests/boundary/filesystem/dependencySecurity.test.ts @@ -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 = { 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");