security: isolate per-target scan errors so one bad file cannot zero the scan
ober
7fce23d649a6c3e591a8c0e0070568514587ad11
--- a/lib/semgrep/cli.sls +++ b/lib/semgrep/cli.sls @@ -515,6 +515,14 @@ (lambda (source identity) (scan-string rules language target-path source))) (scan-file rules language target-path))))) + (def (report-scan-error path condition) + (let ([port (current-error-port)]) + (display "semgrep: scan-error: skipping " port) + (display path port) + (display ": " port) + (display-condition condition port) + (newline port) + (flush-output-port port))) (def (findings-for-path path findings) (filter (lambda (finding) (string=? (finding-path finding) path)) @@ -582,10 +590,17 @@ (apply append (map (lambda (target) - (scan-one - rules - language - target)) + (guard (scan-failure + [#t + (report-scan-error + (target-display + target) + scan-failure) + '()]) + (scan-one + rules + language + target))) expanded-targets)))]) (when autofix? (apply-autofix! expanded-targets findings)) --- a/src/.jerbuild-hashes +++ b/src/.jerbuild-hashes @@ -34,7 +34,7 @@ . "285FDB423AD7E28C") ("src/semgrep/result/builders.ss" . "93A64AF4435E6132") - ("src/semgrep/cli.ss" . "6145E877B2DA1583") + ("src/semgrep/cli.ss" . "4FE7A57C41C71E97") ("src/semgrep/output/sarif.ss" . "740FF3708E6C1BB") ("src/semgrep/engine/js-vardef-scan.ss" . --- a/src/semgrep/cli.ss +++ b/src/semgrep/cli.ss @@ -495,6 +495,15 @@ (scan-string rules language target-path source))) (scan-file rules language target-path))))) +(def (report-scan-error path condition) + (let ([port (current-error-port)]) + (display "semgrep: scan-error: skipping " port) + (display path port) + (display ": " port) + (display-condition condition port) + (newline port) + (flush-output-port port))) + (def (findings-for-path path findings) (filter (lambda (finding) (string=? (finding-path finding) path)) findings)) @@ -557,7 +566,13 @@ severities (apply append (map (lambda (target) - (scan-one rules language target)) + (guard (scan-failure + [#t + (report-scan-error + (target-display target) + scan-failure) + '()]) + (scan-one rules language target))) expanded-targets)))]) (when autofix? (apply-autofix! expanded-targets findings)) --- a/tests/smoke.ss +++ b/tests/smoke.ss @@ -5287,6 +5287,41 @@ (check (string-contains? out "skip.py") => #f) (check (string-contains? err "skip.py") => #t))) +(test-case "cli main isolates per-target scan errors" + (let ([dir "/tmp/jerboa-semgrep-scan-error-isolation"] + [good-path "/tmp/jerboa-semgrep-scan-error-isolation/good.py"] + [bad-path "/tmp/jerboa-semgrep-scan-error-isolation/bad.py"]) + (guard (setup-failure [#t (void)]) (safe-delete-file good-path)) + (guard (setup-failure [#t (void)]) (safe-delete-file bad-path)) + (guard (setup-failure [#t (void)]) (delete-directory dir)) + (mkdir dir) + (dynamic-wind + (lambda () (void)) + (lambda () + (write-file-string good-path "eval(user_input)\n") + (write-file-string + bad-path + (with-output-to-string + (lambda () + (let loop ([i 0]) + (when (< i 50001) + (display "eval(\n") + (loop (+ i 1))))))) + (let-values ([(code out err) + (call-with-captured-output + (lambda () + (main (list "scan" "--json" "--config" + "tests/fixtures/eval.yml" dir))))]) + (check code => 1) + (check (string-contains? out "good.py") => #t) + (check (string-contains? out "bad.py") => #f) + (check (string-contains? err "scan-error") => #t) + (check (string-contains? err "bad.py") => #t))) + (lambda () + (guard (teardown-failure [#t (void)]) (safe-delete-file good-path)) + (guard (teardown-failure [#t (void)]) (safe-delete-file bad-path)) + (guard (teardown-failure [#t (void)]) (delete-directory dir)))))) + (test-case "cli main accepts json flag" (check (main '("scan" "--json" "--config" "tests/fixtures/eval.yml" "tests/fixtures/demo.py")) => 1))