Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/proxy/processors/push-action/gitleaks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ function runCommand(
stderr: string;
}> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, { cwd, shell: true });
// No shell. With shell:true Node hands the whole command line to the
// platform shell, and cmd.exe neither treats ' as a quote nor leaves ^
// alone, so any argument carrying a space or a git ^ arrives corrupted.
const child = spawn(command, args, { cwd });

let stdout = '';
let stderr = '';
Expand Down Expand Up @@ -171,7 +174,7 @@ const exec = async (_req: Request, action: Action): Promise<Action> => {
'git',
// not using --no-merges to be sure we're scanning the diff
// only add ^ if the commitFrom isn't the repo's rootCommit
`--log-opts='--first-parent ${rootCommit === commitFrom ? rootCommit : `${commitFrom}^`}..${commitTo}'`,
`--log-opts=--first-parent ${rootCommit === commitFrom ? rootCommit : `${commitFrom}^`}..${commitTo}`,
].filter((v) => typeof v === 'string');
const gitleaks = await runCommand(workingDir, 'gitleaks', gitleaksArgs);

Expand Down
34 changes: 34 additions & 0 deletions test/processors/gitLeaks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,40 @@ describe('gitleaks', () => {
expect(result.steps[0].logs[2]).toContain('gitleaks - Gitleaks output: No leaks found');
});

it('should pass --log-opts as one argument, unquoted and without a shell', async () => {
// With shell:true, Node joins command and args into a single command line
// for the platform shell. cmd.exe does not treat ' as a quote and does
// treat ^ as an escape, so the POSIX-quoted form arrived as
// ["--log-opts='--first-parent", "abc123..def456'"]
// on Windows: split in two, and the ^ silently dropped. Dropping the ^
// narrows the revision range by one commit, so the first commit of the
// push goes unscanned.
vi.mocked(getAPIs).mockReturnValue({ gitleaks: { enabled: true } });

const mockChild = (exitCode: number, stdout: string, stderr: string) => ({
on: (event: string, cb: (exitCode: number) => void) => {
if (event === 'close') cb(exitCode);
return { stdout: { on: () => {} }, stderr: { on: () => {} } };
},
stdout: { on: (_: string, cb: (out: string) => void) => cb(stdout) },
stderr: { on: (_: string, cb: (err: string) => void) => cb(stderr) },
});

vi.mocked(spawn)
.mockReturnValueOnce(mockChild(0, 'rootcommit123', ''))
.mockReturnValueOnce(mockChild(0, '', 'No leaks found'));

await exec(req, action);

// call 0 is `git rev-list`, call 1 is `gitleaks`
const [command, args, opts] = vi.mocked(spawn).mock.calls[1];

expect(command).toBe('gitleaks');
expect(args).toContain('--log-opts=--first-parent abc123^..def456');
expect((args as string[]).some((arg) => arg.includes("'"))).toBe(false);
expect((opts as { shell?: boolean })?.shell).not.toBe(true);
});

it('should handle scan with findings', async () => {
vi.mocked(getAPIs).mockReturnValue({ gitleaks: { enabled: true } });

Expand Down