diff --git a/lib/gstack-memory-helpers.ts b/lib/gstack-memory-helpers.ts index c76a5f179..11a3f2239 100644 --- a/lib/gstack-memory-helpers.ts +++ b/lib/gstack-memory-helpers.ts @@ -19,7 +19,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, appendFileSync } from "fs"; import { dirname, join } from "path"; -import { execSync, execFileSync } from "child_process"; +import { execFileSync } from "child_process"; import { homedir } from "os"; // ── Types ────────────────────────────────────────────────────────────────── @@ -122,7 +122,11 @@ let _gitleaksAvailability: boolean | null = null; function gitleaksAvailable(): boolean { if (_gitleaksAvailability !== null) return _gitleaksAvailability; try { - execSync("command -v gitleaks", { stdio: "ignore" }); + execFileSync("gitleaks", ["version"], { + env: process.env, + stdio: "ignore", + timeout: 2_000, + }); _gitleaksAvailability = true; } catch { _gitleaksAvailability = false; @@ -157,7 +161,7 @@ export function secretScanFile(path: string): SecretScanResult { const out = execFileSync( "gitleaks", ["detect", "--no-git", "--source", path, "--report-format", "json", "--report-path", "/dev/stdout", "--exit-code", "0"], - { encoding: "utf-8", maxBuffer: 16 * 1024 * 1024 } + { encoding: "utf-8", env: process.env, maxBuffer: 16 * 1024 * 1024 } ); const trimmed = out.trim(); if (!trimmed) return { scanned: true, findings: [], scanner: "gitleaks" }; diff --git a/test/gstack-memory-helpers.test.ts b/test/gstack-memory-helpers.test.ts index f1d2bf379..886ea593d 100644 --- a/test/gstack-memory-helpers.test.ts +++ b/test/gstack-memory-helpers.test.ts @@ -12,7 +12,7 @@ */ import { describe, it, expect, beforeEach, afterAll } from "bun:test"; -import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync, mkdirSync } from "fs"; +import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync, mkdirSync, chmodSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; @@ -96,6 +96,47 @@ describe("secretScanFile", () => { } rmSync(dir, { recursive: true, force: true }); }); + + it("probes the gitleaks executable directly before scanning", () => { + const dir = mkdtempSync(join(tmpdir(), "gstack-test-")); + const binDir = join(dir, "bin"); + const log = join(dir, "gitleaks-calls.log"); + const file = join(dir, "clean.txt"); + mkdirSync(binDir, { recursive: true }); + writeFileSync(file, "no secrets here\n"); + writeFileSync( + join(binDir, "gitleaks"), + `#!/bin/sh +printf '%s\\n' "$*" >> "${log}" +if [ "$1" = "version" ]; then + exit 0 +fi +if [ "$1" = "detect" ]; then + echo '[]' + exit 0 +fi +exit 2 +`, + "utf-8", + ); + chmodSync(join(binDir, "gitleaks"), 0o755); + + const oldPath = process.env.PATH; + process.env.PATH = `${binDir}:${oldPath || ""}`; + try { + _resetGitleaksAvailabilityCache(); + const result = secretScanFile(file); + expect(result.scanner).toBe("gitleaks"); + expect(result.findings).toEqual([]); + const calls = readFileSync(log, "utf-8").trim().split("\n"); + expect(calls[0]).toBe("version"); + expect(calls[1]).toContain("detect --no-git --source"); + } finally { + if (oldPath === undefined) delete process.env.PATH; + else process.env.PATH = oldPath; + rmSync(dir, { recursive: true, force: true }); + } + }); }); // ── parseSkillManifest ─────────────────────────────────────────────────────