diff --git a/.changeset/semgrep-complete-diff-input.md b/.changeset/semgrep-complete-diff-input.md new file mode 100644 index 0000000..b00eefb --- /dev/null +++ b/.changeset/semgrep-complete-diff-input.md @@ -0,0 +1,5 @@ +--- +"layne": patch +--- + +Run Semgrep on complete selected HEAD files in diff-only mode, preserving syntax and enclosing context while filtering reported findings to changed lines. diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 561fdf8..053242e 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: '22' - - run: rm -f package-lock.json && npm install + - run: npm ci - run: npm run build - run: npm run lint - run: npm run validate-config diff --git a/src/__tests__/dispatcher.test.ts b/src/__tests__/dispatcher.test.ts index f44b37a..8293d05 100644 --- a/src/__tests__/dispatcher.test.ts +++ b/src/__tests__/dispatcher.test.ts @@ -81,7 +81,7 @@ describe('dispatch()', () => { })); }); - it('passes scanFiles and scanWorkspacePath to the semgrep adapter', async () => { + it('passes scanFiles and repoWorkspacePath to the semgrep adapter', async () => { await dispatch(BASE); expect(runSemgrep).toHaveBeenCalledWith(expect.objectContaining({ workspacePath: '/tmp/ws', @@ -89,6 +89,19 @@ describe('dispatch()', () => { })); }); + it('preserves full-file Semgrep context while Trufflehog scans projected hunks', async () => { + await dispatch({ + ...BASE, + scanContext: { ...BASE_SCAN_CONTEXT, mode: 'diff_only', scanWorkspacePath: '/tmp/ws/.layne/diff-only' }, + }); + expect(runSemgrep).toHaveBeenCalledWith(expect.objectContaining({ + workspacePath: '/tmp/ws', changedFiles: BASE_SCAN_CONTEXT.scanFiles, + })); + expect(runTrufflehog).toHaveBeenCalledWith(expect.objectContaining({ + workspacePath: '/tmp/ws/.layne/diff-only', changedFiles: BASE_SCAN_CONTEXT.scanFiles, + })); + }); + it('returns an empty array when all adapters return no findings', async () => { const findings = await dispatch(BASE); expect(findings).toEqual([]); diff --git a/src/dispatcher.ts b/src/dispatcher.ts index 7d2bf19..059e67f 100644 --- a/src/dispatcher.ts +++ b/src/dispatcher.ts @@ -42,7 +42,8 @@ export async function dispatch({ scanContext, changedLineRanges, owner, repo }: const [trufflehogFindings, semgrepFindings, claudeFindings, spectreFindings, depDoctorFindings] = await Promise.all([ runTrufflehog({ workspacePath: scanWorkspacePath, changedFiles: eligibleFiles, toolConfig: scanConfig.trufflehog }), - runSemgrep({ workspacePath: scanWorkspacePath, changedFiles: eligibleFiles, toolConfig: scanConfig.semgrep }), + // Semgrep needs complete syntax; the worker filters findings to changed lines. + runSemgrep({ workspacePath: repoWorkspacePath, changedFiles: eligibleFiles, toolConfig: scanConfig.semgrep }), runClaude({ workspacePath: repoWorkspacePath, changedFiles: eligibleFiles, changedLineRanges, promptFiles, toolConfig: scanConfig.claude }), runSpectre({ workspacePath: repoWorkspacePath, changedFiles: eligibleFiles, changedLineRanges, promptFiles, toolConfig: scanConfig.spectre }), runDepDoctor({ workspacePath: repoWorkspacePath, changedFiles: eligibleFiles, baseSha, toolConfig: scanConfig.depDoctor }), diff --git a/website/docs/scanners/semgrep.md b/website/docs/scanners/semgrep.md index 5351ad6..46a6996 100644 --- a/website/docs/scanners/semgrep.md +++ b/website/docs/scanners/semgrep.md @@ -1,5 +1,9 @@ # Semgrep +In both scan modes, Semgrep parses complete selected HEAD files. In `diff_only` +mode, Layne filters findings to exact changed lines after scanning. This preserves +the syntax and enclosing context required by structural and dataflow rules. +