fix: suppress entropy false positives on Docker image refs

ober

eb8edf4bdfed3afcdc72fd37453d39bd68c02ac9

diff --git a/gitsafe/scanner.ss b/gitsafe/scanner.ss
index 561e34d..02bdad7 100644
--- a/gitsafe/scanner.ss
+++ b/gitsafe/scanner.ss
@@ -119,10 +119,12 @@
   (def *hunk-header-re*
     (re "^@@ -[0-9,]+ \\+([0-9]+)(?:,[0-9]+)? @@"))
 
-  ;; --- URL context detection ---
-  ;; Pattern IDs whose matches should be suppressed when they appear inside a URL.
-  ;; These are the noisy entropy patterns; precise patterns (AWS keys, GitHub PATs,
-  ;; etc.) are intentionally excluded so credentials embedded in URLs are still caught.
+  ;; --- 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
+  ;; image refs are still caught.
   (def *url-sensitive-patterns*
     '(high-entropy-base64 high-entropy-hex))
 
@@ -142,6 +144,44 @@
            #t]
           [else (loop (+ i 1))]))))
 
+  ;; 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…),
+  ;; subresource integrity hashes, lockfile resolved fields, etc. — never
+  ;; credentials.
+  (def (in-digest-context? line match-start)
+    (and (>= match-start 7)
+         (let ([prefix (substring line (- match-start 7) match-start)])
+           (or (string=? prefix "sha256:")
+               (string=? prefix "sha384:")
+               (string=? prefix "sha512:")))))
+
+  ;; Returns #t when the line is a Dockerfile FROM directive. Tags and
+  ;; digests on a FROM line are content-addressable image references
+  ;; (e.g. "FROM image:abc123…", "FROM image@sha256:…") and are never
+  ;; credentials. Case-insensitive ("from" is also valid per Dockerfile spec).
+  (def (in-dockerfile-from? line)
+    (let ([n (string-length line)])
+      (let loop ([i 0])
+        (cond
+          [(>= i n) #f]
+          [(or (char=? (string-ref line i) #\space)
+               (char=? (string-ref line i) #\tab))
+           (loop (+ i 1))]
+          [(and (>= n (+ i 5))
+                (let ([c0 (string-ref line i)]
+                      [c1 (string-ref line (+ i 1))]
+                      [c2 (string-ref line (+ i 2))]
+                      [c3 (string-ref line (+ i 3))]
+                      [c4 (string-ref line (+ i 4))])
+                  (and (or (char=? c0 #\F) (char=? c0 #\f))
+                       (or (char=? c1 #\R) (char=? c1 #\r))
+                       (or (char=? c2 #\O) (char=? c2 #\o))
+                       (or (char=? c3 #\M) (char=? c3 #\m))
+                       (char=? c4 #\space))))
+           #t]
+          [else #f]))))
+
   ;; --- Severity ordering ---
   (def (severity-level sev)
     (match sev
@@ -197,9 +237,12 @@
                 (let ([matched (extract-match m)])
                   (if (not matched)
                     (loop (cdr pats) results)
-                    ;; For high-noise entropy patterns, suppress matches inside URLs
+                    ;; For high-noise entropy patterns, suppress matches inside
+                    ;; URLs, OCI content digests, and Dockerfile FROM lines.
                     (if (and (member (secret-pattern-id pat) *url-sensitive-patterns*)
-                             (in-url-context? line (re-match-start m)))
+                             (or (in-url-context? line (re-match-start m))
+                                 (in-digest-context? line (re-match-start m))
+                                 (in-dockerfile-from? line)))
                       (loop (cdr pats) results)
                       ;; Run validator if present
                       (let ([valid? (let ([v (secret-pattern-validator pat)])
diff --git a/test/fixtures/false-positives.txt b/test/fixtures/false-positives.txt
index 293c03e..50c3def 100644
--- a/test/fixtures/false-positives.txt
+++ b/test/fixtures/false-positives.txt
@@ -36,3 +36,10 @@ secret_key = "sk_live_thisshouldbesuppressed"  # gitsafe:ignore
 ;; https://en.wikipedia.org/wiki/RTTRP#History-2ddb12341234abcdef12abcd2cf5
 ;; https://sourceforge.net/projects/moldudp64/files/spec/moldudp64.pdf
 ;; https://standards.ieee.org/findstds/standard/802.3304-2017.html
+
+# Docker / OCI image references — content-addressable, not credentials.
+FROM jerboa21/jerboa:5f528310118172516d969cfa03dce03e278bbeca AS builder
+FROM ubuntu@sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd
+from alpine:3.19
+  FROM  python:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789
+image: nginx@sha256:e9954c1fc875017be1c3e36eca16be2d9e9bccc4bf072163515467d6a823c7cf
diff --git a/test/test-gitsafe.ss b/test/test-gitsafe.ss
index 0acc686..22e606f 100644
--- a/test/test-gitsafe.ss
+++ b/test/test-gitsafe.ss
@@ -330,6 +330,20 @@
                                             '(high-entropy-base64 high-entropy-hex)))
                                   findings))))
 
+    (test-case "scan-content: Docker image refs are not flagged as entropy secrets"
+      ;; Dockerfile FROM lines and @sha256: digests are content-addressable.
+      (let* ([c        (default-config)]
+             [findings (scan-content "Dockerfile"
+                         (string-append
+                           "FROM jerboa21/jerboa:5f528310118172516d969cfa03dce03e278bbeca AS builder\n"
+                           "FROM ubuntu@sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd\n"
+                           "image: nginx@sha256:e9954c1fc875017be1c3e36eca16be2d9e9bccc4bf072163515467d6a823c7cf\n")
+                         c)])
+        (check-equal? '() (filter (lambda (f)
+                                    (member (finding-pattern-id f)
+                                            '(high-entropy-base64 high-entropy-hex)))
+                                  findings))))
+
     (test-case "scan-content: false-positives fixture produces no entropy findings"
       (let* ([c        (default-config)]
              [content  (read-file-string "test/fixtures/false-positives.txt")]