security: isolate per-target scan errors so one bad file cannot zero the scan

ober

7fce23d649a6c3e591a8c0e0070568514587ad11

diff --git a/lib/semgrep/cli.sls b/lib/semgrep/cli.sls
index 5013e1a..355aba4 100644
--- 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))
diff --git a/src/.jerbuild-hashes b/src/.jerbuild-hashes
index cddbbb2..045aff8 100644
--- 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"
    .
diff --git a/src/semgrep/cli.ss b/src/semgrep/cli.ss
index bd4a85b..1d6eace 100644
--- 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))
diff --git a/tests/smoke.ss b/tests/smoke.ss
index fb65e60..47452a5 100644
--- 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))