Migrate from (std pregexp) to (std regex) throughout
ober
01b8c46024d87146cb65f4beace9298101f623cc
--- a/gitsafe/allowlist.ss +++ b/gitsafe/allowlist.ss @@ -14,7 +14,7 @@ partition make-date make-time) (except (jerboa prelude) meta atom?) - (std pregexp) + (std regex) (std misc ports) (std misc string) (gitsafe config)) @@ -26,16 +26,15 @@ ;; // gitsafe:ignore ;; /* gitsafe:ignore */ - (def *suppress-pat* - (pregexp "(?:#|//|/\\*)\\s*gitsafe:ignore(?:=([A-Za-z0-9_-]+))?")) + (def *suppress-re* + (re "(?:#|//|/\\*)\\s*gitsafe:ignore(?:=([A-Za-z0-9_-]+))?")) ;; Returns #t if the line is suppressed (optionally for a specific pattern-id symbol). (def (line-suppressed? line (pattern-id #f)) - (let ([m (pregexp-match *suppress-pat* line)]) + (let ([m (re-search *suppress-re* line)]) (if (not m) #f - ;; m = (full-match maybe-pattern-id-group) - (let ([specific (and (pair? (cdr m)) (cadr m))]) + (let ([specific (re-match-group m 1)]) (cond ;; No specific pattern in comment — suppress all [(not specific) #t] --- a/gitsafe/git.ss +++ b/gitsafe/git.ss @@ -24,7 +24,7 @@ partition make-date make-time) (except (jerboa prelude) meta atom?) - (std pregexp) + (std regex) (std misc process) (std misc string) (std misc ports)) @@ -66,6 +66,9 @@ ;; --- Parse unified diff format --- ;; Returns list of diff-hunk structs from a git diff output string. ;; Only captures added lines (lines starting with +, not +++). + (def *hunk-header-re* + (re "^@@ -[0-9,]+ \\+([0-9]+)(?:,[0-9]+)? @@")) + (def (parse-unified-diff diff-text current-file) (let loop ([lines (string-split diff-text #\newline)] [hunks '()] @@ -81,11 +84,10 @@ [rest (cdr lines)]) (cond ;; Hunk header: @@ -old,count +new,count @@ - [(pregexp-match "^@@ -[0-9,]+ \\+([0-9]+)(?:,[0-9]+)? @@" line) + [(re-search *hunk-header-re* line) => (lambda (m) - (let* ([start-str (cadr m)] - [new-start (string->number start-str)] + (let* ([new-start (string->number (re-match-group m 1))] ;; Save previous hunk if it had findings [hunks* (if (and cur-hunk (not (null? (diff-hunk-lines cur-hunk)))) --- a/gitsafe/patterns.ss +++ b/gitsafe/patterns.ss @@ -20,7 +20,7 @@ partition make-date make-time) (except (jerboa prelude) meta atom?) - (std pregexp)) + (std regex)) ;; --- Secret pattern struct --- (defstruct secret-pattern @@ -58,13 +58,16 @@ (let ([p (/ (inexact c) n)]) (loop (+ i 1) (- e (* p (log p 2)))))))))))))) + (def *placeholder-re* + (re "(?i:(?:^|[_.-])(?:example|placeholder|dummy|sample|your[_-]?(?:api[_-]?)?key|replace[_-]?me|change[_-]?me|insert[_-]?here)(?:$|[_.-]))")) + (def (not-placeholder? str) ;; Reject common test/example placeholder strings. ;; Only match placeholder words at word boundaries (start/end or separator) ;; to avoid rejecting real secrets that happen to contain "test" or "fake". - (not (or (pregexp-match "^[Xx]+$" str) - (pregexp-match "^[Aa]+$" str) - (pregexp-match "(?i:(?:^|[_.-])(?:example|placeholder|dummy|sample|your[_-]?(?:api[_-]?)?key|replace[_-]?me|change[_-]?me|insert[_-]?here)(?:$|[_.-]))" str) + (not (or (re-match? "[Xx]+" str) + (re-match? "[Aa]+" str) + (re-search *placeholder-re* str) (string=? str "") (< (string-length str) 8)))) @@ -75,7 +78,7 @@ 'aws-access-key "AWS Access Key ID" 'critical - (pregexp "(?:^|[^A-Za-z0-9])((?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16})(?:[^A-Za-z0-9]|$)") + (re "(?:^|[^A-Za-z0-9])((?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16})(?:[^A-Za-z0-9]|$)") (lambda (m) (= (string-length m) 20)) "AWS IAM Access Key ID (starts with AKIA/ABIA/ACCA/ASIA)")) @@ -84,7 +87,7 @@ 'aws-secret-key "AWS Secret Access Key" 'critical - (pregexp "(?i:aws[_-]?secret[_-]?(?:access[_-]?)?key|secret[_-]?key)\\s*[=:]\\s*['\"]?([A-Za-z0-9/+=]{40})['\"]?") + (re "(?i:aws[_-]?secret[_-]?(?:access[_-]?)?key|secret[_-]?key)\\s*[=:]\\s*['\"]?([A-Za-z0-9/+=]{40})['\"]?") (lambda (m) (= (string-length m) 40)) "AWS Secret Access Key (40-char base64)")) @@ -93,7 +96,7 @@ 'github-pat "GitHub Personal Access Token" 'critical - (pregexp "gh[pousr]_[A-Za-z0-9_]{36,255}") + (re "gh[pousr]_[A-Za-z0-9_]{36,255}") (lambda (m) (has-prefix? m "gh")) "GitHub PAT (classic: ghp_, gho_, ghu_, ghs_, ghr_)")) @@ -102,7 +105,7 @@ 'github-fine-grained "GitHub Fine-Grained PAT" 'critical - (pregexp "github_pat_[A-Za-z0-9_]{22,255}") + (re "github_pat_[A-Za-z0-9_]{22,255}") #f "GitHub Fine-Grained Personal Access Token")) @@ -111,7 +114,7 @@ 'openai-api-key "OpenAI API Key" 'critical - (pregexp "sk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}") + (re "sk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}") (lambda (m) (string-contains m "T3BlbkFJ")) "OpenAI API Key (contains T3BlbkFJ marker)")) @@ -120,7 +123,7 @@ 'openai-project-key "OpenAI Project Key" 'critical - (pregexp "sk-proj-[A-Za-z0-9_-]{40,200}") + (re "sk-proj-[A-Za-z0-9_-]{40,200}") #f "OpenAI Project-scoped API Key")) @@ -129,7 +132,7 @@ 'anthropic-api-key "Anthropic API Key" 'critical - (pregexp "sk-ant-(?:[a-z0-9]+-)?[A-Za-z0-9_-]{80,200}") + (re "sk-ant-(?:[a-z0-9]+-)?[A-Za-z0-9_-]{80,200}") (lambda (m) (has-prefix? m "sk-ant-")) "Anthropic Claude API Key")) @@ -138,7 +141,7 @@ 'openai-svcacct-key "OpenAI Service Account Key" 'critical - (pregexp "sk-svcacct-[A-Za-z0-9_-]{40,200}") + (re "sk-svcacct-[A-Za-z0-9_-]{40,200}") #f "OpenAI Service Account API Key")) @@ -147,7 +150,7 @@ 'stripe-secret "Stripe Secret Key" 'critical - (pregexp "[sr]k_live_[A-Za-z0-9]{24,99}") + (re "[sr]k_live_[A-Za-z0-9]{24,99}") #f "Stripe live secret or restricted key")) @@ -156,7 +159,7 @@ 'private-key-pem "Private Key (PEM)" 'critical - (pregexp "-----BEGIN (?:RSA |DSA |EC |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY-----") + (re "-----BEGIN (?:RSA |DSA |EC |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY-----") #f "PEM-encoded private key block")) @@ -165,7 +168,7 @@ 'putty-private-key "PuTTY Private Key" 'critical - (pregexp "PuTTY-User-Key-File-[0-9]+:") + (re "PuTTY-User-Key-File-[0-9]+:") #f "PuTTY PPK private key file")) @@ -176,7 +179,7 @@ 'generic-api-key "Generic API Key Assignment" 'high - (pregexp "(?i:(api[_-]?key|apikey|api[_-]?secret|access[_-]?key))\\s*[=:]\\s*['\"]([A-Za-z0-9_/+=.\\-]{16,})['\"]") + (re "(?i:(api[_-]?key|apikey|api[_-]?secret|access[_-]?key))\\s*[=:]\\s*['\"]([A-Za-z0-9_/+=.\\-]{16,})['\"]") (lambda (m) (and (not-placeholder? m) (entropy-above? m 3.5))) "Key/value assignment with high-entropy value")) @@ -185,7 +188,7 @@ 'generic-secret "Generic Secret Assignment" 'high - (pregexp "(?i:(secret|token|password|passwd|credential|auth[_-]?key))\\s*[=:]\\s*['\"]([^'\"\\s]{8,})['\"]") + (re "(?i:(secret|token|password|passwd|credential|auth[_-]?key))\\s*[=:]\\s*['\"]([^'\"\\s]{8,})['\"]") (lambda (m) (and (not-placeholder? m) (entropy-above? m 3.5))) "Assignment of secret/token/password with high-entropy value")) @@ -194,7 +197,7 @@ 'generic-bearer "Bearer Token" 'high - (pregexp "(?i:bearer)\\s+([A-Za-z0-9_.~+/=\\-]{20,})") + (re "(?i:bearer)\\s+([A-Za-z0-9_.~+/=\\-]{20,})") (lambda (m) (entropy-above? m 3.0)) "HTTP Authorization Bearer token")) @@ -203,7 +206,7 @@ 'slack-token "Slack Token" 'high - (pregexp "xox[bpors]-[A-Za-z0-9\\-]{10,250}") + (re "xox[bpors]-[A-Za-z0-9\\-]{10,250}") #f "Slack API token (xoxb-, xoxp-, xoxo-, xoxr-, xoxs-)")) @@ -212,7 +215,7 @@ 'slack-webhook "Slack Webhook URL" 'high - (pregexp "hooks\\.slack\\.com/services/T[A-Z0-9]{8,10}/B[A-Z0-9]{8,10}/[A-Za-z0-9]{20,30}") + (re "hooks\\.slack\\.com/services/T[A-Z0-9]{8,10}/B[A-Z0-9]{8,10}/[A-Za-z0-9]{20,30}") #f "Slack incoming webhook URL")) @@ -221,7 +224,7 @@ 'google-api-key "Google API Key" 'high - (pregexp "AIza[A-Za-z0-9_\\-]{35}") + (re "AIza[A-Za-z0-9_\\-]{35}") (lambda (m) (= (string-length m) 39)) "Google Cloud / Maps API key (starts with AIza)")) @@ -230,7 +233,7 @@ 'twilio-api-key "Twilio API Key" 'high - (pregexp "SK[a-f0-9]{32}") + (re "SK[a-f0-9]{32}") (lambda (m) (= (string-length m) 34)) "Twilio API key SID")) @@ -239,7 +242,7 @@ 'sendgrid-api-key "SendGrid API Key" 'high - (pregexp "SG\\.[A-Za-z0-9_\\-]{22}\\.[A-Za-z0-9_\\-]{43}") + (re "SG\\.[A-Za-z0-9_\\-]{22}\\.[A-Za-z0-9_\\-]{43}") #f "SendGrid API key (SG. format)")) @@ -248,7 +251,7 @@ 'mailgun-api-key "Mailgun API Key" 'high - (pregexp "key-[a-f0-9]{32}") + (re "key-[a-f0-9]{32}") (lambda (m) (= (string-length m) 36)) "Mailgun private API key")) @@ -257,7 +260,7 @@ 'npm-token "NPM Token" 'high - (pregexp "npm_[A-Za-z0-9]{36}") + (re "npm_[A-Za-z0-9]{36}") #f "NPM publish/automation token")) @@ -266,7 +269,7 @@ 'pypi-token "PyPI Token" 'high - (pregexp "pypi-[A-Za-z0-9_\\-]{50,}") + (re "pypi-[A-Za-z0-9_\\-]{50,}") #f "PyPI upload token")) @@ -275,19 +278,20 @@ 'jwt "JSON Web Token" 'high - (pregexp "eyJ[A-Za-z0-9_\\-]{10,}\\.[A-Za-z0-9_\\-]{10,}\\.[A-Za-z0-9_\\-]{10,}") + (re "eyJ[A-Za-z0-9_\\-]{10,}\\.[A-Za-z0-9_\\-]{10,}\\.[A-Za-z0-9_\\-]{10,}") #f "JWT (three-part base64url token starting with eyJ)")) + (def *basic-auth-placeholder-re* + (re "(?i://(?:user|username|admin|root):(?:pass|password|passwd|secret|changeme|x{3,})@)")) + (def pat-basic-auth-url (make-secret-pattern 'basic-auth-url "Credentials in URL" 'high - (pregexp "[a-z+]+://[^:@\\s]+:[^:@\\s]+@[^\\s\"']+") - (lambda (m) (not (pregexp-match - "(?i://(?:user|username|admin|root):(?:pass|password|passwd|secret|changeme|x{3,})@)" - m))) + (re "[a-z+]+://[^:@\\s]+:[^:@\\s]+@[^\\s\"']+") + (lambda (m) (not (re-search *basic-auth-placeholder-re* m))) "URL with embedded user:password credentials")) ;; --- MEDIUM patterns --- @@ -297,7 +301,7 @@ 'connection-string "Database Connection String" 'medium - (pregexp "(?i:(?:mongodb|postgres|postgresql|mysql|redis|amqp|mssql))://[^:@\\s]+:[^:@\\s]+@[^\\s\"']+") + (re "(?i:(?:mongodb|postgres|postgresql|mysql|redis|amqp|mssql))://[^:@\\s]+:[^:@\\s]+@[^\\s\"']+") #f "Database/broker connection string with credentials")) @@ -306,7 +310,7 @@ 'high-entropy-hex "High-Entropy Hex String" 'medium - (pregexp "[0-9a-f]{40,}") + (re "[0-9a-f]{40,}") (lambda (m) (entropy-above? m 3.0)) "Long hex string with high entropy (possible API key or token)")) @@ -315,7 +319,7 @@ 'high-entropy-base64 "High-Entropy Base64 String" 'medium - (pregexp "[A-Za-z0-9+/]{40,}={0,2}") + (re "[A-Za-z0-9+/]{40,}={0,2}") (lambda (m) (entropy-above? m 4.0)) "Long base64 string with high entropy (possible encoded secret)")) --- a/gitsafe/scanner.ss +++ b/gitsafe/scanner.ss @@ -27,7 +27,7 @@ partition make-date make-time) (except (jerboa prelude) meta atom?) - (std pregexp) + (std regex) (std misc string) (std misc ports) (std os path) @@ -81,6 +81,33 @@ (config-excluded? config path)) #t))) + ;; --- Compiled patterns --- + (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. + (def *url-sensitive-patterns* + '(high-entropy-base64 high-entropy-hex)) + + ;; Returns #t if the position match-start in line follows a "://" sequence, + ;; indicating the match is part of a URL path, query string, or fragment. + ;; Looks back up to 300 chars to handle long URLs before the match. + (def (in-url-context? line match-start) + (let ([limit (string-length line)]) + (let loop ([i (max 0 (- match-start 300))]) + (cond + ;; Need at least 3 chars "://" before match-start + [(>= (+ i 2) match-start) #f] + [(and (< (+ i 2) limit) + (char=? (string-ref line i) #\:) + (char=? (string-ref line (+ i 1)) #\/) + (char=? (string-ref line (+ i 2)) #\/)) + #t] + [else (loop (+ i 1))])))) + ;; --- Severity ordering --- (def (severity-level sev) (match sev @@ -97,21 +124,19 @@ (def (min-severity config) (gitsafe-config-severity config)) - ;; --- Extract matched substring from pregexp-match-positions result --- - (def (extract-match line positions) - ;; positions is ((start . end) ...) — first pair is the full match, - ;; rest are capture groups. Use the last non-#f capture group if any, - ;; so validators receive the meaningful value (e.g. the key itself) - ;; rather than the full match including boundary/context chars. - (and (pair? positions) - (let* ([best (let loop ([rest (cdr positions)] [last-good #f]) - (cond - [(null? rest) (or last-good (car positions))] - [(car rest) (loop (cdr rest) (car rest))] - [else (loop (cdr rest) last-good)]))] - [s (car best)] - [e (cdr best)]) - (substring line s e)))) + ;; --- Extract matched substring from a re-match-object --- + (def (extract-match m) + ;; Use the last non-#f capture group if any, so validators receive + ;; the meaningful value (e.g. the key itself) rather than the full + ;; match including boundary/context chars. + (let ([groups (re-match-groups m)]) + (if (null? groups) + (re-match-full m) + (let loop ([gs groups] [last-good #f]) + (cond + [(null? gs) (or last-good (re-match-full m))] + [(car gs) (loop (cdr gs) (car gs))] + [else (loop (cdr gs) last-good)]))))) ;; --- Get active patterns given config --- (def (active-patterns config) @@ -132,15 +157,18 @@ (if (null? pats) (reverse results) (let ([pat (car pats)]) - (let ([positions (pregexp-match-positions - (secret-pattern-pregexp pat) line)]) - (if (not positions) + (let ([m (re-search (secret-pattern-pregexp pat) line)]) + (if (not m) (loop (cdr pats) results) - (let ([matched (extract-match line positions)]) + (let ([matched (extract-match m)]) (if (not matched) (loop (cdr pats) results) - ;; Run validator if present - (let ([valid? (let ([v (secret-pattern-validator pat)]) + ;; For high-noise entropy patterns, suppress matches inside URLs + (if (and (member (secret-pattern-id pat) *url-sensitive-patterns*) + (in-url-context? line (re-match-start m))) + (loop (cdr pats) results) + ;; Run validator if present + (let ([valid? (let ([v (secret-pattern-validator pat)]) (if v (v matched) #t))]) (if (not valid?) (loop (cdr pats) results) @@ -160,7 +188,7 @@ (redact line) matched (redact matched))]) - (loop (cdr pats) (cons f results)))))))))) + (loop (cdr pats) (cons f results))))))))))) )))))) ;; --- Scan full file content (string) --- @@ -248,10 +276,10 @@ (let ([line (car lines)] [rest (cdr lines)]) (cond - [(pregexp-match "^@@ -[0-9,]+ \\+([0-9]+)(?:,[0-9]+)? @@" line) + [(re-search *hunk-header-re* line) => (lambda (m) - (let* ([new-start (string->number (cadr m))] + (let* ([new-start (string->number (re-match-group m 1))] [hunks* (if (and cur-hunk (not (null? (diff-hunk-lines cur-hunk)))) (cons cur-hunk hunks) --- a/test/fixtures/false-positives.txt +++ b/test/fixtures/false-positives.txt @@ -27,3 +27,12 @@ integrity = "sha512-abc123def456" # Test that gitsafe:ignore suppression works secret_key = "sk_live_thisshouldbesuppressed" # gitsafe:ignore + +# URLs in comments — long URL paths look like base64 but are not secrets. +# These are real examples from Wireshark dissector source files. +;; https://github.com/couchbase/couchbase-protocol +;; https://kafka.apache.org/protocol/protocol +;; https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-memcache.c +;; 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 --- a/test/test-gitsafe.ss +++ b/test/test-gitsafe.ss @@ -26,7 +26,7 @@ (import (except (jerboa prelude) meta atom?) (std test) - (std pregexp) + (std regex) (gitsafe entropy) (gitsafe patterns) (gitsafe config) @@ -102,14 +102,14 @@ (let ([pat (car (filter (lambda (p) (eq? (secret-pattern-id p) 'aws-access-key)) (all-patterns)))]) (check-predicate - (pregexp-match (secret-pattern-pregexp pat) "AKIAIOSFODNN7EXAMPLE!!!") + (re-search (secret-pattern-pregexp pat) "AKIAIOSFODNN7EXAMPLE!!!") (lambda (m) m)))) (test-case "github-pat: detects ghp_ token" (let ([pat (car (filter (lambda (p) (eq? (secret-pattern-id p) 'github-pat)) (all-patterns)))]) (check-predicate - (pregexp-match (secret-pattern-pregexp pat) + (re-search (secret-pattern-pregexp pat) "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef1234") (lambda (m) m)))) @@ -117,7 +117,7 @@ (let ([pat (car (filter (lambda (p) (eq? (secret-pattern-id p) 'anthropic-api-key)) (all-patterns)))]) (check-predicate - (pregexp-match (secret-pattern-pregexp pat) + (re-search (secret-pattern-pregexp pat) "sk-ant-api03-ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcde") (lambda (m) m)))) @@ -125,7 +125,7 @@ (let ([pat (car (filter (lambda (p) (eq? (secret-pattern-id p) 'jwt)) (all-patterns)))]) (check-predicate - (pregexp-match (secret-pattern-pregexp pat) + (re-search (secret-pattern-pregexp pat) "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.SflKxwRJSMeKKF2QT4fw") (lambda (m) m)))) @@ -133,7 +133,7 @@ (let ([pat (car (filter (lambda (p) (eq? (secret-pattern-id p) 'stripe-secret)) (all-patterns)))]) (check-predicate - (pregexp-match (secret-pattern-pregexp pat) + (re-search (secret-pattern-pregexp pat) "sk_live_ABCDEFGHIJKLMNOPQRSTUVWXYZabc") (lambda (m) m)))) @@ -141,7 +141,7 @@ (let ([pat (car (filter (lambda (p) (eq? (secret-pattern-id p) 'private-key-pem)) (all-patterns)))]) (check-predicate - (pregexp-match (secret-pattern-pregexp pat) + (re-search (secret-pattern-pregexp pat) "-----BEGIN OPENSSH PRIVATE KEY-----") (lambda (m) m)))) @@ -149,7 +149,7 @@ (let ([pat (car (filter (lambda (p) (eq? (secret-pattern-id p) 'private-key-pem)) (all-patterns)))]) (check-predicate - (pregexp-match (secret-pattern-pregexp pat) + (re-search (secret-pattern-pregexp pat) "-----BEGIN ENCRYPTED PRIVATE KEY-----") (lambda (m) m)))) @@ -157,7 +157,7 @@ (let ([pat (car (filter (lambda (p) (eq? (secret-pattern-id p) 'putty-private-key)) (all-patterns)))]) (check-predicate - (pregexp-match (secret-pattern-pregexp pat) + (re-search (secret-pattern-pregexp pat) "PuTTY-User-Key-File-3: ssh-rsa") (lambda (m) m)))) @@ -319,6 +319,27 @@ c)]) (check-equal? '() 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)] + [findings (scan-content "dissectors/kafka.ss" + ";; https://kafka.apache.org/protocol/protocol\n;; https://github.com/couchbase/couchbase-protocol\n;; https://en.wikipedia.org/wiki/RTTRP#History-2ddb12341234abcdef12abcd2cf5\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")] + [findings (scan-content "false-positives.txt" content c)] + [entropy-findings (filter (lambda (f) + (member (finding-pattern-id f) + '(high-entropy-base64 high-entropy-hex))) + findings)]) + (check-equal? '() entropy-findings))) + )) ;; ============================================================