fix: suppress entropy false positives on Docker image refs
ober
eb8edf4bdfed3afcdc72fd37453d39bd68c02ac9
--- 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)]) --- 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 --- 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")]