From fb517c9a6f111147686aa829eb69beab37d5e034 Mon Sep 17 00:00:00 2001 From: RebeccaMahany Date: Thu, 16 Apr 2026 15:22:12 -0400 Subject: [PATCH 1/3] Move test --- ee/tables/secretscan/table_test.go | 84 +++++++++++++++--------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/ee/tables/secretscan/table_test.go b/ee/tables/secretscan/table_test.go index 67b4b56c3..e70151848 100644 --- a/ee/tables/secretscan/table_test.go +++ b/ee/tables/secretscan/table_test.go @@ -370,6 +370,48 @@ func extractTestData(tb testing.TB) string { return filepath.Join(tempDir, "test_data") } +func Test_isEncryptedJWTFamilyValue(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + testCaseName string + encryptedJWT string + expectedIsEncryptedJWT bool + }{ + { + testCaseName: "encrypted JWK, Appendix C RFC example", // https://datatracker.ietf.org/doc/html/rfc7517#appendix-C + encryptedJWT: "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJwMnMiOiIyV0NUY0paMVJ2ZF9DSnVKcmlwUTF3IiwicDJjIjo0MDk2LCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiY3R5IjoiandrK2pzb24ifQ.TrqXOwuNUfDV9VPTNbyGvEJ9JMjefAVn-TR1uIxR9p6hsRQh9Tk7BA.Ye9j1qs22DmRSAddIh-VnA.AwhB8lxrlKjFn02LGWEqg27H4Tg9fyZAbFv3p5ZicHpj64QyHC44qqlZ3JEmnZTgQowIqZJ13jbyHB8LgePiqUJ1hf6M2HPLgzw8L-mEeQ0jvDUTrE07NtOerBk8bwBQyZ6g0kQ3DEOIglfYxV8-FJvNBYwbqN1Bck6d_i7OtjSHV-8DIrp-3JcRIe05YKy3Oi34Z_GOiAc1EK21B11c_AE11PII_wvvtRiUiG8YofQXakWd1_O98Kap-UgmyWPfreUJ3lJPnbD4Ve95owEfMGLOPflo2MnjaTDCwQokoJ_xplQ2vNPz8iguLcHBoKllyQFJL2mOWBwqhBo9Oj-O800as5mmLsvQMTflIrIEbbTMzHMBZ8EFW9fWwwFu0DWQJGkMNhmBZQ-3lvqTc-M6-gWA6D8PDhONfP2Oib2HGizwG1iEaX8GRyUpfLuljCLIe1DkGOewhKuKkZh04DKNM5Nbugf2atmU9OP0Ldx5peCUtRG1gMVl7Qup5ZXHTjgPDr5b2N731UooCGAUqHdgGhg0JVJ_ObCTdjsH4CF1SJsdUhrXvYx3HJh2Xd7CwJRzU_3Y1GxYU6-s3GFPbirfqqEipJDBTHpcoCmyrwYjYHFgnlqBZRotRrS95g8F95bRXqsaDY7UgQGwBQBwy665d0zpvTasvfXf_c0MWAl-neFaKOW_Px6g4EUDjG1GWSXV9cLStLw_0ovdApDIFLHYHePyagyHjouQUuGiq7BsYwYrwaF06tgB8hV8omLNfMEmDPJaZUzMuHw6tBDwGkzD-tS_ub9hxrpJ4UsOWnt5rGUyoN2N_c1-TQlXxm5oto14MxnoAyBQBpwIEgSH3Y4ZhwKBhHPjSo0cdwuNdYbGPpb-YUvF-2NZzODiQ1OvWQBRHSbPWYz_xbGkgD504LRtqRwCO7CC_CyyURi1sEssPVsMJRX_U4LFEOc82TiDdqjKOjRUfKK5rqLi8nBE9soQ0DSaOoFQZiGrBrqxDsNYiAYAmxxkos-i3nX4qtByVx85sCE5U_0MqG7COxZWMOPEFrDaepUV-cOyrvoUIng8i8ljKBKxETY2BgPegKBYCxsAUcAkKamSCC9AiBxA0UOHyhTqtlvMksO7AEhNC2-YzPyx1FkhMoS4LLe6E_pFsMlmjA6P1NSge9C5G5tETYXGAn6b1xZbHtmwrPScro9LWhVmAaA7_bxYObnFUxgWtK4vzzQBjZJ36UTk4OTB-JvKWgfVWCFsaw5WCHj6Oo4jpO7d2yN7WMfAj2hTEabz9wumQ0TMhBduZ-QON3pYObSy7TSC1vVme0NJrwF_cJRehKTFmdlXGVldPxZCplr7ZQqRQhF8JP-l4mEQVnCaWGn9ONHlemczGOS-A-wwtnmwjIB1V_vgJRf4FdpV-4hUk4-QLpu3-1lWFxrtZKcggq3tWTduRo5_QebQbUUT_VSCgsFcOmyWKoj56lbxthN19hq1XGWbLGfrrR6MWh23vk01zn8FVwi7uFwEnRYSafsnWLa1Z5TpBj9GvAdl2H9NHwzpB5NqHpZNkQ3NMDj13Fn8fzO0JB83Etbm_tnFQfcb13X3bJ15Cz-Ww1MGhvIpGGnMBT_ADp9xSIyAM9dQ1yeVXk-AIgWBUlN5uyWSGyCxp0cJwx7HxM38z0UIeBu-MytL-eqndM7LxytsVzCbjOTSVRmhYEMIzUAnS1gs7uMQAGRdgRIElTJESGMjb_4bZq9s6Ve1LKkSi0_QDsrABaLe55UY0zF4ZSfOV5PMyPtocwV_dcNPlxLgNAD1BFX_Z9kAdMZQW6fAmsfFle0zAoMe4l9pMESH0JB4sJGdCKtQXj1cXNydDYozF7l8H00BV_Er7zd6VtIw0MxwkFCTatsv_R-GsBCH218RgVPsfYhwVuT8R4HarpzsDBufC4r8_c8fc9Z278sQ081jFjOja6L2x0N_ImzFNXU6xwO-Ska-QeuvYZ3X_L31ZOX4Llp-7QSfgDoHnOxFv1Xws-D5mDHD3zxOup2b2TppdKTZb9eW2vxUVviM8OI9atBfPKMGAOv9omA-6vv5IxUH0-lWMiHLQ_g8vnswp-Jav0c4t6URVUzujNOoNd_CBGGVnHiJTCHl88LQxsqLHHIu4Fz-U2SGnlxGTj0-ihit2ELGRv4vO8E1BosTmf0cx3qgG0Pq0eOLBDIHsrdZ_CCAiTc0HVkMbyq1M6qEhM-q5P6y1QCIrwg.0HFmhOzsQ98nNWJjIHkR7A", + expectedIsEncryptedJWT: true, + }, + { + testCaseName: "JWE, Appendix A.1 example", // https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.1 + encryptedJWT: "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg.48V1_ALb6US04U3b.5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A.XFBoMYUZodetZdvTiFvSkQ", + expectedIsEncryptedJWT: true, + }, + { + testCaseName: "JWE, Appendix A.2 example", // https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.2 + encryptedJWT: "eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPhcCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPgwCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A.AxY8DCtDaGlsbGljb3RoZQ.KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.9hH0vgRfYgPnAHOd8stkvw", + expectedIsEncryptedJWT: true, + }, + { + testCaseName: "JWE, Appendix A.3 example", // https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.3 + encryptedJWT: "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ.AxY8DCtDaGlsbGljb3RoZQ.KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.U0m_YmjN04DJvceFICbCVQ", + expectedIsEncryptedJWT: true, + }, + { + testCaseName: "unencrypted JWT", + encryptedJWT: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.gog41qgIIHkH2h-83gwRq5-NYOciZ4DgN4ulHFSkh6k", + expectedIsEncryptedJWT: false, + }, + } { + t.Run(tt.testCaseName, func(t *testing.T) { + t.Parallel() + + require.Equal(t, tt.expectedIsEncryptedJWT, isEncryptedJWTFamilyValue(report.Finding{Secret: tt.encryptedJWT})) + }) + } +} + // Benchmarks func BenchmarkSecretScanDirectory(b *testing.B) { @@ -417,48 +459,6 @@ func BenchmarkSecretScanFile(b *testing.B) { ci.ReportNonGolangMemoryUsage(b, baselineStats) } -func Test_isEncryptedJWTFamilyValue(t *testing.T) { - t.Parallel() - - for _, tt := range []struct { - testCaseName string - encryptedJWT string - expectedIsEncryptedJWT bool - }{ - { - testCaseName: "encrypted JWK, Appendix C RFC example", // https://datatracker.ietf.org/doc/html/rfc7517#appendix-C - encryptedJWT: "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJwMnMiOiIyV0NUY0paMVJ2ZF9DSnVKcmlwUTF3IiwicDJjIjo0MDk2LCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiY3R5IjoiandrK2pzb24ifQ.TrqXOwuNUfDV9VPTNbyGvEJ9JMjefAVn-TR1uIxR9p6hsRQh9Tk7BA.Ye9j1qs22DmRSAddIh-VnA.AwhB8lxrlKjFn02LGWEqg27H4Tg9fyZAbFv3p5ZicHpj64QyHC44qqlZ3JEmnZTgQowIqZJ13jbyHB8LgePiqUJ1hf6M2HPLgzw8L-mEeQ0jvDUTrE07NtOerBk8bwBQyZ6g0kQ3DEOIglfYxV8-FJvNBYwbqN1Bck6d_i7OtjSHV-8DIrp-3JcRIe05YKy3Oi34Z_GOiAc1EK21B11c_AE11PII_wvvtRiUiG8YofQXakWd1_O98Kap-UgmyWPfreUJ3lJPnbD4Ve95owEfMGLOPflo2MnjaTDCwQokoJ_xplQ2vNPz8iguLcHBoKllyQFJL2mOWBwqhBo9Oj-O800as5mmLsvQMTflIrIEbbTMzHMBZ8EFW9fWwwFu0DWQJGkMNhmBZQ-3lvqTc-M6-gWA6D8PDhONfP2Oib2HGizwG1iEaX8GRyUpfLuljCLIe1DkGOewhKuKkZh04DKNM5Nbugf2atmU9OP0Ldx5peCUtRG1gMVl7Qup5ZXHTjgPDr5b2N731UooCGAUqHdgGhg0JVJ_ObCTdjsH4CF1SJsdUhrXvYx3HJh2Xd7CwJRzU_3Y1GxYU6-s3GFPbirfqqEipJDBTHpcoCmyrwYjYHFgnlqBZRotRrS95g8F95bRXqsaDY7UgQGwBQBwy665d0zpvTasvfXf_c0MWAl-neFaKOW_Px6g4EUDjG1GWSXV9cLStLw_0ovdApDIFLHYHePyagyHjouQUuGiq7BsYwYrwaF06tgB8hV8omLNfMEmDPJaZUzMuHw6tBDwGkzD-tS_ub9hxrpJ4UsOWnt5rGUyoN2N_c1-TQlXxm5oto14MxnoAyBQBpwIEgSH3Y4ZhwKBhHPjSo0cdwuNdYbGPpb-YUvF-2NZzODiQ1OvWQBRHSbPWYz_xbGkgD504LRtqRwCO7CC_CyyURi1sEssPVsMJRX_U4LFEOc82TiDdqjKOjRUfKK5rqLi8nBE9soQ0DSaOoFQZiGrBrqxDsNYiAYAmxxkos-i3nX4qtByVx85sCE5U_0MqG7COxZWMOPEFrDaepUV-cOyrvoUIng8i8ljKBKxETY2BgPegKBYCxsAUcAkKamSCC9AiBxA0UOHyhTqtlvMksO7AEhNC2-YzPyx1FkhMoS4LLe6E_pFsMlmjA6P1NSge9C5G5tETYXGAn6b1xZbHtmwrPScro9LWhVmAaA7_bxYObnFUxgWtK4vzzQBjZJ36UTk4OTB-JvKWgfVWCFsaw5WCHj6Oo4jpO7d2yN7WMfAj2hTEabz9wumQ0TMhBduZ-QON3pYObSy7TSC1vVme0NJrwF_cJRehKTFmdlXGVldPxZCplr7ZQqRQhF8JP-l4mEQVnCaWGn9ONHlemczGOS-A-wwtnmwjIB1V_vgJRf4FdpV-4hUk4-QLpu3-1lWFxrtZKcggq3tWTduRo5_QebQbUUT_VSCgsFcOmyWKoj56lbxthN19hq1XGWbLGfrrR6MWh23vk01zn8FVwi7uFwEnRYSafsnWLa1Z5TpBj9GvAdl2H9NHwzpB5NqHpZNkQ3NMDj13Fn8fzO0JB83Etbm_tnFQfcb13X3bJ15Cz-Ww1MGhvIpGGnMBT_ADp9xSIyAM9dQ1yeVXk-AIgWBUlN5uyWSGyCxp0cJwx7HxM38z0UIeBu-MytL-eqndM7LxytsVzCbjOTSVRmhYEMIzUAnS1gs7uMQAGRdgRIElTJESGMjb_4bZq9s6Ve1LKkSi0_QDsrABaLe55UY0zF4ZSfOV5PMyPtocwV_dcNPlxLgNAD1BFX_Z9kAdMZQW6fAmsfFle0zAoMe4l9pMESH0JB4sJGdCKtQXj1cXNydDYozF7l8H00BV_Er7zd6VtIw0MxwkFCTatsv_R-GsBCH218RgVPsfYhwVuT8R4HarpzsDBufC4r8_c8fc9Z278sQ081jFjOja6L2x0N_ImzFNXU6xwO-Ska-QeuvYZ3X_L31ZOX4Llp-7QSfgDoHnOxFv1Xws-D5mDHD3zxOup2b2TppdKTZb9eW2vxUVviM8OI9atBfPKMGAOv9omA-6vv5IxUH0-lWMiHLQ_g8vnswp-Jav0c4t6URVUzujNOoNd_CBGGVnHiJTCHl88LQxsqLHHIu4Fz-U2SGnlxGTj0-ihit2ELGRv4vO8E1BosTmf0cx3qgG0Pq0eOLBDIHsrdZ_CCAiTc0HVkMbyq1M6qEhM-q5P6y1QCIrwg.0HFmhOzsQ98nNWJjIHkR7A", - expectedIsEncryptedJWT: true, - }, - { - testCaseName: "JWE, Appendix A.1 example", // https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.1 - encryptedJWT: "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg.48V1_ALb6US04U3b.5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A.XFBoMYUZodetZdvTiFvSkQ", - expectedIsEncryptedJWT: true, - }, - { - testCaseName: "JWE, Appendix A.2 example", // https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.2 - encryptedJWT: "eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPhcCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPgwCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A.AxY8DCtDaGlsbGljb3RoZQ.KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.9hH0vgRfYgPnAHOd8stkvw", - expectedIsEncryptedJWT: true, - }, - { - testCaseName: "JWE, Appendix A.3 example", // https://datatracker.ietf.org/doc/html/rfc7516#appendix-A.3 - encryptedJWT: "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ.AxY8DCtDaGlsbGljb3RoZQ.KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.U0m_YmjN04DJvceFICbCVQ", - expectedIsEncryptedJWT: true, - }, - { - testCaseName: "unencrypted JWT", - encryptedJWT: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.gog41qgIIHkH2h-83gwRq5-NYOciZ4DgN4ulHFSkh6k", - expectedIsEncryptedJWT: false, - }, - } { - t.Run(tt.testCaseName, func(t *testing.T) { - t.Parallel() - - require.Equal(t, tt.expectedIsEncryptedJWT, isEncryptedJWTFamilyValue(report.Finding{Secret: tt.encryptedJWT})) - }) - } -} - func BenchmarkSecretScanRawData(b *testing.B) { // Use single-line data to avoid JSON escaping issues in the request builder rawData := `slack_bot_token: xoxb-9876543210-9876543210-zyxwvutsrqponmlk` From 6e5370de5738bbc690ca0df85fefb0d4ff7db06d Mon Sep 17 00:00:00 2001 From: RebeccaMahany Date: Thu, 16 Apr 2026 16:42:31 -0400 Subject: [PATCH 2/3] Exclude empty variable false positives --- ee/tables/secretscan/table.go | 72 +++++++++++++++++++++--- ee/tables/secretscan/table_test.go | 90 ++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 9 deletions(-) diff --git a/ee/tables/secretscan/table.go b/ee/tables/secretscan/table.go index 40a46d042..0ed650f97 100644 --- a/ee/tables/secretscan/table.go +++ b/ee/tables/secretscan/table.go @@ -231,14 +231,23 @@ func (t *Table) findingsToRows(ctx context.Context, argon2idSalts []string, find keyNamesInFindings := findingsToKeyNames(findings) - // Just for logging purposes -- we're curious how frequently we detect these - encryptedJwtCount := 0 + // Just for logging purposes -- we're curious how frequently we detect false positives + encryptedJwtFalsePositiveCount := 0 + emptyVariableFalsePositiveCount := 0 for idx, f := range findings { - // Encrypted data is a false positive -- we should not count these as secrets. - if isEncryptedJWTFamilyValue(f) { - encryptedJwtCount += 1 - continue + // We sometimes see false positives under the "generic-api-key" rule. + // Check for these. + if f.RuleID == "generic-api-key" { + if isEncryptedJWTFamilyValue(f) { + encryptedJwtFalsePositiveCount += 1 + continue + } + if isEmptyVariable(f) { + emptyVariableFalsePositiveCount += 1 + continue + } } + // Get the hash of this secret. If there's an error, log it, and allow the rest of the data to be returned. // But note that there's an error, since it's probably a salting issue, and we don't need to log a billion times. var argon2idHash string @@ -271,10 +280,11 @@ func (t *Table) findingsToRows(ctx context.Context, argon2idSalts []string, find results = append(results, row) } - if encryptedJwtCount > 0 { + if encryptedJwtFalsePositiveCount > 0 || emptyVariableFalsePositiveCount > 0 { t.slogger.Log(ctx, slog.LevelInfo, - "detected and skipped encrypted JWT family values", - "jwt_family_count", encryptedJwtCount, + "detected and skipped false positive generic-api-key findings", + "jwt_family_count", encryptedJwtFalsePositiveCount, + "empty_variable", emptyVariableFalsePositiveCount, ) } @@ -323,6 +333,50 @@ func isEncryptedJWTFamilyValue(finding report.Finding) bool { return false } +// emptyVariableRegexp matches strings that start with a word char, +// contain only word chars and underscores or hyphens, and end with a +// singular equal sign -- for example, `MY_ENV_VAR=`. +var emptyVariableRegexp = regexp.MustCompile(`^\w[\w-]*=$`) + +// isEmptyVariable inspects the given finding to determine if it is actually +// an empty variable name instead. +func isEmptyVariable(finding report.Finding) bool { + // This type of false positive typically has an entropy score around 3, + // so we exclude higher-entropy values right off the bat. + if finding.Entropy >= 4 { + return false + } + + // Next, check for our regex match. + if !emptyVariableRegexp.MatchString(finding.Secret) { + return false + } + + // We expect that this "secret" would be at the start of a line, with either nothing + // or whitespace in front of it. However, sometimes our finding.Line will contain + // multiple lines -- in this case, it looks like "\nMY_ENV_VAR1=\nMY_ENV_VAR2=". + // So first we isolate the actual line we're looking at, then check to see if there's + // anything besides whitespace in front of it. + lines := strings.Split(strings.ReplaceAll(finding.Line, "\r\n", "\n"), "\n") + var lineWithSecret string + for _, line := range lines { + if strings.Contains(line, finding.Secret) { + lineWithSecret = line + break + } + } + if lineWithSecret == "" { + return false + } + before, _, _ := strings.Cut(lineWithSecret, finding.Secret) + beforeTrimmed := strings.TrimSpace(before) + if beforeTrimmed != "" { + return false + } + + return true +} + // findingsToKeyNames attempts to extract the key names (eg: in an .env file) to help understand the context // of the discovered secret. Because of the multitude of possible ways people can stash secrets, and the myriad of // secret types, this is very hard to get right. So instead, we aim to solve the simple case, and ignore the rest. diff --git a/ee/tables/secretscan/table_test.go b/ee/tables/secretscan/table_test.go index e70151848..477aaec62 100644 --- a/ee/tables/secretscan/table_test.go +++ b/ee/tables/secretscan/table_test.go @@ -17,7 +17,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/zricethezav/gitleaks/v8/detect" "github.com/zricethezav/gitleaks/v8/report" + "github.com/zricethezav/gitleaks/v8/sources" ) func TestSecretScan(t *testing.T) { @@ -412,6 +414,94 @@ func Test_isEncryptedJWTFamilyValue(t *testing.T) { } } +func Test_isEmptyVariable(t *testing.T) { + t.Parallel() + + // Set up one table for use for all test cases + tbl := &Table{ + slogger: multislogger.NewNopLogger(), + } + cfg, err := newDefaultConfig() + require.NoError(t, err) + tbl.defaultConfig = &cfg + + for _, tt := range []struct { + testCaseName string + rawData string + expectedReturn bool + }{ + { + testCaseName: "underscore", + rawData: ` +123_S3_CREDS= +123_S3_IP_REGION= +`, + expectedReturn: true, + }, + { + testCaseName: "hyphen", + rawData: ` +123-S3-CREDS= +123-S3-IP-REGION= +`, + expectedReturn: true, + }, + { + testCaseName: "alphanumeric", + rawData: ` +123S3CREDS= +123S3IPREGION= +`, + expectedReturn: true, + }, + { + testCaseName: "tab before empty variable", + rawData: ` + 123_S3_CREDS= + 123_S3_IP_REGION= +`, + expectedReturn: true, + }, + { + testCaseName: "non-empty", + rawData: ` +123_S3_CREDS=9b065cc5-cf2e-4b3f-9a20-3422e060807a +123_S3_IP_REGION=52b22b1e-2178-4a1e-bbba-50d0160ffab3 +`, + expectedReturn: false, + }, + { + testCaseName: "high entropy", // 4.19 entropy + rawData: ` +375E6860-39D4-11F1-B4AC-0800200C9A66-375E6861-39D4-11F1-B4AC-0800200C9A66_123_S3_CREDS= +4DE613D1-39D4-11F1-B4AC-0800200C9A66_123_S3_IP_REGION_4DE613D0-39D4-11F1-B4AC-0800200C9A66= +`, + expectedReturn: false, + }, + } { + t.Run(tt.testCaseName, func(t *testing.T) { + t.Parallel() + + detector := detect.NewDetector(*tbl.defaultConfig) + fileSource := &sources.File{ + Content: strings.NewReader(tt.rawData), + Config: &detector.Config, + } + + findings, err := detector.DetectSource(t.Context(), fileSource) + require.NoError(t, err) + require.Greater(t, len(findings), 0) + + for _, finding := range findings { + // Make sure the test finding we generated is the type we expected + require.Equal(t, "generic-api-key", finding.RuleID) + // Confirm that isEmptyVariable classifies the finding appropriately + require.Equal(t, tt.expectedReturn, isEmptyVariable(finding)) + } + }) + } +} + // Benchmarks func BenchmarkSecretScanDirectory(b *testing.B) { From 90ea75e457d51f2c5a01e673ddb5e0d8a6dd5e94 Mon Sep 17 00:00:00 2001 From: RebeccaMahany Date: Thu, 16 Apr 2026 17:03:49 -0400 Subject: [PATCH 3/3] lint --- ee/tables/secretscan/table.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ee/tables/secretscan/table.go b/ee/tables/secretscan/table.go index 0ed650f97..28bdfd2d1 100644 --- a/ee/tables/secretscan/table.go +++ b/ee/tables/secretscan/table.go @@ -370,11 +370,7 @@ func isEmptyVariable(finding report.Finding) bool { } before, _, _ := strings.Cut(lineWithSecret, finding.Secret) beforeTrimmed := strings.TrimSpace(before) - if beforeTrimmed != "" { - return false - } - - return true + return beforeTrimmed == "" } // findingsToKeyNames attempts to extract the key names (eg: in an .env file) to help understand the context