Ignore Git object IDs in entropy scan

ober

6ad5cd8240a0f415c75af5c966449736ba424b7c

diff --git a/gitsafe/scanner.ss b/gitsafe/scanner.ss
index 66341bf..cb6d6f8 100644
--- a/gitsafe/scanner.ss
+++ b/gitsafe/scanner.ss
@@ -194,9 +194,9 @@
 
   ;; --- Context detection for noisy entropy patterns ---
   ;; Pattern IDs whose matches should be suppressed in non-secret contexts:
-  ;; URL paths, OCI content digests, and Dockerfile FROM lines. These are
-  ;; the broad entropy patterns; precise patterns (AWS keys, GitHub PATs,
-  ;; etc.) are intentionally excluded so credentials embedded in URLs or
+  ;; URL paths, OCI content digests, Git object IDs, and Dockerfile FROM lines.
+  ;; These are the broad entropy patterns; precise patterns (AWS keys, GitHub
+  ;; PATs, etc.) are intentionally excluded so credentials embedded in URLs or
   ;; image refs are still caught.
   (def *url-sensitive-patterns*
     '(high-entropy-base64 high-entropy-hex))
@@ -217,6 +217,21 @@
            #t]
           [else (loop (+ i 1))]))))
 
+  (def *git-sha-prefix-re*
+    (re "(?i:(^|[^A-Za-z0-9_])(?:old\\s+head|new\\s+head|head|commit|revision|rev|object|tree|parent|merge|merged|checkout|reset|rebase|tag)[^A-Za-z0-9_]{0,24}$)"))
+
+  (def (git-object-id-length? s)
+    (let ([n (string-length s)])
+      (or (= n 40) (= n 64))))
+
+  ;; Returns #t when a high-entropy hex match is in a Git object-id context,
+  ;; e.g. "OLD HEAD: <sha1>", "commit <sha1>", or "parent <sha1>".
+  (def (in-git-sha-context? line match-start matched)
+    (and (git-object-id-length? matched)
+         (let* ([prefix-start (max 0 (- match-start 96))]
+                [prefix (substring line prefix-start match-start)])
+           (and (re-search *git-sha-prefix-re* prefix) #t))))
+
   ;; Returns #t when the match at match-start is the value of a content
   ;; digest reference like "sha256:<hex>", "sha384:<hex>", or "sha512:<hex>".
   ;; These appear in OCI/Docker image refs (e.g. image@sha256:abc…),
@@ -311,10 +326,12 @@
                   (if (not matched)
                     (loop (cdr pats) results)
                     ;; For high-noise entropy patterns, suppress matches inside
-                    ;; URLs, OCI content digests, and Dockerfile FROM lines.
+                    ;; URLs, OCI content digests, Git object IDs, and Dockerfile
+                    ;; FROM lines.
                     (if (and (member (secret-pattern-id pat) *url-sensitive-patterns*)
                              (or (in-url-context? line (re-match-start m))
                                  (in-digest-context? line (re-match-start m))
+                                 (in-git-sha-context? line (re-match-start m) matched)
                                  (in-dockerfile-from? line)))
                       (loop (cdr pats) results)
                       ;; Run validator if present
diff --git a/test/test-gitsafe.ss b/test/test-gitsafe.ss
index 6c4572d..a5a7ad4 100644
--- a/test/test-gitsafe.ss
+++ b/test/test-gitsafe.ss
@@ -320,6 +320,19 @@
                          c)])
         (check-equal? '() findings)))
 
+    (test-case "scan-content: git shas are not flagged as entropy secrets"
+      (let* ([c        (default-config)]
+             [findings (scan-content "rebase.log"
+                         (string-append
+                           "OLD HEAD: 89ca49128bc2f2f38480be0e697d02a3a5339121\n"
+                           "commit 89ca49128bc2f2f38480be0e697d02a3a5339121\n"
+                           "parent 89ca49128bc2f2f38480be0e697d02a3a5339121\n")
+                         c)])
+        (check-equal? '() (filter (lambda (f)
+                                    (member (finding-pattern-id f)
+                                            '(high-entropy-base64 high-entropy-hex)))
+                                  findings))))
+
     (test-case "scan-content: URLs in comments are not flagged as entropy secrets"
       ;; Long URL paths match [A-Za-z0-9/]{40+} but are not secrets.
       (let* ([c        (default-config)]