fix(audit-185): probe binary presence live instead of caching (Refs #243)

Follow-up to the negative-cache fix: caching positives is stale the
other way — a PATH shim's finally-restore leaves the cached hit behind
and the next gate run spawns a binary that is gone (CI: cli.test.ts
gates run failed with `Executable not found in $PATH: gitleaks`).
installed() is a live PATH probe now; a probe is microseconds and gate
runs call it a handful of times. The unit test pins both directions
(miss→shim→seen→removed→miss).

TDD: CI run 38105290150 is the red witness for the stale-positive
direction; gates-seeded.test.ts 13/13 green locally after.

Refs #185
This commit is contained in:
John Smith
2026-10-10 22:36:19 -04:00
parent 932041f95d
commit f459ebb667
2 changed files with 10 additions and 11 deletions
+6 -11
View File
@@ -8,21 +8,16 @@ import type { GateFinding } from "../schema/gates.ts"
import { parseJsonc } from "../util/jsonc.ts"
import { run } from "../util/proc.ts"
const which = new Map<string, boolean>()
/**
* Is `binary` an executable on PATH? Pure lookup (no shell), so no injection.
* Only positive answers are cached: a miss is re-probed every time, so a
* binary installed (or shimmed onto PATH) later in the process is seen.
* Caching a miss pinned Gate skips across PATH changes (#243: the gitleaks
* PATH shims in these tests read as absent once any earlier gate run cached
* the miss on a machine without the binary).
* Probed live on every call: caching the answer pinned Gate skips across
* PATH changes in both directions (#243 — a cached miss hid later shims;
* a cached hit outlived the shim's finally-restore and sent the gate at a
* binary that was already gone). A PATH probe is microseconds; gate runs
* call this a handful of times.
*/
export function installed(binary: string): boolean {
if (which.get(binary) === true) return true
const found = onPath(binary)
if (found) which.set(binary, true)
return found
return onPath(binary)
}
function onPath(binary: string): boolean {
+4
View File
@@ -325,6 +325,10 @@ describe("gitleaks shape and waiver glob edges", () => {
process.env.PATH = previous
await rm(shimdir, { recursive: true, force: true })
}
// And no stale positive survives the shim's removal: the next gate run
// must skip the scanner instead of spawning a binary that is gone (the
// cli.test.ts gates failure this exact staleness caused in CI).
expect(installed(missing)).toBe(false)
})
test("a rule+files glob waiver hides only what both globs match", async () => {