package detect import ( "bytes" "context" "fmt" "io" "os" "path/filepath" "runtime" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/rs/zerolog" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/exp/maps" "github.com/zricethezav/gitleaks/v8/cmd/scm" "github.com/zricethezav/gitleaks/v8/config" "github.com/zricethezav/gitleaks/v8/detect/codec" "github.com/zricethezav/gitleaks/v8/logging" "github.com/zricethezav/gitleaks/v8/regexp" "github.com/zricethezav/gitleaks/v8/report" "github.com/zricethezav/gitleaks/v8/sources" ) const maxDecodeDepth = 8 const configPath = "../testdata/config/" const repoBasePath = "../testdata/repos/" const archivesBasePath = "../testdata/archives/" const encodedTestValues = ` # Decoded -----BEGIN PRIVATE KEY----- 135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb u+QDkg0spw== -----END PRIVATE KEY----- # Encoded private_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K' # Double Encoded: b64 encoded aws config inside a jwt eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA # A small secret at the end to make sure that as the other ones above shrink # when decoded, the positions are taken into consideration for overlaps c21hbGwtc2VjcmV0 # This tests how it handles when the match bounds go outside the decoded value secret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA== # The above encoded again c2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0= # Confirm you can ignore on the decoded value password="bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw==" # This tests that it can do hex encoded data secret=6465636F6465642D7365637265742D76616C756576484558 # This tests that it can do percent encoded data ## partial encoded data secret=decoded-%73%65%63%72%65%74-valuev2 ## scattered encoded secret=%64%65coded-%73%65%63%72%65%74-valuev3 # Test multi levels of encoding where the source is a partal encoding # it is important that the bounds of the predecessors are properly # considered ## single percent encoding in the middle of multi layer b64 c2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09 ## single percent encoding at the beginning of hex secret%3d6465636F6465642D7365637265742D76616C75657635 ## multiple percent encodings in a single layer base64 secret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY= # ends in x86 ## base64 encoded partially percent encoded value secret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU= ## one of the lines above that went through... a lot ## and there's surrounding text around it Look at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy? ## Multi percent encode two random characters close to the bounds of the base64 ## encoded data to make sure that the bounds are still correctly calculated secret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU= ## The similar to the above but also touching the edge of the base64 secret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34 ## The similar to the above but also touching and overlapping the base64 secret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34 ` var multili = ` username = "admin" password = "secret123" ` func compare(t *testing.T, a, b []report.Finding) { if diff := cmp.Diff(a, b, cmpopts.SortSlices(func(a, b report.Finding) bool { if a.File != b.File { return a.File < b.File } if a.StartLine != b.StartLine { return a.StartLine < b.StartLine } if a.StartColumn != b.StartColumn { return a.StartColumn < b.StartColumn } if a.EndLine != b.EndLine { return a.EndLine < b.EndLine } if a.EndColumn != b.EndColumn { return a.EndColumn < b.EndColumn } if a.RuleID != b.RuleID { return a.RuleID < b.RuleID } return a.Secret < b.Secret }), cmpopts.IgnoreFields(report.Finding{}, "Fingerprint", "Author", "Email", "Date", "Message", "Commit", "requiredFindings"), cmpopts.EquateApprox(0.0001, 0), // For floating point Entropy comparison ); diff != "" { t.Errorf("findings mismatch (-want +got):\n%s", diff) } } func TestDetect(t *testing.T) { logging.Logger = logging.Logger.Level(zerolog.TraceLevel) tests := map[string]struct { cfgName string baselinePath string fragment Fragment // NOTE: for expected findings, all line numbers will be 0 // because line deltas are added _after_ the finding is created. // I.e., if the finding is from a --no-git file, the line number will be // increase by 1 in DetectFromFiles(). If the finding is from git, // the line number will be increased by the patch delta. expectedFindings []report.Finding wantError error expectedAuxOutput string }{ // General "valid allow comment (1)": { cfgName: "simple", fragment: Fragment{ Raw: `awsToken := \"AKIALALEMEL33243OKIA\ // gitleaks:allow"`, FilePath: "tmp.go", }, }, "valid allow comment (2)": { cfgName: "simple", fragment: Fragment{ Raw: `awsToken := \ \"AKIALALEMEL33243OKIA\ // gitleaks:allow" `, FilePath: "tmp.go", }, }, "invalid allow comment": { cfgName: "simple", fragment: Fragment{ Raw: `awsToken := \"AKIALALEMEL33243OKIA\" // gitleaks:allow" `, FilePath: "tmp.go", }, expectedFindings: []report.Finding{ { Description: "AWS Access Key", Secret: "AKIALALEMEL33243OKIA", Match: "AKIALALEMEL33243OKIA", File: "tmp.go", Line: `awsToken := \"AKIALALEMEL33243OKIA\"`, RuleID: "aws-access-key", Tags: []string{"key", "AWS"}, StartLine: 0, EndLine: 0, StartColumn: 15, EndColumn: 34, Entropy: 3.1464393, }, }, }, "detect finding - aws": { cfgName: "simple", fragment: Fragment{ Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`, FilePath: "tmp.go", }, expectedFindings: []report.Finding{ { RuleID: "aws-access-key", Description: "AWS Access Key", File: "tmp.go", Line: `awsToken := \"AKIALALEMEL33243OLIA\"`, Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", Entropy: 3.0841837, StartLine: 0, EndLine: 0, StartColumn: 15, EndColumn: 34, Tags: []string{"key", "AWS"}, }, }, }, "detect finding - sidekiq env var": { cfgName: "simple", fragment: Fragment{ Raw: `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`, FilePath: "tmp.sh", }, expectedFindings: []report.Finding{ { RuleID: "sidekiq-secret", Description: "Sidekiq Secret", File: "tmp.sh", Line: `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`, Match: "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;", Secret: "cafebabe:deadbeef", Entropy: 2.6098502, StartLine: 0, EndLine: 0, StartColumn: 8, EndColumn: 60, Tags: []string{}, }, }, }, "detect finding - sidekiq env var, semicolon": { cfgName: "simple", fragment: Fragment{ Raw: `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`, FilePath: "tmp.sh", }, expectedFindings: []report.Finding{ { RuleID: "sidekiq-secret", Description: "Sidekiq Secret", File: "tmp.sh", Line: `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`, Match: "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=\"cafebabe:deadbeef\"", Secret: "cafebabe:deadbeef", Entropy: 2.6098502, StartLine: 0, EndLine: 0, StartColumn: 21, EndColumn: 74, Tags: []string{}, }, }, }, "detect finding - sidekiq url": { cfgName: "simple", fragment: Fragment{ Raw: `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true¶m2=false#heading1"`, FilePath: "tmp.sh", }, expectedFindings: []report.Finding{ { RuleID: "sidekiq-sensitive-url", Description: "Sidekiq Sensitive URL", File: "tmp.sh", Line: `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true¶m2=false#heading1"`, Match: "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:", Secret: "cafeb4b3:d3adb33f", Entropy: 2.984234, StartLine: 0, EndLine: 0, StartColumn: 8, EndColumn: 58, Tags: []string{}, }, }, }, "ignore finding - our config file": { cfgName: "simple", fragment: Fragment{ Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`, FilePath: filepath.Join(configPath, "simple.toml"), }, }, "ignore finding - doesn't match path": { cfgName: "generic_with_py_path", fragment: Fragment{ Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`, FilePath: "tmp.go", }, }, "detect finding - matches path,regex,entropy": { cfgName: "generic_with_py_path", fragment: Fragment{ Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`, FilePath: "tmp.py", }, expectedFindings: []report.Finding{ { RuleID: "generic-api-key", Description: "Generic API Key", File: "tmp.py", Line: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`, Match: "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"", Secret: "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5", Entropy: 3.7906237, StartLine: 0, EndLine: 0, StartColumn: 22, EndColumn: 93, Tags: []string{}, }, }, }, "ignore finding - allowlist regex": { cfgName: "generic_with_py_path", fragment: Fragment{ Raw: `const Discord_Public_Key = "load2523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`, FilePath: "tmp.py", }, }, // Rule "rule - ignore path": { cfgName: "valid/rule_path_only", baselinePath: ".baseline.json", fragment: Fragment{ Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`, FilePath: ".baseline.json", }, }, "rule - detect path ": { cfgName: "valid/rule_path_only", fragment: Fragment{ Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`, FilePath: "tmp.py", }, expectedFindings: []report.Finding{ { Description: "Python Files", Match: "file detected: tmp.py", File: "tmp.py", RuleID: "python-files-only", Tags: []string{}, }, }, }, "rule - match based on entropy": { cfgName: "valid/rule_entropy_group", fragment: Fragment{ Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5" //const Discord_Public_Key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" `, FilePath: "tmp.go", }, expectedFindings: []report.Finding{ { RuleID: "discord-api-key", Description: "Discord API key", File: "tmp.go", Line: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`, Match: "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"", Secret: "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5", Entropy: 3.7906237, StartLine: 0, EndLine: 0, StartColumn: 7, EndColumn: 93, Tags: []string{}, }, }, }, // Allowlists "global allowlist - ignore regex": { cfgName: "valid/allowlist_global_regex", fragment: Fragment{ Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`, FilePath: "tmp.go", }, }, "global allowlist - detect, doesn't match all conditions": { cfgName: "valid/allowlist_global_multiple", fragment: Fragment{ Raw: ` const token = "mockSecret"; // const token = "changeit";`, FilePath: "config.txt", }, expectedFindings: []report.Finding{ { RuleID: "test", File: "config.txt", Line: "\nconst token = \"mockSecret\";", Match: `token = "mockSecret"`, Secret: "mockSecret", Entropy: 2.9219282, StartLine: 1, EndLine: 1, StartColumn: 8, EndColumn: 27, Tags: []string{}, }, }, }, "global allowlist - ignore, matches all conditions": { cfgName: "valid/allowlist_global_multiple", fragment: Fragment{ Raw: `token := "mockSecret";`, FilePath: "node_modules/config.txt", }, }, "global allowlist - detect path, doesn't match all conditions": { cfgName: "valid/allowlist_global_multiple", fragment: Fragment{ Raw: `var token = "fakeSecret";`, FilePath: "node_modules/config.txt", }, expectedFindings: []report.Finding{ { RuleID: "test", File: "node_modules/config.txt", Line: "var token = \"fakeSecret\";", Match: `token = "fakeSecret"`, Secret: "fakeSecret", Entropy: 2.8464394, StartLine: 0, EndLine: 0, StartColumn: 5, EndColumn: 24, Tags: []string{}, }, }, }, "allowlist - ignore commit": { cfgName: "valid/allowlist_rule_commit", fragment: Fragment{ Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`, FilePath: "tmp.go", CommitSHA: "allowthiscommit", }, }, "allowlist - ignore path": { cfgName: "valid/allowlist_rule_path", fragment: Fragment{ Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`, FilePath: "tmp.go", }, }, "allowlist - ignore path when extending": { cfgName: "valid/allowlist_rule_extend_default", fragment: Fragment{ Raw: `token = "aebfab88-7596-481d-82e8-c60c8f7de0c0"`, FilePath: "path/to/your/problematic/file.js", }, }, "allowlist - ignore regex": { cfgName: "valid/allowlist_rule_regex", fragment: Fragment{ Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`, FilePath: "tmp.go", }, }, "fragment level composite": { cfgName: "composite", fragment: Fragment{ Raw: multili, }, expectedFindings: []report.Finding{ { Description: "Primary rule", RuleID: "primary-rule", StartLine: 5, EndLine: 5, StartColumn: 5, EndColumn: 26, Line: "\n\t\t\tpassword = \"secret123\"", Match: `password = "secret123"`, Secret: "secret123", Entropy: 2.9477028846740723, Tags: []string{}, }, }, expectedAuxOutput: "Required: username-rule:1:admin\n", }, // Decoding "detect encoded": { cfgName: "encoded", fragment: Fragment{ Raw: encodedTestValues, FilePath: "tmp.go", }, expectedFindings: []report.Finding{ { // Plain text key captured by normal rule Description: "Private Key", Secret: "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----", Match: "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----", File: "tmp.go", Line: "\n-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----", RuleID: "private-key", Tags: []string{"key", "private"}, StartLine: 2, EndLine: 5, StartColumn: 2, EndColumn: 26, Entropy: 5.350665, }, { // Encoded key captured by custom b64 regex rule Description: "Private Key", Secret: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K", Match: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K", File: "tmp.go", Line: "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'", RuleID: "b64-encoded-private-key", Tags: []string{"key", "private"}, StartLine: 8, EndLine: 8, StartColumn: 16, EndColumn: 207, Entropy: 5.3861146, }, { // Encoded key captured by plain text rule using the decoder Description: "Private Key", Secret: "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----", Match: "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----", File: "tmp.go", Line: "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'", RuleID: "private-key", Tags: []string{"key", "private", "decoded:base64", "decode-depth:1"}, StartLine: 8, EndLine: 8, StartColumn: 16, EndColumn: 207, Entropy: 5.350665, }, { // Encoded Small secret at the end to make sure it's picked up by the decoding Description: "Small Secret", Secret: "small-secret", Match: "small-secret", File: "tmp.go", Line: "\nc21hbGwtc2VjcmV0", RuleID: "small-secret", Tags: []string{"small", "secret", "decoded:base64", "decode-depth:1"}, StartLine: 15, EndLine: 15, StartColumn: 2, EndColumn: 17, Entropy: 3.0849626, }, { // Secret where the decoded match goes outside the encoded value Description: "Overlapping", Secret: "decoded-secret-value00", Match: "secret=decoded-secret-value00", File: "tmp.go", Line: "\nsecret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==", RuleID: "overlapping", Tags: []string{"overlapping", "decoded:base64", "decode-depth:1"}, StartLine: 18, EndLine: 18, StartColumn: 2, EndColumn: 40, Entropy: 3.4428623, }, { // This just confirms that with no allowlist the pattern is detected (i.e. the regex is good) Description: "Make sure this would be detected with no allowlist", Secret: "lRqBK-z5kf4-please-ignore-me-X-XIJM2Pddw", Match: "password=\"lRqBK-z5kf4-please-ignore-me-X-XIJM2Pddw\"", File: "tmp.go", Line: "\npassword=\"bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw==\"", RuleID: "decoded-password-dont-ignore", Tags: []string{"decode-ignore", "decoded:base64", "decode-depth:1"}, StartLine: 23, EndLine: 23, StartColumn: 2, EndColumn: 68, Entropy: 4.5841837, }, { // Hex encoded data check Description: "Overlapping", Secret: "decoded-secret-valuevHEX", Match: "secret=decoded-secret-valuevHEX", File: "tmp.go", Line: "\nsecret=6465636F6465642D7365637265742D76616C756576484558", RuleID: "overlapping", Tags: []string{"overlapping", "decoded:hex", "decode-depth:1"}, StartLine: 26, EndLine: 26, StartColumn: 2, EndColumn: 56, Entropy: 3.6531072, }, { // handle partial encoded percent data Description: "Overlapping", Secret: "decoded-secret-valuev2", Match: "secret=decoded-secret-valuev2", File: "tmp.go", Line: "\nsecret=decoded-%73%65%63%72%65%74-valuev2", RuleID: "overlapping", Tags: []string{"overlapping", "decoded:percent", "decode-depth:1"}, StartLine: 30, EndLine: 30, StartColumn: 2, EndColumn: 42, Entropy: 3.4428623, }, { // handle partial encoded percent data Description: "Overlapping", Secret: "decoded-secret-valuev3", Match: "secret=decoded-secret-valuev3", File: "tmp.go", Line: "\nsecret=%64%65coded-%73%65%63%72%65%74-valuev3", RuleID: "overlapping", Tags: []string{"overlapping", "decoded:percent", "decode-depth:1"}, StartLine: 32, EndLine: 32, StartColumn: 2, EndColumn: 46, Entropy: 3.4428623, }, { // Encoded AWS config with a access key id inside a JWT Description: "AWS IAM Unique Identifier", Secret: "ASIAIOSFODNN7LXM10JI", Match: " ASIAIOSFODNN7LXM10JI", File: "tmp.go", Line: "\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA", RuleID: "aws-iam-unique-identifier", Tags: []string{"aws", "identifier", "decoded:base64", "decode-depth:2"}, StartLine: 11, EndLine: 11, StartColumn: 39, EndColumn: 344, Entropy: 3.6841838, }, { // Encoded AWS config with a secret access key inside a JWT Description: "AWS Secret Access Key", Secret: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEFUCDlEX1A", Match: "aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEFUCDlEX1A", File: "tmp.go", Line: "\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA", RuleID: "aws-secret-access-key", Tags: []string{"aws", "secret", "decoded:base64", "decode-depth:2"}, StartLine: 11, EndLine: 11, StartColumn: 39, EndColumn: 344, Entropy: 4.721928, }, { // Secret where the decoded match goes outside the encoded value and then encoded again Description: "Overlapping", Secret: "decoded-secret-value", Match: "secret=decoded-secret-value", File: "tmp.go", Line: "\nc2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=", RuleID: "overlapping", Tags: []string{"overlapping", "decoded:base64", "decode-depth:2"}, StartLine: 20, EndLine: 20, StartColumn: 2, EndColumn: 49, Entropy: 3.3037016, }, { // handle encodings that touch eachother Description: "Overlapping", Secret: "decoded-secret-valuev5", Match: "secret=decoded-secret-valuev5", File: "tmp.go", Line: "\nsecret%3d6465636F6465642D7365637265742D76616C75657635", RuleID: "overlapping", Tags: []string{"overlapping", "decoded:percent", "decoded:hex", "decode-depth:2"}, StartLine: 40, EndLine: 40, StartColumn: 2, EndColumn: 54, Entropy: 3.4428623, }, { // handle partial encoded percent data465642D7365637265742D76616C75657635 Description: "Overlapping", Secret: "decoded-secret-valuev4", Match: "secret=decoded-secret-valuev4", File: "tmp.go", Line: "\nc2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09", RuleID: "overlapping", Tags: []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:3"}, StartLine: 38, EndLine: 38, StartColumn: 2, EndColumn: 55, Entropy: 3.4428623, }, { // multiple percent encodings in a single layer base64 Description: "Overlapping", Secret: "decoded-secret-valuex86", Match: "secret=decoded-secret-valuex86", File: "tmp.go", Line: "\nsecret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY= # ends in x86", RuleID: "overlapping", Tags: []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:2"}, StartLine: 42, EndLine: 42, StartColumn: 2, EndColumn: 44, Entropy: 3.6381476, }, { // base64 encoded partially percent encoded value Description: "Overlapping", Secret: "decoded-secret-value", Match: "secret=decoded-secret-value", File: "tmp.go", Line: "\nsecret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU=", RuleID: "overlapping", Tags: []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:2"}, StartLine: 44, EndLine: 44, StartColumn: 2, EndColumn: 52, Entropy: 3.3037016, }, { // one of the lines above that went through... a lot Description: "Overlapping", Secret: "decoded-secret-value", Match: "secret=decoded-secret-value", File: "tmp.go", Line: "\nLook at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy?", RuleID: "overlapping", Tags: []string{"overlapping", "decoded:percent", "decoded:hex", "decoded:base64", "decode-depth:7"}, StartLine: 47, EndLine: 47, StartColumn: 22, EndColumn: 177, Entropy: 3.3037016, }, { // Multi percent encode two random characters close to the bounds of the base64 Description: "Overlapping", Secret: "decoded-secret-value", Match: "secret=decoded-secret-value", File: "tmp.go", Line: "\nsecret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU=", RuleID: "overlapping", Tags: []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:5"}, StartLine: 50, EndLine: 50, StartColumn: 2, EndColumn: 300, Entropy: 3.3037016, }, { // The similar to the above but also touching the edge of the base64 Description: "Overlapping", Secret: "decoded-secret-value", Match: "secret=decoded-secret-value", File: "tmp.go", Line: "\nsecret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34", RuleID: "overlapping", Tags: []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:4"}, StartLine: 52, EndLine: 52, StartColumn: 2, EndColumn: 86, Entropy: 3.3037016, }, { // The similar to the above but also touching and overlapping the base64 Description: "Overlapping", Secret: "decoded-secret-value", Match: "secret=decoded-secret-value", File: "tmp.go", Line: "\nsecret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34", RuleID: "overlapping", Tags: []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:4"}, StartLine: 54, EndLine: 54, StartColumn: 2, EndColumn: 88, Entropy: 3.3037016, }, }, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { viper.Reset() viper.AddConfigPath(configPath) viper.SetConfigName(tt.cfgName) viper.SetConfigType("toml") err := viper.ReadInConfig() require.NoError(t, err) var vc config.ViperConfig err = viper.Unmarshal(&vc) require.NoError(t, err) cfg, err := vc.Translate() cfg.Path = filepath.Join(configPath, tt.cfgName+".toml") assert.Equal(t, tt.wantError, err) d := NewDetector(cfg) d.MaxDecodeDepth = maxDecodeDepth d.baselinePath = tt.baselinePath findings := d.Detect(tt.fragment) compare(t, findings, tt.expectedFindings) // extremely goofy way to test auxiliary findings // capture stdout and print that sonabitch // TODO if tt.expectedAuxOutput != "" { capturedOutput := captureStdout(func() { for _, finding := range findings { finding.PrintRequiredFindings() } }) // Clean up the output for comparison (remove ANSI color codes) cleanOutput := stripANSI(capturedOutput) expectedClean := stripANSI(tt.expectedAuxOutput) assert.Equal(t, expectedClean, cleanOutput, "Auxiliary output should match") } }) } } func stripANSI(s string) string { ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`) return ansiRegex.ReplaceAllString(s, "") } func captureStdout(f func()) string { oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w f() w.Close() os.Stdout = oldStdout var buf bytes.Buffer io.Copy(&buf, r) return buf.String() } // TestFromGit tests the FromGit function func TestFromGit(t *testing.T) { // TODO: Fix this test on windows. if runtime.GOOS == "windows" { t.Skipf("TODO: this fails on Windows: [git] fatal: bad object refs/remotes/origin/main?") return } tests := []struct { cfgName string source string logOpts string expectedFindings []report.Finding }{ { source: filepath.Join(repoBasePath, "small"), cfgName: "simple", // the remote url is `git@github.com:gitleaks/test.git` expectedFindings: []report.Finding{ { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 19, EndColumn: 38, Line: "\n awsToken := \"AKIALALEMEL33243OLIA\"", Secret: "AKIALALEMEL33243OLIA", Match: "AKIALALEMEL33243OLIA", Entropy: 3.0841837, File: "main.go", Date: "2021-11-02T23:37:53Z", Commit: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587", Author: "Zachary Rice", Email: "zricer@protonmail.com", Message: "Accidentally add a secret", Tags: []string{"key", "AWS"}, Fingerprint: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587:main.go:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/1b6da43b82b22e4eaa10bcf8ee591e91abbfc587/main.go#L20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 9, EndLine: 9, StartColumn: 17, EndColumn: 36, Secret: "AKIALALEMEL33243OLIA", Match: "AKIALALEMEL33243OLIA", Line: "\n\taws_token := \"AKIALALEMEL33243OLIA\"", File: "foo/foo.go", Date: "2021-11-02T23:48:06Z", Commit: "491504d5a31946ce75e22554cc34203d8e5ff3ca", Author: "Zach Rice", Email: "zricer@protonmail.com", Message: "adding foo package with secret", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9", Link: "https://github.com/gitleaks/test/blob/491504d5a31946ce75e22554cc34203d8e5ff3ca/foo/foo.go#L9", }, }, }, { source: filepath.Join(repoBasePath, "small"), logOpts: "--all foo...", cfgName: "simple", expectedFindings: []report.Finding{ { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 9, EndLine: 9, StartColumn: 17, EndColumn: 36, Secret: "AKIALALEMEL33243OLIA", Line: "\n\taws_token := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Date: "2021-11-02T23:48:06Z", File: "foo/foo.go", Commit: "491504d5a31946ce75e22554cc34203d8e5ff3ca", Author: "Zach Rice", Email: "zricer@protonmail.com", Message: "adding foo package with secret", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9", Link: "https://github.com/gitleaks/test/blob/491504d5a31946ce75e22554cc34203d8e5ff3ca/foo/foo.go#L9", }, }, }, { source: filepath.Join(repoBasePath, "archives"), cfgName: "archives", expectedFindings: []report.Finding{ { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "main.go.zst", Commit: "db8789716fc664dbce0ed2d492570e92abf717a5", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:10:39Z", Message: "Add main.go.zst", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "db8789716fc664dbce0ed2d492570e92abf717a5:main.go.zst:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/db8789716fc664dbce0ed2d492570e92abf717a5/main.go.zst#L20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files.tar!files/api.go", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar!files/api.go:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files.tar!files/main.go", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar!files/main.go:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files.zip!files/api.go", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.zip!files/api.go:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files.zip!files/main.go", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.zip!files/main.go:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files.7z!files/api.go", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.7z!files/api.go:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files.7z!files/main.go", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.7z!files/main.go:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files.tar.zst!files/api.go", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.zst!files/api.go:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files.tar.zst!files/main.go", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.zst!files/main.go:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files/api.go", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/api.go:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files/main.go", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files/main.go.xz", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.xz:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files/main.go.zst", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.zst:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files/main.go.gz", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.gz:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files.tar.xz!files/api.go", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.xz!files/api.go:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "nested.tar.gz!archives/files.tar.xz!files/main.go", Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", Author: "Test User", Email: "user@example.com", Date: "2025-05-27T05:08:50Z", Message: "Add nested.tar.gz", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.xz!files/main.go:aws-access-key:20", Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", }, }, }, } moveDotGit(t, "dotGit", ".git") defer moveDotGit(t, ".git", "dotGit") for _, tt := range tests { t.Run(strings.Join([]string{tt.cfgName, tt.source, tt.logOpts}, "/"), func(t *testing.T) { viper.AddConfigPath(configPath) viper.SetConfigName("simple") viper.SetConfigType("toml") err := viper.ReadInConfig() require.NoError(t, err) var vc config.ViperConfig err = viper.Unmarshal(&vc) require.NoError(t, err) cfg, err := vc.Translate() require.NoError(t, err) detector := NewDetector(cfg) detector.MaxArchiveDepth = 8 var ignorePath string info, err := os.Stat(tt.source) require.NoError(t, err) if info.IsDir() { ignorePath = filepath.Join(tt.source, ".gitleaksignore") } else { ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore") } err = detector.AddGitleaksIgnore(ignorePath) require.NoError(t, err) gitCmd, err := sources.NewGitLogCmd(tt.source, tt.logOpts) require.NoError(t, err) remote := NewRemoteInfo(scm.UnknownPlatform, tt.source) findings, err := detector.DetectGit(gitCmd, remote) require.NoError(t, err) for _, f := range findings { f.Match = "" // remove lines cause copying and pasting them has some wack formatting } assert.ElementsMatch(t, tt.expectedFindings, findings) }) } } func TestFromGitStaged(t *testing.T) { tests := []struct { cfgName string source string logOpts string expectedFindings []report.Finding }{ { source: filepath.Join(repoBasePath, "staged"), cfgName: "simple", expectedFindings: []report.Finding{ { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 7, EndLine: 7, StartColumn: 18, EndColumn: 37, Line: "\n\taws_token2 := \"AKIALALEMEL33243OLIA\" // this one is not", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "api/api.go", SymlinkFile: "", Commit: "", Entropy: 3.0841837, Author: "", Email: "", Date: "0001-01-01T00:00:00Z", Message: "", Tags: []string{ "key", "AWS", }, Fingerprint: "api/api.go:aws-access-key:7", Link: "", }, }, }, } moveDotGit(t, "dotGit", ".git") defer moveDotGit(t, ".git", "dotGit") for _, tt := range tests { viper.AddConfigPath(configPath) viper.SetConfigName("simple") viper.SetConfigType("toml") err := viper.ReadInConfig() require.NoError(t, err) var vc config.ViperConfig err = viper.Unmarshal(&vc) require.NoError(t, err) cfg, err := vc.Translate() require.NoError(t, err) detector := NewDetector(cfg) err = detector.AddGitleaksIgnore(filepath.Join(tt.source, ".gitleaksignore")) require.NoError(t, err) gitCmd, err := sources.NewGitDiffCmd(tt.source, true) require.NoError(t, err) remote := NewRemoteInfo(scm.UnknownPlatform, tt.source) findings, err := detector.DetectGit(gitCmd, remote) require.NoError(t, err) for _, f := range findings { f.Match = "" // remove lines cause copying and pasting them has some wack formatting } assert.ElementsMatch(t, tt.expectedFindings, findings) } } // TestFromFiles tests the FromFiles function func TestFromFiles(t *testing.T) { tests := []struct { cfgName string source string expectedFindings []report.Finding }{ { source: filepath.Join(repoBasePath, "nogit"), cfgName: "simple", expectedFindings: []report.Finding{ { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/repos/nogit/main.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20", }, }, }, { source: filepath.Join(repoBasePath, "nogit", "main.go"), cfgName: "simple", expectedFindings: []report.Finding{ { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/repos/nogit/main.go", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20", }, }, }, { source: filepath.Join(repoBasePath, "nogit", "api.go"), cfgName: "simple", expectedFindings: []report.Finding{}, }, { source: filepath.Join(repoBasePath, "nogit", ".env.prod"), cfgName: "generic", expectedFindings: []report.Finding{ { RuleID: "generic-api-key", Description: "Generic API Key", StartLine: 4, EndLine: 4, StartColumn: 5, EndColumn: 35, Line: "\nDB_PASSWORD=8ae31cacf141669ddfb5da", Match: "PASSWORD=8ae31cacf141669ddfb5da", Secret: "8ae31cacf141669ddfb5da", File: "../testdata/repos/nogit/.env.prod", Tags: []string{}, Entropy: 3.5383105, Fingerprint: "../testdata/repos/nogit/.env.prod:generic-api-key:4", }, }, }, } for _, tt := range tests { t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) { viper.AddConfigPath(configPath) viper.SetConfigName(tt.cfgName) viper.SetConfigType("toml") err := viper.ReadInConfig() require.NoError(t, err) var vc config.ViperConfig err = viper.Unmarshal(&vc) require.NoError(t, err) cfg, _ := vc.Translate() detector := NewDetector(cfg) info, err := os.Stat(tt.source) require.NoError(t, err) var ignorePath string if info.IsDir() { ignorePath = filepath.Join(tt.source, ".gitleaksignore") } else { ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore") } err = detector.AddGitleaksIgnore(ignorePath) require.NoError(t, err) detector.FollowSymlinks = true paths, err := sources.DirectoryTargets(tt.source, detector.Sema, true, cfg.Allowlists) require.NoError(t, err) findings, err := detector.DetectFiles(paths) require.NoError(t, err) // TODO: Temporary mitigation. // https://github.com/gitleaks/gitleaks/issues/1641 normalizedFindings := make([]report.Finding, len(findings)) for i, f := range findings { if strings.HasSuffix(f.Line, "\r") { f.Line = strings.ReplaceAll(f.Line, "\r", "") } if strings.HasSuffix(f.Match, "\r") { f.EndColumn = f.EndColumn - 1 f.Match = strings.ReplaceAll(f.Match, "\r", "") } normalizedFindings[i] = f } assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings) }) } } func TestDetectWithArchives(t *testing.T) { tests := []struct { cfgName string source string expireContext bool expectedFindings []report.Finding }{ { source: filepath.Join(archivesBasePath, "this-path-does-not-exist"), cfgName: "archives", expectedFindings: []report.Finding{}, }, { source: filepath.Join(archivesBasePath, "files"), cfgName: "archives", expectedFindings: []report.Finding{ { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files/api.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files/api.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files/main.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files/main.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files/main.go.gz", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files/main.go.gz:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files/main.go.xz", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files/main.go.xz:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files/main.go.zst", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files/main.go.zst:aws-access-key:20", }, }, }, { source: filepath.Join(archivesBasePath, "files.7z"), cfgName: "archives", expectedFindings: []report.Finding{ { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files.7z!files/api.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files.7z!files/api.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files.7z!files/main.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files.7z!files/main.go:aws-access-key:20", }, }, }, { source: filepath.Join(archivesBasePath, "files.tar"), cfgName: "archives", expectedFindings: []report.Finding{ { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files.tar!files/api.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files.tar!files/api.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files.tar!files/main.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files.tar!files/main.go:aws-access-key:20", }, }, }, { source: filepath.Join(archivesBasePath, "files.tar.xz"), cfgName: "archives", expectedFindings: []report.Finding{ { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files.tar.xz!files/api.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files.tar.xz!files/api.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files.tar.xz!files/main.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files.tar.xz!files/main.go:aws-access-key:20", }, }, }, { source: filepath.Join(archivesBasePath, "files.tar.zst"), cfgName: "archives", expectedFindings: []report.Finding{ { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files.tar.zst!files/api.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files.tar.zst!files/api.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files.tar.zst!files/main.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files.tar.zst!files/main.go:aws-access-key:20", }, }, }, { source: filepath.Join(archivesBasePath, "files.zip"), cfgName: "archives", expectedFindings: []report.Finding{ { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files.zip!files/api.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files.zip!files/api.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/files.zip!files/main.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/files.zip!files/main.go:aws-access-key:20", }, }, }, { source: filepath.Join(archivesBasePath, "nested.tar.gz"), cfgName: "archives", expectedFindings: []report.Finding{ { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files.tar!files/api.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar!files/api.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files.tar!files/main.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar!files/main.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files.zip!files/api.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.zip!files/api.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files.zip!files/main.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.zip!files/main.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files.7z!files/api.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.7z!files/api.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files.7z!files/main.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.7z!files/main.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files.tar.zst!files/api.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.zst!files/api.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files.tar.zst!files/main.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.zst!files/main.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files/api.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/api.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files/main.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/main.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files/main.go.xz", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/main.go.xz:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files/main.go.zst", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/main.go.zst:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files/main.go.gz", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/main.go.gz:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/api.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/api.go:aws-access-key:20", }, { RuleID: "aws-access-key", Description: "AWS Access Key", StartLine: 20, EndLine: 20, StartColumn: 16, EndColumn: 35, Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", Match: "AKIALALEMEL33243OLIA", Secret: "AKIALALEMEL33243OLIA", File: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/main.go", SymlinkFile: "", Tags: []string{"key", "AWS"}, Entropy: 3.0841837, Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/main.go:aws-access-key:20", }, }, }, { source: filepath.Join(archivesBasePath, "nested.tar.gz"), cfgName: "archives", expireContext: true, expectedFindings: []report.Finding{}, }, } for _, tt := range tests { t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) { viper.AddConfigPath(configPath) viper.SetConfigName(tt.cfgName) viper.SetConfigType("toml") err := viper.ReadInConfig() require.NoError(t, err) var vc config.ViperConfig err = viper.Unmarshal(&vc) require.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) if tt.expireContext { cancel() } cfg, _ := vc.Translate() detector := NewDetectorContext(ctx, cfg) detector.MaxArchiveDepth = 8 findings, err := detector.DetectSource( ctx, &sources.Files{ Path: tt.source, Sema: detector.Sema, Config: &cfg, MaxArchiveDepth: detector.MaxArchiveDepth, }, ) if tt.expireContext { require.EqualError(t, err, "context canceled") } else { cancel() require.NoError(t, err) } // TODO: Temporary mitigation. // https://github.com/gitleaks/gitleaks/issues/1641 normalizedFindings := make([]report.Finding, len(findings)) for i, f := range findings { if strings.HasSuffix(f.Line, "\r") { f.Line = strings.ReplaceAll(f.Line, "\r", "") } if strings.HasSuffix(f.Match, "\r") { f.EndColumn = f.EndColumn - 1 f.Match = strings.ReplaceAll(f.Match, "\r", "") } normalizedFindings[i] = f } assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings) }) } } func TestDetectWithSymlinks(t *testing.T) { // TODO: Fix this test on windows. if runtime.GOOS == "windows" { t.Skipf("TODO: this returns no results on windows, I'm not sure why.") return } tests := []struct { cfgName string source string expectedFindings []report.Finding }{ { source: filepath.Join(repoBasePath, "symlinks/file_symlink"), cfgName: "simple", expectedFindings: []report.Finding{ { RuleID: "apkey", Description: "Asymmetric Private Key", StartLine: 1, EndLine: 1, StartColumn: 1, EndColumn: 35, Match: "-----BEGIN OPENSSH PRIVATE KEY-----", Secret: "-----BEGIN OPENSSH PRIVATE KEY-----", Line: "-----BEGIN OPENSSH PRIVATE KEY-----", File: "../testdata/repos/symlinks/source_file/id_ed25519", SymlinkFile: "../testdata/repos/symlinks/file_symlink/symlinked_id_ed25519", Tags: []string{"key", "AsymmetricPrivateKey"}, Entropy: 3.587164, Fingerprint: "../testdata/repos/symlinks/source_file/id_ed25519:apkey:1", }, }, }, } for _, tt := range tests { viper.AddConfigPath(configPath) viper.SetConfigName("simple") viper.SetConfigType("toml") err := viper.ReadInConfig() require.NoError(t, err) var vc config.ViperConfig err = viper.Unmarshal(&vc) require.NoError(t, err) cfg, _ := vc.Translate() detector := NewDetector(cfg) detector.FollowSymlinks = true paths, err := sources.DirectoryTargets(tt.source, detector.Sema, true, cfg.Allowlists) require.NoError(t, err) findings, err := detector.DetectFiles(paths) require.NoError(t, err) assert.ElementsMatch(t, tt.expectedFindings, findings) } } func TestDetectRuleAllowlist(t *testing.T) { cases := map[string]struct { fragment Fragment allowlist *config.Allowlist expected []report.Finding }{ // Commit / path "commit allowed": { fragment: Fragment{ CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", }, allowlist: &config.Allowlist{ Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"}, }, }, "path allowed": { fragment: Fragment{ FilePath: "package-lock.json", }, allowlist: &config.Allowlist{ Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)}, }, }, "commit AND path allowed": { fragment: Fragment{ CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", FilePath: "package-lock.json", }, allowlist: &config.Allowlist{ MatchCondition: config.AllowlistMatchAnd, Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"}, Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)}, }, }, "commit AND path NOT allowed": { fragment: Fragment{ CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", FilePath: "package.json", }, allowlist: &config.Allowlist{ MatchCondition: config.AllowlistMatchAnd, Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"}, Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)}, }, expected: []report.Finding{ { StartLine: 1, EndLine: 1, StartColumn: 18, EndColumn: 28, Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';", Match: "Summer2024!", Secret: "Summer2024!", File: "package.json", Commit: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", Entropy: 3.095795154571533, RuleID: "test-rule", }, }, }, "commit AND path NOT allowed - other conditions": { fragment: Fragment{ CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", FilePath: "package-lock.json", }, allowlist: &config.Allowlist{ MatchCondition: config.AllowlistMatchAnd, Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"}, Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)}, Regexes: []*regexp.Regexp{regexp.MustCompile("password")}, }, expected: []report.Finding{ { StartLine: 1, EndLine: 1, StartColumn: 18, EndColumn: 28, Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';", Match: "Summer2024!", Secret: "Summer2024!", File: "package-lock.json", Commit: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", Entropy: 3.095795154571533, RuleID: "test-rule", }, }, }, "commit OR path allowed": { fragment: Fragment{ CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", FilePath: "package-lock.json", }, allowlist: &config.Allowlist{ MatchCondition: config.AllowlistMatchOr, Commits: []string{"704178e7dca77ff143778a31cff0fc192d59b030"}, Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)}, }, }, // Regex / stopwords "regex allowed": { fragment: Fragment{}, allowlist: &config.Allowlist{ Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)}, }, }, "stopwords allowed": { fragment: Fragment{}, allowlist: &config.Allowlist{ StopWords: []string{"summer"}, }, }, "regex AND stopword allowed": { fragment: Fragment{}, allowlist: &config.Allowlist{ MatchCondition: config.AllowlistMatchAnd, Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)}, StopWords: []string{"2024"}, }, }, "regex AND stopword allowed - other conditions": { fragment: Fragment{ CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", FilePath: "config.js", }, allowlist: &config.Allowlist{ MatchCondition: config.AllowlistMatchAnd, Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"}, Paths: []*regexp.Regexp{regexp.MustCompile(`config.js`)}, Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)}, StopWords: []string{"2024"}, }, }, "regex AND stopword NOT allowed - non-git, other conditions": { fragment: Fragment{ FilePath: "config.js", }, allowlist: &config.Allowlist{ MatchCondition: config.AllowlistMatchAnd, Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"}, Paths: []*regexp.Regexp{regexp.MustCompile(`config.js`)}, Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)}, StopWords: []string{"2024"}, }, expected: []report.Finding{ { StartLine: 1, EndLine: 1, StartColumn: 18, EndColumn: 28, Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';", Match: "Summer2024!", Secret: "Summer2024!", File: "config.js", Entropy: 3.095795154571533, RuleID: "test-rule", }, }, }, "regex AND stopword NOT allowed": { fragment: Fragment{}, allowlist: &config.Allowlist{ MatchCondition: config.AllowlistMatchAnd, Regexes: []*regexp.Regexp{ regexp.MustCompile(`(?i)winter.+`), }, StopWords: []string{"2024"}, }, expected: []report.Finding{ { StartLine: 1, EndLine: 1, StartColumn: 18, EndColumn: 28, Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';", Match: "Summer2024!", Secret: "Summer2024!", Entropy: 3.095795154571533, RuleID: "test-rule", }, }, }, "regex AND stopword NOT allowed - other conditions": { fragment: Fragment{ CommitSHA: "a060c9d2d5e90c992763f1bd4c3cd2a6f121241b", FilePath: "config.js", }, allowlist: &config.Allowlist{ MatchCondition: config.AllowlistMatchAnd, Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"}, Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)}, Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)winter.+`)}, StopWords: []string{"2024"}, }, expected: []report.Finding{ { StartLine: 1, EndLine: 1, StartColumn: 18, EndColumn: 28, Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';", Match: "Summer2024!", Secret: "Summer2024!", File: "config.js", Commit: "a060c9d2d5e90c992763f1bd4c3cd2a6f121241b", Entropy: 3.095795154571533, RuleID: "test-rule", }, }, }, "regex OR stopword allowed": { fragment: Fragment{}, allowlist: &config.Allowlist{ MatchCondition: config.AllowlistMatchOr, Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)}, StopWords: []string{"winter"}, }, }, } raw := `let username = 'james@mail.com'; let password = 'Summer2024!';` for name, tc := range cases { t.Run(name, func(t *testing.T) { err := tc.allowlist.Validate() require.NoError(t, err) rule := config.Rule{ RuleID: "test-rule", Regex: regexp.MustCompile(`Summer2024!`), Allowlists: []*config.Allowlist{ tc.allowlist, }, } d, err := NewDetectorDefaultConfig() require.NoError(t, err) f := tc.fragment f.Raw = raw actual := d.detectRule(f, raw, rule, []*codec.EncodedSegment{}) compare(t, tc.expected, actual) }) } } func moveDotGit(t *testing.T, from, to string) { t.Helper() repoDirs, err := os.ReadDir("../testdata/repos") require.NoError(t, err) for _, dir := range repoDirs { if to == ".git" { _, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), "dotGit")) if os.IsNotExist(err) { // dont want to delete the only copy of .git accidentally continue } _ = os.RemoveAll(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), ".git")) } if !dir.IsDir() { continue } _, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from)) if os.IsNotExist(err) { continue } err = os.Rename(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from), fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), to)) require.NoError(t, err) } } // region Windows-specific tests[] func TestNormalizeGitleaksIgnorePaths(t *testing.T) { d, err := NewDetectorDefaultConfig() require.NoError(t, err) err = d.AddGitleaksIgnore("../testdata/gitleaksignore/.windowspaths") require.NoError(t, err) assert.Len(t, d.gitleaksIgnore, 3) expected := map[string]struct{}{ "foo/bar/gitleaks-false-positive.yaml:aws-access-token:4": {}, "foo/bar/gitleaks-false-positive.yaml:aws-access-token:5": {}, "b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:73": {}, } assert.ElementsMatch(t, maps.Keys(d.gitleaksIgnore), maps.Keys(expected)) } func TestWindowsFileSeparator_RulePath(t *testing.T) { unixRule := config.Rule{ RuleID: "test-rule", Path: regexp.MustCompile(`(^|/)\.m2/settings\.xml`), } windowsRule := config.Rule{ RuleID: "test-rule", Path: regexp.MustCompile(`(^|\\)\.m2\\settings\.xml`), } expected := []report.Finding{ { RuleID: "test-rule", Match: "file detected: .m2/settings.xml", File: ".m2/settings.xml", }, } tests := map[string]struct { fragment Fragment rule config.Rule expected []report.Finding }{ // unix rule "unix rule - unix path separator": { fragment: Fragment{ FilePath: `.m2/settings.xml`, }, rule: unixRule, expected: expected, }, "unix rule - windows path separator": { fragment: Fragment{ FilePath: `.m2/settings.xml`, WindowsFilePath: `.m2\settings.xml`, }, rule: unixRule, expected: expected, }, "unix regex+path rule - windows path separator": { fragment: Fragment{ Raw: `s3cr3t`, FilePath: `.m2/settings.xml`, }, rule: config.Rule{ RuleID: "test-rule", Regex: regexp.MustCompile(`(.+?)`), Path: regexp.MustCompile(`(^|/)\.m2/settings\.xml`), }, expected: []report.Finding{ { RuleID: "test-rule", StartColumn: 1, EndColumn: 27, Line: "s3cr3t", Match: "s3cr3t", Secret: "s3cr3t", Entropy: 2.251629114151001, File: ".m2/settings.xml", }, }, }, // windows rule "windows rule - unix path separator": { fragment: Fragment{ FilePath: `.m2/settings.xml`, }, rule: windowsRule, // This never worked, and continues not to work. // Paths should be normalized to use Unix file separators. expected: nil, }, "windows rule - windows path separator": { fragment: Fragment{ FilePath: `.m2/settings.xml`, WindowsFilePath: `.m2\settings.xml`, }, rule: windowsRule, expected: expected, }, "windows regex+path rule - windows path separator": { fragment: Fragment{ Raw: `s3cr3t`, FilePath: `.m2/settings.xml`, WindowsFilePath: `.m2\settings.xml`, }, rule: config.Rule{ RuleID: "test-rule", Regex: regexp.MustCompile(`(.+?)`), Path: regexp.MustCompile(`(^|\\)\.m2\\settings\.xml`), }, expected: []report.Finding{ { RuleID: "test-rule", StartColumn: 1, EndColumn: 27, Line: "s3cr3t", Match: "s3cr3t", Secret: "s3cr3t", Entropy: 2.251629114151001, File: ".m2/settings.xml", }, }}, } d, err := NewDetectorDefaultConfig() require.NoError(t, err) for name, test := range tests { t.Run(name, func(t *testing.T) { actual := d.detectRule(test.fragment, test.fragment.Raw, test.rule, []*codec.EncodedSegment{}) compare(t, test.expected, actual) }) } } func TestWindowsFileSeparator_RuleAllowlistPaths(t *testing.T) { tests := map[string]struct { fragment Fragment rule config.Rule expected []report.Finding }{ // unix "unix path separator - unix rule - OR allowlist path-only": { fragment: Fragment{ Raw: `value: "s3cr3t"`, FilePath: `ignoreme/unix.txt`, }, rule: config.Rule{ RuleID: "unix-rule", Regex: regexp.MustCompile(`s3cr3t`), Allowlists: []*config.Allowlist{ { Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)}, }, }, }, expected: nil, }, "unix path separator - windows rule - OR allowlist path-only": { fragment: Fragment{ Raw: `value: "s3cr3t"`, FilePath: `ignoreme/unix.txt`, }, rule: config.Rule{ RuleID: "windows-rule", Regex: regexp.MustCompile(`s3cr3t`), Allowlists: []*config.Allowlist{ { Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)}, }, }, }, // Windows separators in regex don't work for unix. expected: []report.Finding{ { RuleID: "windows-rule", StartColumn: 9, EndColumn: 14, Line: `value: "s3cr3t"`, Match: `s3cr3t`, Secret: `s3cr3t`, File: "ignoreme/unix.txt", Entropy: 2.251629114151001, }, }, }, "unix path separator - unix rule - AND allowlist path+stopwords": { fragment: Fragment{ Raw: `value: "f4k3s3cr3t"`, FilePath: `ignoreme/unix.txt`, }, rule: config.Rule{ RuleID: "unix-rule", Regex: regexp.MustCompile(`value: "[^"]+"`), Allowlists: []*config.Allowlist{ { MatchCondition: config.AllowlistMatchAnd, Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)}, StopWords: []string{"f4k3"}, }, }, }, expected: nil, }, "unix path separator - windows rule - AND allowlist path+stopwords": { fragment: Fragment{ Raw: `value: "f4k3s3cr3t"`, FilePath: `ignoreme/unix.txt`, }, rule: config.Rule{ RuleID: "windows-rule", Regex: regexp.MustCompile(`value: "[^"]+"`), Allowlists: []*config.Allowlist{ { MatchCondition: config.AllowlistMatchAnd, Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)}, StopWords: []string{"f4k3"}, }, }, }, expected: []report.Finding{ { RuleID: "windows-rule", StartColumn: 1, EndColumn: 19, Line: `value: "f4k3s3cr3t"`, Match: `value: "f4k3s3cr3t"`, Secret: `value: "f4k3s3cr3t"`, File: "ignoreme/unix.txt", Entropy: 3.892407178878784, }, }, }, // windows "windows path separator - unix rule - OR allowlist path-only": { fragment: Fragment{ Raw: `value: "s3cr3t"`, FilePath: `ignoreme/windows.txt`, WindowsFilePath: `ignoreme\windows.txt`, }, rule: config.Rule{ RuleID: "unix-rule", Regex: regexp.MustCompile(`s3cr3t`), Allowlists: []*config.Allowlist{ { Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)}, }, }, }, expected: nil, }, "windows path separator - windows rule - OR allowlist path-only": { fragment: Fragment{ Raw: `value: "s3cr3t"`, FilePath: `ignoreme/windows.txt`, WindowsFilePath: `ignoreme\windows.txt`, }, rule: config.Rule{ RuleID: "windows-rule", Regex: regexp.MustCompile(`s3cr3t`), Allowlists: []*config.Allowlist{ { Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)}, }, }, }, expected: nil, }, "windows path separator - unix rule - AND allowlist path+stopwords": { fragment: Fragment{ Raw: `value: "f4k3s3cr3t"`, FilePath: `ignoreme/unix.txt`, WindowsFilePath: `ignoreme\windows.txt`, }, rule: config.Rule{ RuleID: "unix-rule", Regex: regexp.MustCompile(`value: "[^"]+"`), Allowlists: []*config.Allowlist{ { MatchCondition: config.AllowlistMatchAnd, Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)}, StopWords: []string{"f4k3"}, }, }, }, expected: nil, }, "windows path separator - windows rule - AND allowlist path+stopwords": { fragment: Fragment{ Raw: `value: "f4k3s3cr3t"`, FilePath: `ignoreme/unix.txt`, WindowsFilePath: `ignoreme\windows.txt`, }, rule: config.Rule{ RuleID: "windows-rule", Regex: regexp.MustCompile(`value: "[^"]+"`), Allowlists: []*config.Allowlist{ { MatchCondition: config.AllowlistMatchAnd, Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)}, StopWords: []string{"f4k3"}, }, }, }, expected: nil, }, } d, err := NewDetectorDefaultConfig() require.NoError(t, err) for name, test := range tests { t.Run(name, func(t *testing.T) { actual := d.detectRule(test.fragment, test.fragment.Raw, test.rule, []*codec.EncodedSegment{}) compare(t, test.expected, actual) }) } }