detect_test.go 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555
  1. package detect
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "runtime"
  7. "strings"
  8. "testing"
  9. "github.com/google/go-cmp/cmp"
  10. "github.com/rs/zerolog"
  11. "github.com/spf13/viper"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/require"
  14. "golang.org/x/exp/maps"
  15. "github.com/zricethezav/gitleaks/v8/cmd/scm"
  16. "github.com/zricethezav/gitleaks/v8/config"
  17. "github.com/zricethezav/gitleaks/v8/logging"
  18. "github.com/zricethezav/gitleaks/v8/regexp"
  19. "github.com/zricethezav/gitleaks/v8/report"
  20. "github.com/zricethezav/gitleaks/v8/sources"
  21. )
  22. const maxDecodeDepth = 8
  23. const configPath = "../testdata/config/"
  24. const repoBasePath = "../testdata/repos/"
  25. const b64TestValues = `
  26. # Decoded
  27. -----BEGIN PRIVATE KEY-----
  28. 135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb
  29. u+QDkg0spw==
  30. -----END PRIVATE KEY-----
  31. # Encoded
  32. private_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'
  33. # Double Encoded: b64 encoded aws config inside a jwt
  34. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA
  35. # A small secret at the end to make sure that as the other ones above shrink
  36. # when decoded, the positions are taken into consideratoin for overlaps
  37. c21hbGwtc2VjcmV0
  38. # This tests how it handles when the match bounds go outside the decoded value
  39. secret=ZGVjb2RlZC1zZWNyZXQtdmFsdWU=
  40. # The above encoded again
  41. c2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=
  42. # Confirm you can ignore on the decoded value
  43. password="bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw=="
  44. `
  45. func TestDetect(t *testing.T) {
  46. logging.Logger = logging.Logger.Level(zerolog.TraceLevel)
  47. tests := map[string]struct {
  48. cfgName string
  49. baselinePath string
  50. fragment Fragment
  51. // NOTE: for expected findings, all line numbers will be 0
  52. // because line deltas are added _after_ the finding is created.
  53. // I.e., if the finding is from a --no-git file, the line number will be
  54. // increase by 1 in DetectFromFiles(). If the finding is from git,
  55. // the line number will be increased by the patch delta.
  56. expectedFindings []report.Finding
  57. wantError error
  58. }{
  59. // General
  60. "valid allow comment (1)": {
  61. cfgName: "simple",
  62. fragment: Fragment{
  63. Raw: `Key: A3-ASWWYB-798JRY-LJVD4-23DC2-86TVM-H43EB`,
  64. FilePath: "tmp.go",
  65. },
  66. expectedFindings: []report.Finding{
  67. {
  68. Description: "1Password Secret Key",
  69. Secret: "A3-ASWWYB-798JRY-LJVD4-23DC2-86TVM-H43EB",
  70. Match: "A3-ASWWYB-798JRY-LJVD4-23DC2-86TVM-H43EB",
  71. Line: "Key: A3-ASWWYB-798JRY-LJVD4-23DC2-86TVM-H43EB",
  72. File: "tmp.go",
  73. RuleID: "1password-secret-key",
  74. Tags: []string{"1Password"},
  75. StartLine: 0,
  76. EndLine: 0,
  77. StartColumn: 6,
  78. EndColumn: 45,
  79. Entropy: 4.3153114,
  80. },
  81. },
  82. },
  83. {
  84. cfgName: "simple",
  85. fragment: Fragment{
  86. Raw: `awsToken := \"AKIALALEMEL33243OKIA\ // gitleaks:allow"`,
  87. FilePath: "tmp.go",
  88. },
  89. },
  90. "valid allow comment (2)": {
  91. cfgName: "simple",
  92. fragment: Fragment{
  93. Raw: `awsToken := \
  94. \"AKIALALEMEL33243OKIA\ // gitleaks:allow"
  95. `,
  96. FilePath: "tmp.go",
  97. },
  98. },
  99. "invalid allow comment": {
  100. cfgName: "simple",
  101. fragment: Fragment{
  102. Raw: `awsToken := \"AKIALALEMEL33243OKIA\"
  103. // gitleaks:allow"
  104. `,
  105. FilePath: "tmp.go",
  106. },
  107. expectedFindings: []report.Finding{
  108. {
  109. Description: "AWS Access Key",
  110. Secret: "AKIALALEMEL33243OKIA",
  111. Match: "AKIALALEMEL33243OKIA",
  112. File: "tmp.go",
  113. Line: `awsToken := \"AKIALALEMEL33243OKIA\"`,
  114. RuleID: "aws-access-key",
  115. Tags: []string{"key", "AWS"},
  116. StartLine: 0,
  117. EndLine: 0,
  118. StartColumn: 15,
  119. EndColumn: 34,
  120. Entropy: 3.1464393,
  121. },
  122. },
  123. },
  124. "detect finding - aws": {
  125. cfgName: "simple",
  126. fragment: Fragment{
  127. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  128. FilePath: "tmp.go",
  129. },
  130. expectedFindings: []report.Finding{
  131. {
  132. RuleID: "aws-access-key",
  133. Description: "AWS Access Key",
  134. File: "tmp.go",
  135. Line: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  136. Match: "AKIALALEMEL33243OLIA",
  137. Secret: "AKIALALEMEL33243OLIA",
  138. Entropy: 3.0841837,
  139. StartLine: 0,
  140. EndLine: 0,
  141. StartColumn: 15,
  142. EndColumn: 34,
  143. Tags: []string{"key", "AWS"},
  144. },
  145. },
  146. },
  147. "detect finding - sidekiq env var": {
  148. cfgName: "simple",
  149. fragment: Fragment{
  150. Raw: `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
  151. FilePath: "tmp.sh",
  152. },
  153. expectedFindings: []report.Finding{
  154. {
  155. RuleID: "sidekiq-secret",
  156. Description: "Sidekiq Secret",
  157. File: "tmp.sh",
  158. Line: `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
  159. Match: "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;",
  160. Secret: "cafebabe:deadbeef",
  161. Entropy: 2.6098502,
  162. StartLine: 0,
  163. EndLine: 0,
  164. StartColumn: 8,
  165. EndColumn: 60,
  166. Tags: []string{},
  167. },
  168. },
  169. },
  170. "detect finding - sidekiq env var, semicolon": {
  171. cfgName: "simple",
  172. fragment: Fragment{
  173. Raw: `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
  174. FilePath: "tmp.sh",
  175. },
  176. expectedFindings: []report.Finding{
  177. {
  178. RuleID: "sidekiq-secret",
  179. Description: "Sidekiq Secret",
  180. File: "tmp.sh",
  181. Line: `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
  182. Match: "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=\"cafebabe:deadbeef\"",
  183. Secret: "cafebabe:deadbeef",
  184. Entropy: 2.6098502,
  185. StartLine: 0,
  186. EndLine: 0,
  187. StartColumn: 21,
  188. EndColumn: 74,
  189. Tags: []string{},
  190. },
  191. },
  192. },
  193. "detect finding - sidekiq url": {
  194. cfgName: "simple",
  195. fragment: Fragment{
  196. Raw: `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
  197. FilePath: "tmp.sh",
  198. },
  199. expectedFindings: []report.Finding{
  200. {
  201. RuleID: "sidekiq-sensitive-url",
  202. Description: "Sidekiq Sensitive URL",
  203. File: "tmp.sh",
  204. Line: `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
  205. Match: "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:",
  206. Secret: "cafeb4b3:d3adb33f",
  207. Entropy: 2.984234,
  208. StartLine: 0,
  209. EndLine: 0,
  210. StartColumn: 8,
  211. EndColumn: 58,
  212. Tags: []string{},
  213. },
  214. },
  215. },
  216. "ignore finding - our config file": {
  217. cfgName: "simple",
  218. fragment: Fragment{
  219. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  220. FilePath: filepath.Join(configPath, "simple.toml"),
  221. },
  222. },
  223. "ignore finding - doesn't match path": {
  224. cfgName: "generic_with_py_path",
  225. fragment: Fragment{
  226. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  227. FilePath: "tmp.go",
  228. },
  229. },
  230. "detect finding - matches path,regex,entropy": {
  231. cfgName: "generic_with_py_path",
  232. fragment: Fragment{
  233. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  234. FilePath: "tmp.py",
  235. },
  236. expectedFindings: []report.Finding{
  237. {
  238. RuleID: "generic-api-key",
  239. Description: "Generic API Key",
  240. File: "tmp.py",
  241. Line: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  242. Match: "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
  243. Secret: "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
  244. Entropy: 3.7906237,
  245. StartLine: 0,
  246. EndLine: 0,
  247. StartColumn: 22,
  248. EndColumn: 93,
  249. Tags: []string{},
  250. },
  251. },
  252. },
  253. "ignore finding - allowlist regex": {
  254. cfgName: "generic_with_py_path",
  255. fragment: Fragment{
  256. Raw: `const Discord_Public_Key = "load2523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  257. FilePath: "tmp.py",
  258. },
  259. },
  260. // Rule
  261. "rule - ignore path": {
  262. cfgName: "valid/rule_path_only",
  263. baselinePath: ".baseline.json",
  264. fragment: Fragment{
  265. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  266. FilePath: ".baseline.json",
  267. },
  268. },
  269. "rule - detect path ": {
  270. cfgName: "valid/rule_path_only",
  271. fragment: Fragment{
  272. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  273. FilePath: "tmp.py",
  274. },
  275. expectedFindings: []report.Finding{
  276. {
  277. Description: "Python Files",
  278. Match: "file detected: tmp.py",
  279. File: "tmp.py",
  280. RuleID: "python-files-only",
  281. Tags: []string{},
  282. },
  283. },
  284. },
  285. "rule - match based on entropy": {
  286. cfgName: "valid/rule_entropy_group",
  287. fragment: Fragment{
  288. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"
  289. //const Discord_Public_Key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
  290. `,
  291. FilePath: "tmp.go",
  292. },
  293. expectedFindings: []report.Finding{
  294. {
  295. RuleID: "discord-api-key",
  296. Description: "Discord API key",
  297. File: "tmp.go",
  298. Line: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  299. Match: "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
  300. Secret: "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
  301. Entropy: 3.7906237,
  302. StartLine: 0,
  303. EndLine: 0,
  304. StartColumn: 7,
  305. EndColumn: 93,
  306. Tags: []string{},
  307. },
  308. },
  309. },
  310. // Allowlists
  311. "global allowlist - ignore regex": {
  312. cfgName: "valid/allowlist_global_regex",
  313. fragment: Fragment{
  314. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  315. FilePath: "tmp.go",
  316. },
  317. },
  318. "global allowlist - detect, doesn't match all conditions": {
  319. cfgName: "valid/allowlist_global_multiple",
  320. fragment: Fragment{
  321. Raw: `
  322. const token = "mockSecret";
  323. // const token = "changeit";`,
  324. FilePath: "config.txt",
  325. },
  326. expectedFindings: []report.Finding{
  327. {
  328. RuleID: "test",
  329. File: "config.txt",
  330. Line: "\nconst token = \"mockSecret\";",
  331. Match: `token = "mockSecret"`,
  332. Secret: "mockSecret",
  333. Entropy: 2.9219282,
  334. StartLine: 1,
  335. EndLine: 1,
  336. StartColumn: 8,
  337. EndColumn: 27,
  338. Tags: []string{},
  339. },
  340. },
  341. },
  342. "global allowlist - ignore, matches all conditions": {
  343. cfgName: "valid/allowlist_global_multiple",
  344. fragment: Fragment{
  345. Raw: `token := "mockSecret";`,
  346. FilePath: "node_modules/config.txt",
  347. },
  348. },
  349. "global allowlist - detect path, doesn't match all conditions": {
  350. cfgName: "valid/allowlist_global_multiple",
  351. fragment: Fragment{
  352. Raw: `var token = "fakeSecret";`,
  353. FilePath: "node_modules/config.txt",
  354. },
  355. expectedFindings: []report.Finding{
  356. {
  357. RuleID: "test",
  358. File: "node_modules/config.txt",
  359. Line: "var token = \"fakeSecret\";",
  360. Match: `token = "fakeSecret"`,
  361. Secret: "fakeSecret",
  362. Entropy: 2.8464394,
  363. StartLine: 0,
  364. EndLine: 0,
  365. StartColumn: 5,
  366. EndColumn: 24,
  367. Tags: []string{},
  368. },
  369. },
  370. },
  371. "allowlist - ignore commit": {
  372. cfgName: "valid/allowlist_rule_commit",
  373. fragment: Fragment{
  374. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  375. FilePath: "tmp.go",
  376. CommitSHA: "allowthiscommit",
  377. },
  378. },
  379. "allowlist - ignore path": {
  380. cfgName: "valid/allowlist_rule_path",
  381. fragment: Fragment{
  382. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  383. FilePath: "tmp.go",
  384. },
  385. },
  386. "allowlist - ignore path when extending": {
  387. cfgName: "valid/allowlist_rule_extend_default",
  388. fragment: Fragment{
  389. Raw: `token = "aebfab88-7596-481d-82e8-c60c8f7de0c0"`,
  390. FilePath: "path/to/your/problematic/file.js",
  391. },
  392. },
  393. "allowlist - ignore regex": {
  394. cfgName: "valid/allowlist_rule_regex",
  395. fragment: Fragment{
  396. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  397. FilePath: "tmp.go",
  398. },
  399. },
  400. // Base64-decoding
  401. "detect base64": {
  402. cfgName: "base64_encoded",
  403. fragment: Fragment{
  404. Raw: b64TestValues,
  405. FilePath: "tmp.go",
  406. },
  407. expectedFindings: []report.Finding{
  408. { // Plain text key captured by normal rule
  409. Description: "Private Key",
  410. Secret: "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
  411. Match: "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
  412. File: "tmp.go",
  413. Line: "\n-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
  414. RuleID: "private-key",
  415. Tags: []string{"key", "private"},
  416. StartLine: 2,
  417. EndLine: 5,
  418. StartColumn: 2,
  419. EndColumn: 26,
  420. Entropy: 5.350665,
  421. },
  422. { // Encoded key captured by custom b64 regex rule
  423. Description: "Private Key",
  424. Secret: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K",
  425. Match: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K",
  426. File: "tmp.go",
  427. Line: "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'",
  428. RuleID: "b64-encoded-private-key",
  429. Tags: []string{"key", "private"},
  430. StartLine: 8,
  431. EndLine: 8,
  432. StartColumn: 16,
  433. EndColumn: 207,
  434. Entropy: 5.3861146,
  435. },
  436. { // Encoded key captured by plain text rule using the decoder
  437. Description: "Private Key",
  438. Secret: "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
  439. Match: "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
  440. File: "tmp.go",
  441. Line: "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'",
  442. RuleID: "private-key",
  443. Tags: []string{"key", "private", "decoded:base64", "decode-depth:1"},
  444. StartLine: 8,
  445. EndLine: 8,
  446. StartColumn: 16,
  447. EndColumn: 207,
  448. Entropy: 5.350665,
  449. },
  450. { // Encoded AWS config with a access key id inside a JWT
  451. Description: "AWS IAM Unique Identifier",
  452. Secret: "ASIAIOSFODNN7LXM10JI",
  453. Match: " ASIAIOSFODNN7LXM10JI",
  454. File: "tmp.go",
  455. Line: "\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA",
  456. RuleID: "aws-iam-unique-identifier",
  457. Tags: []string{"aws", "identifier", "decoded:base64", "decode-depth:2"},
  458. StartLine: 11,
  459. EndLine: 11,
  460. StartColumn: 39,
  461. EndColumn: 344,
  462. Entropy: 3.6841838,
  463. },
  464. { // Encoded AWS config with a secret access key inside a JWT
  465. Description: "AWS Secret Access Key",
  466. Secret: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEFUCDlEX1A",
  467. Match: "aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEFUCDlEX1A",
  468. File: "tmp.go",
  469. Line: "\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA",
  470. RuleID: "aws-secret-access-key",
  471. Tags: []string{"aws", "secret", "decoded:base64", "decode-depth:2"},
  472. StartLine: 11,
  473. EndLine: 11,
  474. StartColumn: 39,
  475. EndColumn: 344,
  476. Entropy: 4.721928,
  477. },
  478. { // Encoded Small secret at the end to make sure it's picked up by the decoding
  479. Description: "Small Secret",
  480. Secret: "small-secret",
  481. Match: "small-secret",
  482. File: "tmp.go",
  483. Line: "\nc21hbGwtc2VjcmV0",
  484. RuleID: "small-secret",
  485. Tags: []string{"small", "secret", "decoded:base64", "decode-depth:1"},
  486. StartLine: 15,
  487. EndLine: 15,
  488. StartColumn: 2,
  489. EndColumn: 17,
  490. Entropy: 3.0849626,
  491. },
  492. { // Secret where the decoded match goes outside the encoded value
  493. Description: "Overlapping",
  494. Secret: "decoded-secret-value",
  495. Match: "secret=decoded-secret-value",
  496. File: "tmp.go",
  497. Line: "\nsecret=ZGVjb2RlZC1zZWNyZXQtdmFsdWU=",
  498. RuleID: "overlapping",
  499. Tags: []string{"overlapping", "decoded:base64", "decode-depth:1"},
  500. StartLine: 18,
  501. EndLine: 18,
  502. StartColumn: 2,
  503. EndColumn: 36,
  504. Entropy: 3.3037016,
  505. },
  506. { // Secret where the decoded match goes outside the encoded value and then encoded again
  507. Description: "Overlapping",
  508. Secret: "decoded-secret-value",
  509. Match: "secret=decoded-secret-value",
  510. File: "tmp.go",
  511. Line: "\nc2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=",
  512. RuleID: "overlapping",
  513. Tags: []string{"overlapping", "decoded:base64", "decode-depth:2"},
  514. StartLine: 20,
  515. EndLine: 20,
  516. StartColumn: 2,
  517. EndColumn: 49,
  518. Entropy: 3.3037016,
  519. },
  520. { // This just confirms that with no allowlist the pattern is detected (i.e. the regex is good)
  521. Description: "Make sure this would be detected with no allowlist",
  522. Secret: "lRqBK-z5kf4-please-ignore-me-X-XIJM2Pddw",
  523. Match: "password=\"lRqBK-z5kf4-please-ignore-me-X-XIJM2Pddw\"",
  524. File: "tmp.go",
  525. Line: "\npassword=\"bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw==\"",
  526. RuleID: "decoded-password-dont-ignore",
  527. Tags: []string{"decode-ignore", "decoded:base64", "decode-depth:1"},
  528. StartLine: 23,
  529. EndLine: 23,
  530. StartColumn: 2,
  531. EndColumn: 68,
  532. Entropy: 4.5841837,
  533. },
  534. },
  535. },
  536. }
  537. for name, tt := range tests {
  538. t.Run(name, func(t *testing.T) {
  539. viper.Reset()
  540. viper.AddConfigPath(configPath)
  541. viper.SetConfigName(tt.cfgName)
  542. viper.SetConfigType("toml")
  543. err := viper.ReadInConfig()
  544. require.NoError(t, err)
  545. var vc config.ViperConfig
  546. err = viper.Unmarshal(&vc)
  547. require.NoError(t, err)
  548. cfg, err := vc.Translate()
  549. cfg.Path = filepath.Join(configPath, tt.cfgName+".toml")
  550. assert.Equal(t, tt.wantError, err)
  551. d := NewDetector(cfg)
  552. d.MaxDecodeDepth = maxDecodeDepth
  553. d.baselinePath = tt.baselinePath
  554. findings := d.Detect(tt.fragment)
  555. assert.ElementsMatch(t, tt.expectedFindings, findings)
  556. })
  557. }
  558. }
  559. // TestFromGit tests the FromGit function
  560. func TestFromGit(t *testing.T) {
  561. // TODO: Fix this test on windows.
  562. if runtime.GOOS == "windows" {
  563. t.Skipf("TODO: this fails on Windows: [git] fatal: bad object refs/remotes/origin/main?")
  564. return
  565. }
  566. tests := []struct {
  567. cfgName string
  568. source string
  569. logOpts string
  570. expectedFindings []report.Finding
  571. }{
  572. {
  573. source: filepath.Join(repoBasePath, "small"),
  574. cfgName: "simple", // the remote url is `git@github.com:gitleaks/test.git`
  575. expectedFindings: []report.Finding{
  576. {
  577. RuleID: "aws-access-key",
  578. Description: "AWS Access Key",
  579. StartLine: 20,
  580. EndLine: 20,
  581. StartColumn: 19,
  582. EndColumn: 38,
  583. Line: "\n awsToken := \"AKIALALEMEL33243OLIA\"",
  584. Secret: "AKIALALEMEL33243OLIA",
  585. Match: "AKIALALEMEL33243OLIA",
  586. Entropy: 3.0841837,
  587. File: "main.go",
  588. Date: "2021-11-02T23:37:53Z",
  589. Commit: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587",
  590. Author: "Zachary Rice",
  591. Email: "zricer@protonmail.com",
  592. Message: "Accidentally add a secret",
  593. Tags: []string{"key", "AWS"},
  594. Fingerprint: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587:main.go:aws-access-key:20",
  595. Link: "https://github.com/gitleaks/test/blob/1b6da43b82b22e4eaa10bcf8ee591e91abbfc587/main.go#L20",
  596. },
  597. {
  598. RuleID: "aws-access-key",
  599. Description: "AWS Access Key",
  600. StartLine: 9,
  601. EndLine: 9,
  602. StartColumn: 17,
  603. EndColumn: 36,
  604. Secret: "AKIALALEMEL33243OLIA",
  605. Match: "AKIALALEMEL33243OLIA",
  606. Line: "\n\taws_token := \"AKIALALEMEL33243OLIA\"",
  607. File: "foo/foo.go",
  608. Date: "2021-11-02T23:48:06Z",
  609. Commit: "491504d5a31946ce75e22554cc34203d8e5ff3ca",
  610. Author: "Zach Rice",
  611. Email: "zricer@protonmail.com",
  612. Message: "adding foo package with secret",
  613. Tags: []string{"key", "AWS"},
  614. Entropy: 3.0841837,
  615. Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9",
  616. Link: "https://github.com/gitleaks/test/blob/491504d5a31946ce75e22554cc34203d8e5ff3ca/foo/foo.go#L9",
  617. },
  618. },
  619. },
  620. {
  621. source: filepath.Join(repoBasePath, "small"),
  622. logOpts: "--all foo...",
  623. cfgName: "simple",
  624. expectedFindings: []report.Finding{
  625. {
  626. RuleID: "aws-access-key",
  627. Description: "AWS Access Key",
  628. StartLine: 9,
  629. EndLine: 9,
  630. StartColumn: 17,
  631. EndColumn: 36,
  632. Secret: "AKIALALEMEL33243OLIA",
  633. Line: "\n\taws_token := \"AKIALALEMEL33243OLIA\"",
  634. Match: "AKIALALEMEL33243OLIA",
  635. Date: "2021-11-02T23:48:06Z",
  636. File: "foo/foo.go",
  637. Commit: "491504d5a31946ce75e22554cc34203d8e5ff3ca",
  638. Author: "Zach Rice",
  639. Email: "zricer@protonmail.com",
  640. Message: "adding foo package with secret",
  641. Tags: []string{"key", "AWS"},
  642. Entropy: 3.0841837,
  643. Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9",
  644. Link: "https://github.com/gitleaks/test/blob/491504d5a31946ce75e22554cc34203d8e5ff3ca/foo/foo.go#L9",
  645. },
  646. },
  647. },
  648. }
  649. moveDotGit(t, "dotGit", ".git")
  650. defer moveDotGit(t, ".git", "dotGit")
  651. for _, tt := range tests {
  652. t.Run(strings.Join([]string{tt.cfgName, tt.source, tt.logOpts}, "/"), func(t *testing.T) {
  653. viper.AddConfigPath(configPath)
  654. viper.SetConfigName("simple")
  655. viper.SetConfigType("toml")
  656. err := viper.ReadInConfig()
  657. require.NoError(t, err)
  658. var vc config.ViperConfig
  659. err = viper.Unmarshal(&vc)
  660. require.NoError(t, err)
  661. cfg, err := vc.Translate()
  662. require.NoError(t, err)
  663. detector := NewDetector(cfg)
  664. var ignorePath string
  665. info, err := os.Stat(tt.source)
  666. require.NoError(t, err)
  667. if info.IsDir() {
  668. ignorePath = filepath.Join(tt.source, ".gitleaksignore")
  669. } else {
  670. ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore")
  671. }
  672. err = detector.AddGitleaksIgnore(ignorePath)
  673. require.NoError(t, err)
  674. gitCmd, err := sources.NewGitLogCmd(tt.source, tt.logOpts)
  675. require.NoError(t, err)
  676. remote := NewRemoteInfo(scm.UnknownPlatform, tt.source)
  677. findings, err := detector.DetectGit(gitCmd, remote)
  678. require.NoError(t, err)
  679. for _, f := range findings {
  680. f.Match = "" // remove lines cause copying and pasting them has some wack formatting
  681. }
  682. assert.ElementsMatch(t, tt.expectedFindings, findings)
  683. })
  684. }
  685. }
  686. func TestFromGitStaged(t *testing.T) {
  687. tests := []struct {
  688. cfgName string
  689. source string
  690. logOpts string
  691. expectedFindings []report.Finding
  692. }{
  693. {
  694. source: filepath.Join(repoBasePath, "staged"),
  695. cfgName: "simple",
  696. expectedFindings: []report.Finding{
  697. {
  698. RuleID: "aws-access-key",
  699. Description: "AWS Access Key",
  700. StartLine: 7,
  701. EndLine: 7,
  702. StartColumn: 18,
  703. EndColumn: 37,
  704. Line: "\n\taws_token2 := \"AKIALALEMEL33243OLIA\" // this one is not",
  705. Match: "AKIALALEMEL33243OLIA",
  706. Secret: "AKIALALEMEL33243OLIA",
  707. File: "api/api.go",
  708. SymlinkFile: "",
  709. Commit: "",
  710. Entropy: 3.0841837,
  711. Author: "",
  712. Email: "",
  713. Date: "0001-01-01T00:00:00Z",
  714. Message: "",
  715. Tags: []string{
  716. "key",
  717. "AWS",
  718. },
  719. Fingerprint: "api/api.go:aws-access-key:7",
  720. Link: "",
  721. },
  722. },
  723. },
  724. }
  725. moveDotGit(t, "dotGit", ".git")
  726. defer moveDotGit(t, ".git", "dotGit")
  727. for _, tt := range tests {
  728. viper.AddConfigPath(configPath)
  729. viper.SetConfigName("simple")
  730. viper.SetConfigType("toml")
  731. err := viper.ReadInConfig()
  732. require.NoError(t, err)
  733. var vc config.ViperConfig
  734. err = viper.Unmarshal(&vc)
  735. require.NoError(t, err)
  736. cfg, err := vc.Translate()
  737. require.NoError(t, err)
  738. detector := NewDetector(cfg)
  739. err = detector.AddGitleaksIgnore(filepath.Join(tt.source, ".gitleaksignore"))
  740. require.NoError(t, err)
  741. gitCmd, err := sources.NewGitDiffCmd(tt.source, true)
  742. require.NoError(t, err)
  743. remote := NewRemoteInfo(scm.UnknownPlatform, tt.source)
  744. findings, err := detector.DetectGit(gitCmd, remote)
  745. require.NoError(t, err)
  746. for _, f := range findings {
  747. f.Match = "" // remove lines cause copying and pasting them has some wack formatting
  748. }
  749. assert.ElementsMatch(t, tt.expectedFindings, findings)
  750. }
  751. }
  752. // TestFromFiles tests the FromFiles function
  753. func TestFromFiles(t *testing.T) {
  754. tests := []struct {
  755. cfgName string
  756. source string
  757. expectedFindings []report.Finding
  758. }{
  759. {
  760. source: filepath.Join(repoBasePath, "nogit"),
  761. cfgName: "simple",
  762. expectedFindings: []report.Finding{
  763. {
  764. RuleID: "aws-access-key",
  765. Description: "AWS Access Key",
  766. StartLine: 20,
  767. EndLine: 20,
  768. StartColumn: 16,
  769. EndColumn: 35,
  770. Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
  771. Match: "AKIALALEMEL33243OLIA",
  772. Secret: "AKIALALEMEL33243OLIA",
  773. File: "../testdata/repos/nogit/main.go",
  774. SymlinkFile: "",
  775. Tags: []string{"key", "AWS"},
  776. Entropy: 3.0841837,
  777. Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20",
  778. },
  779. },
  780. },
  781. {
  782. source: filepath.Join(repoBasePath, "nogit", "main.go"),
  783. cfgName: "simple",
  784. expectedFindings: []report.Finding{
  785. {
  786. RuleID: "aws-access-key",
  787. Description: "AWS Access Key",
  788. StartLine: 20,
  789. EndLine: 20,
  790. StartColumn: 16,
  791. EndColumn: 35,
  792. Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
  793. Match: "AKIALALEMEL33243OLIA",
  794. Secret: "AKIALALEMEL33243OLIA",
  795. File: "../testdata/repos/nogit/main.go",
  796. Tags: []string{"key", "AWS"},
  797. Entropy: 3.0841837,
  798. Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20",
  799. },
  800. },
  801. },
  802. {
  803. source: filepath.Join(repoBasePath, "nogit", "api.go"),
  804. cfgName: "simple",
  805. expectedFindings: []report.Finding{},
  806. },
  807. {
  808. source: filepath.Join(repoBasePath, "nogit", ".env.prod"),
  809. cfgName: "generic",
  810. expectedFindings: []report.Finding{
  811. {
  812. RuleID: "generic-api-key",
  813. Description: "Generic API Key",
  814. StartLine: 4,
  815. EndLine: 4,
  816. StartColumn: 5,
  817. EndColumn: 35,
  818. Line: "\nDB_PASSWORD=8ae31cacf141669ddfb5da",
  819. Match: "PASSWORD=8ae31cacf141669ddfb5da",
  820. Secret: "8ae31cacf141669ddfb5da",
  821. File: "../testdata/repos/nogit/.env.prod",
  822. Tags: []string{},
  823. Entropy: 3.5383105,
  824. Fingerprint: "../testdata/repos/nogit/.env.prod:generic-api-key:4",
  825. },
  826. },
  827. },
  828. }
  829. for _, tt := range tests {
  830. t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) {
  831. viper.AddConfigPath(configPath)
  832. viper.SetConfigName(tt.cfgName)
  833. viper.SetConfigType("toml")
  834. err := viper.ReadInConfig()
  835. require.NoError(t, err)
  836. var vc config.ViperConfig
  837. err = viper.Unmarshal(&vc)
  838. require.NoError(t, err)
  839. cfg, _ := vc.Translate()
  840. detector := NewDetector(cfg)
  841. info, err := os.Stat(tt.source)
  842. require.NoError(t, err)
  843. var ignorePath string
  844. if info.IsDir() {
  845. ignorePath = filepath.Join(tt.source, ".gitleaksignore")
  846. } else {
  847. ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore")
  848. }
  849. err = detector.AddGitleaksIgnore(ignorePath)
  850. require.NoError(t, err)
  851. detector.FollowSymlinks = true
  852. paths, err := sources.DirectoryTargets(tt.source, detector.Sema, true, cfg.Allowlists)
  853. require.NoError(t, err)
  854. findings, err := detector.DetectFiles(paths)
  855. require.NoError(t, err)
  856. // TODO: Temporary mitigation.
  857. // https://github.com/gitleaks/gitleaks/issues/1641
  858. normalizedFindings := make([]report.Finding, len(findings))
  859. for i, f := range findings {
  860. if strings.HasSuffix(f.Line, "\r") {
  861. f.Line = strings.ReplaceAll(f.Line, "\r", "")
  862. }
  863. if strings.HasSuffix(f.Match, "\r") {
  864. f.EndColumn = f.EndColumn - 1
  865. f.Match = strings.ReplaceAll(f.Match, "\r", "")
  866. }
  867. normalizedFindings[i] = f
  868. }
  869. assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings)
  870. })
  871. }
  872. }
  873. func TestDetectWithSymlinks(t *testing.T) {
  874. // TODO: Fix this test on windows.
  875. if runtime.GOOS == "windows" {
  876. t.Skipf("TODO: this returns no results on windows, I'm not sure why.")
  877. return
  878. }
  879. tests := []struct {
  880. cfgName string
  881. source string
  882. expectedFindings []report.Finding
  883. }{
  884. {
  885. source: filepath.Join(repoBasePath, "symlinks/file_symlink"),
  886. cfgName: "simple",
  887. expectedFindings: []report.Finding{
  888. {
  889. RuleID: "apkey",
  890. Description: "Asymmetric Private Key",
  891. StartLine: 1,
  892. EndLine: 1,
  893. StartColumn: 1,
  894. EndColumn: 35,
  895. Match: "-----BEGIN OPENSSH PRIVATE KEY-----",
  896. Secret: "-----BEGIN OPENSSH PRIVATE KEY-----",
  897. Line: "-----BEGIN OPENSSH PRIVATE KEY-----",
  898. File: "../testdata/repos/symlinks/source_file/id_ed25519",
  899. SymlinkFile: "../testdata/repos/symlinks/file_symlink/symlinked_id_ed25519",
  900. Tags: []string{"key", "AsymmetricPrivateKey"},
  901. Entropy: 3.587164,
  902. Fingerprint: "../testdata/repos/symlinks/source_file/id_ed25519:apkey:1",
  903. },
  904. },
  905. },
  906. }
  907. for _, tt := range tests {
  908. viper.AddConfigPath(configPath)
  909. viper.SetConfigName("simple")
  910. viper.SetConfigType("toml")
  911. err := viper.ReadInConfig()
  912. require.NoError(t, err)
  913. var vc config.ViperConfig
  914. err = viper.Unmarshal(&vc)
  915. require.NoError(t, err)
  916. cfg, _ := vc.Translate()
  917. detector := NewDetector(cfg)
  918. detector.FollowSymlinks = true
  919. paths, err := sources.DirectoryTargets(tt.source, detector.Sema, true, cfg.Allowlists)
  920. require.NoError(t, err)
  921. findings, err := detector.DetectFiles(paths)
  922. require.NoError(t, err)
  923. assert.ElementsMatch(t, tt.expectedFindings, findings)
  924. }
  925. }
  926. func TestDetectRuleAllowlist(t *testing.T) {
  927. cases := map[string]struct {
  928. fragment Fragment
  929. allowlist *config.Allowlist
  930. expected []report.Finding
  931. }{
  932. // Commit / path
  933. "commit allowed": {
  934. fragment: Fragment{
  935. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  936. },
  937. allowlist: &config.Allowlist{
  938. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  939. },
  940. },
  941. "path allowed": {
  942. fragment: Fragment{
  943. FilePath: "package-lock.json",
  944. },
  945. allowlist: &config.Allowlist{
  946. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  947. },
  948. },
  949. "commit AND path allowed": {
  950. fragment: Fragment{
  951. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  952. FilePath: "package-lock.json",
  953. },
  954. allowlist: &config.Allowlist{
  955. MatchCondition: config.AllowlistMatchAnd,
  956. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  957. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  958. },
  959. },
  960. "commit AND path NOT allowed": {
  961. fragment: Fragment{
  962. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  963. FilePath: "package.json",
  964. },
  965. allowlist: &config.Allowlist{
  966. MatchCondition: config.AllowlistMatchAnd,
  967. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  968. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  969. },
  970. expected: []report.Finding{
  971. {
  972. StartColumn: 50,
  973. EndColumn: 60,
  974. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  975. Match: "Summer2024!",
  976. Secret: "Summer2024!",
  977. File: "package.json",
  978. Entropy: 3.095795154571533,
  979. RuleID: "test-rule",
  980. },
  981. },
  982. },
  983. "commit AND path NOT allowed - other conditions": {
  984. fragment: Fragment{
  985. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  986. FilePath: "package-lock.json",
  987. },
  988. allowlist: &config.Allowlist{
  989. MatchCondition: config.AllowlistMatchAnd,
  990. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  991. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  992. Regexes: []*regexp.Regexp{regexp.MustCompile("password")},
  993. },
  994. expected: []report.Finding{
  995. {
  996. StartColumn: 50,
  997. EndColumn: 60,
  998. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  999. Match: "Summer2024!",
  1000. Secret: "Summer2024!",
  1001. File: "package-lock.json",
  1002. Entropy: 3.095795154571533,
  1003. RuleID: "test-rule",
  1004. },
  1005. },
  1006. },
  1007. "commit OR path allowed": {
  1008. fragment: Fragment{
  1009. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  1010. FilePath: "package-lock.json",
  1011. },
  1012. allowlist: &config.Allowlist{
  1013. MatchCondition: config.AllowlistMatchOr,
  1014. Commits: []string{"704178e7dca77ff143778a31cff0fc192d59b030"},
  1015. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  1016. },
  1017. },
  1018. // Regex / stopwords
  1019. "regex allowed": {
  1020. fragment: Fragment{},
  1021. allowlist: &config.Allowlist{
  1022. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  1023. },
  1024. },
  1025. "stopwords allowed": {
  1026. fragment: Fragment{},
  1027. allowlist: &config.Allowlist{
  1028. StopWords: []string{"summer"},
  1029. },
  1030. },
  1031. "regex AND stopword allowed": {
  1032. fragment: Fragment{},
  1033. allowlist: &config.Allowlist{
  1034. MatchCondition: config.AllowlistMatchAnd,
  1035. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  1036. StopWords: []string{"2024"},
  1037. },
  1038. },
  1039. "regex AND stopword allowed - other conditions": {
  1040. fragment: Fragment{
  1041. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  1042. FilePath: "config.js",
  1043. },
  1044. allowlist: &config.Allowlist{
  1045. MatchCondition: config.AllowlistMatchAnd,
  1046. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  1047. Paths: []*regexp.Regexp{regexp.MustCompile(`config.js`)},
  1048. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  1049. StopWords: []string{"2024"},
  1050. },
  1051. },
  1052. "regex AND stopword NOT allowed - non-git, other conditions": {
  1053. fragment: Fragment{
  1054. FilePath: "config.js",
  1055. },
  1056. allowlist: &config.Allowlist{
  1057. MatchCondition: config.AllowlistMatchAnd,
  1058. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  1059. Paths: []*regexp.Regexp{regexp.MustCompile(`config.js`)},
  1060. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  1061. StopWords: []string{"2024"},
  1062. },
  1063. expected: []report.Finding{
  1064. {
  1065. StartColumn: 50,
  1066. EndColumn: 60,
  1067. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  1068. Match: "Summer2024!",
  1069. Secret: "Summer2024!",
  1070. File: "config.js",
  1071. Entropy: 3.095795154571533,
  1072. RuleID: "test-rule",
  1073. },
  1074. },
  1075. },
  1076. "regex AND stopword NOT allowed": {
  1077. fragment: Fragment{},
  1078. allowlist: &config.Allowlist{
  1079. MatchCondition: config.AllowlistMatchAnd,
  1080. Regexes: []*regexp.Regexp{
  1081. regexp.MustCompile(`(?i)winter.+`),
  1082. },
  1083. StopWords: []string{"2024"},
  1084. },
  1085. expected: []report.Finding{
  1086. {
  1087. StartColumn: 50,
  1088. EndColumn: 60,
  1089. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  1090. Match: "Summer2024!",
  1091. Secret: "Summer2024!",
  1092. Entropy: 3.095795154571533,
  1093. RuleID: "test-rule",
  1094. },
  1095. },
  1096. },
  1097. "regex AND stopword NOT allowed - other conditions": {
  1098. fragment: Fragment{
  1099. CommitSHA: "a060c9d2d5e90c992763f1bd4c3cd2a6f121241b",
  1100. FilePath: "config.js",
  1101. },
  1102. allowlist: &config.Allowlist{
  1103. MatchCondition: config.AllowlistMatchAnd,
  1104. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  1105. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  1106. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)winter.+`)},
  1107. StopWords: []string{"2024"},
  1108. },
  1109. expected: []report.Finding{
  1110. {
  1111. StartColumn: 50,
  1112. EndColumn: 60,
  1113. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  1114. Match: "Summer2024!",
  1115. Secret: "Summer2024!",
  1116. File: "config.js",
  1117. Entropy: 3.095795154571533,
  1118. RuleID: "test-rule",
  1119. },
  1120. },
  1121. },
  1122. "regex OR stopword allowed": {
  1123. fragment: Fragment{},
  1124. allowlist: &config.Allowlist{
  1125. MatchCondition: config.AllowlistMatchOr,
  1126. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  1127. StopWords: []string{"winter"},
  1128. },
  1129. },
  1130. }
  1131. raw := `let username = 'james@mail.com';
  1132. let password = 'Summer2024!';`
  1133. for name, tc := range cases {
  1134. t.Run(name, func(t *testing.T) {
  1135. rule := config.Rule{
  1136. RuleID: "test-rule",
  1137. Regex: regexp.MustCompile(`Summer2024!`),
  1138. Allowlists: []*config.Allowlist{
  1139. tc.allowlist,
  1140. },
  1141. }
  1142. d, err := NewDetectorDefaultConfig()
  1143. require.NoError(t, err)
  1144. f := tc.fragment
  1145. f.Raw = raw
  1146. actual := d.detectRule(f, raw, rule, []EncodedSegment{})
  1147. if diff := cmp.Diff(tc.expected, actual); diff != "" {
  1148. t.Errorf("diff: (-want +got)\n%s", diff)
  1149. }
  1150. })
  1151. }
  1152. }
  1153. func moveDotGit(t *testing.T, from, to string) {
  1154. t.Helper()
  1155. repoDirs, err := os.ReadDir("../testdata/repos")
  1156. require.NoError(t, err)
  1157. for _, dir := range repoDirs {
  1158. if to == ".git" {
  1159. _, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), "dotGit"))
  1160. if os.IsNotExist(err) {
  1161. // dont want to delete the only copy of .git accidentally
  1162. continue
  1163. }
  1164. os.RemoveAll(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), ".git"))
  1165. }
  1166. if !dir.IsDir() {
  1167. continue
  1168. }
  1169. _, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from))
  1170. if os.IsNotExist(err) {
  1171. continue
  1172. }
  1173. err = os.Rename(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from),
  1174. fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), to))
  1175. require.NoError(t, err)
  1176. }
  1177. }
  1178. // region Windows-specific tests[]
  1179. func TestNormalizeGitleaksIgnorePaths(t *testing.T) {
  1180. d, err := NewDetectorDefaultConfig()
  1181. require.NoError(t, err)
  1182. err = d.AddGitleaksIgnore("../testdata/gitleaksignore/.windowspaths")
  1183. require.NoError(t, err)
  1184. assert.Len(t, d.gitleaksIgnore, 3)
  1185. expected := map[string]struct{}{
  1186. "foo/bar/gitleaks-false-positive.yaml:aws-access-token:4": {},
  1187. "foo/bar/gitleaks-false-positive.yaml:aws-access-token:5": {},
  1188. "b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:73": {},
  1189. }
  1190. assert.ElementsMatch(t, maps.Keys(d.gitleaksIgnore), maps.Keys(expected))
  1191. }
  1192. func TestWindowsFileSeparator_RulePath(t *testing.T) {
  1193. unixRule := config.Rule{
  1194. RuleID: "test-rule",
  1195. Path: regexp.MustCompile(`(^|/)\.m2/settings\.xml`),
  1196. }
  1197. windowsRule := config.Rule{
  1198. RuleID: "test-rule",
  1199. Path: regexp.MustCompile(`(^|\\)\.m2\\settings\.xml`),
  1200. }
  1201. expected := []report.Finding{
  1202. {
  1203. RuleID: "test-rule",
  1204. Match: "file detected: .m2/settings.xml",
  1205. File: ".m2/settings.xml",
  1206. },
  1207. }
  1208. tests := map[string]struct {
  1209. fragment Fragment
  1210. rule config.Rule
  1211. expected []report.Finding
  1212. }{
  1213. // unix rule
  1214. "unix rule - unix path separator": {
  1215. fragment: Fragment{
  1216. FilePath: `.m2/settings.xml`,
  1217. },
  1218. rule: unixRule,
  1219. expected: expected,
  1220. },
  1221. "unix rule - windows path separator": {
  1222. fragment: Fragment{
  1223. FilePath: `.m2/settings.xml`,
  1224. WindowsFilePath: `.m2\settings.xml`,
  1225. },
  1226. rule: unixRule,
  1227. expected: expected,
  1228. },
  1229. "unix regex+path rule - windows path separator": {
  1230. fragment: Fragment{
  1231. Raw: `<password>s3cr3t</password>`,
  1232. FilePath: `.m2/settings.xml`,
  1233. },
  1234. rule: config.Rule{
  1235. RuleID: "test-rule",
  1236. Regex: regexp.MustCompile(`<password>(.+?)</password>`),
  1237. Path: regexp.MustCompile(`(^|/)\.m2/settings\.xml`),
  1238. },
  1239. expected: []report.Finding{
  1240. {
  1241. RuleID: "test-rule",
  1242. StartColumn: 1,
  1243. EndColumn: 27,
  1244. Line: "<password>s3cr3t</password>",
  1245. Match: "<password>s3cr3t</password>",
  1246. Secret: "s3cr3t",
  1247. Entropy: 2.251629114151001,
  1248. File: ".m2/settings.xml",
  1249. },
  1250. },
  1251. },
  1252. // windows rule
  1253. "windows rule - unix path separator": {
  1254. fragment: Fragment{
  1255. FilePath: `.m2/settings.xml`,
  1256. },
  1257. rule: windowsRule,
  1258. // This never worked, and continues not to work.
  1259. // Paths should be normalized to use Unix file separators.
  1260. expected: nil,
  1261. },
  1262. "windows rule - windows path separator": {
  1263. fragment: Fragment{
  1264. FilePath: `.m2/settings.xml`,
  1265. WindowsFilePath: `.m2\settings.xml`,
  1266. },
  1267. rule: windowsRule,
  1268. expected: expected,
  1269. },
  1270. "windows regex+path rule - windows path separator": {
  1271. fragment: Fragment{
  1272. Raw: `<password>s3cr3t</password>`,
  1273. FilePath: `.m2/settings.xml`,
  1274. WindowsFilePath: `.m2\settings.xml`,
  1275. },
  1276. rule: config.Rule{
  1277. RuleID: "test-rule",
  1278. Regex: regexp.MustCompile(`<password>(.+?)</password>`),
  1279. Path: regexp.MustCompile(`(^|\\)\.m2\\settings\.xml`),
  1280. },
  1281. expected: []report.Finding{
  1282. {
  1283. RuleID: "test-rule",
  1284. StartColumn: 1,
  1285. EndColumn: 27,
  1286. Line: "<password>s3cr3t</password>",
  1287. Match: "<password>s3cr3t</password>",
  1288. Secret: "s3cr3t",
  1289. Entropy: 2.251629114151001,
  1290. File: ".m2/settings.xml",
  1291. },
  1292. }},
  1293. }
  1294. d, err := NewDetectorDefaultConfig()
  1295. require.NoError(t, err)
  1296. for name, test := range tests {
  1297. t.Run(name, func(t *testing.T) {
  1298. actual := d.detectRule(test.fragment, test.fragment.Raw, test.rule, []EncodedSegment{})
  1299. if diff := cmp.Diff(test.expected, actual); diff != "" {
  1300. t.Errorf("diff: (-want +got)\n%s", diff)
  1301. }
  1302. })
  1303. }
  1304. }
  1305. func TestWindowsFileSeparator_RuleAllowlistPaths(t *testing.T) {
  1306. tests := map[string]struct {
  1307. fragment Fragment
  1308. rule config.Rule
  1309. expected []report.Finding
  1310. }{
  1311. // unix
  1312. "unix path separator - unix rule - OR allowlist path-only": {
  1313. fragment: Fragment{
  1314. Raw: `value: "s3cr3t"`,
  1315. FilePath: `ignoreme/unix.txt`,
  1316. },
  1317. rule: config.Rule{
  1318. RuleID: "unix-rule",
  1319. Regex: regexp.MustCompile(`s3cr3t`),
  1320. Allowlists: []*config.Allowlist{
  1321. {
  1322. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)},
  1323. },
  1324. },
  1325. },
  1326. expected: nil,
  1327. },
  1328. "unix path separator - windows rule - OR allowlist path-only": {
  1329. fragment: Fragment{
  1330. Raw: `value: "s3cr3t"`,
  1331. FilePath: `ignoreme/unix.txt`,
  1332. },
  1333. rule: config.Rule{
  1334. RuleID: "windows-rule",
  1335. Regex: regexp.MustCompile(`s3cr3t`),
  1336. Allowlists: []*config.Allowlist{
  1337. {
  1338. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)},
  1339. },
  1340. },
  1341. },
  1342. // Windows separators in regex don't work for unix.
  1343. expected: []report.Finding{
  1344. {
  1345. RuleID: "windows-rule",
  1346. StartColumn: 9,
  1347. EndColumn: 14,
  1348. Line: `value: "s3cr3t"`,
  1349. Match: `s3cr3t`,
  1350. Secret: `s3cr3t`,
  1351. File: "ignoreme/unix.txt",
  1352. Entropy: 2.251629114151001,
  1353. },
  1354. },
  1355. },
  1356. "unix path separator - unix rule - AND allowlist path+stopwords": {
  1357. fragment: Fragment{
  1358. Raw: `value: "f4k3s3cr3t"`,
  1359. FilePath: `ignoreme/unix.txt`,
  1360. },
  1361. rule: config.Rule{
  1362. RuleID: "unix-rule",
  1363. Regex: regexp.MustCompile(`value: "[^"]+"`),
  1364. Allowlists: []*config.Allowlist{
  1365. {
  1366. MatchCondition: config.AllowlistMatchAnd,
  1367. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)},
  1368. StopWords: []string{"f4k3"},
  1369. },
  1370. },
  1371. },
  1372. expected: nil,
  1373. },
  1374. "unix path separator - windows rule - AND allowlist path+stopwords": {
  1375. fragment: Fragment{
  1376. Raw: `value: "f4k3s3cr3t"`,
  1377. FilePath: `ignoreme/unix.txt`,
  1378. },
  1379. rule: config.Rule{
  1380. RuleID: "windows-rule",
  1381. Regex: regexp.MustCompile(`value: "[^"]+"`),
  1382. Allowlists: []*config.Allowlist{
  1383. {
  1384. MatchCondition: config.AllowlistMatchAnd,
  1385. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)},
  1386. StopWords: []string{"f4k3"},
  1387. },
  1388. },
  1389. },
  1390. expected: []report.Finding{
  1391. {
  1392. RuleID: "windows-rule",
  1393. StartColumn: 1,
  1394. EndColumn: 19,
  1395. Line: `value: "f4k3s3cr3t"`,
  1396. Match: `value: "f4k3s3cr3t"`,
  1397. Secret: `value: "f4k3s3cr3t"`,
  1398. File: "ignoreme/unix.txt",
  1399. Entropy: 3.892407178878784,
  1400. },
  1401. },
  1402. },
  1403. // windows
  1404. "windows path separator - unix rule - OR allowlist path-only": {
  1405. fragment: Fragment{
  1406. Raw: `value: "s3cr3t"`,
  1407. FilePath: `ignoreme/windows.txt`,
  1408. WindowsFilePath: `ignoreme\windows.txt`,
  1409. },
  1410. rule: config.Rule{
  1411. RuleID: "unix-rule",
  1412. Regex: regexp.MustCompile(`s3cr3t`),
  1413. Allowlists: []*config.Allowlist{
  1414. {
  1415. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)},
  1416. },
  1417. },
  1418. },
  1419. expected: nil,
  1420. },
  1421. "windows path separator - windows rule - OR allowlist path-only": {
  1422. fragment: Fragment{
  1423. Raw: `value: "s3cr3t"`,
  1424. FilePath: `ignoreme/windows.txt`,
  1425. WindowsFilePath: `ignoreme\windows.txt`,
  1426. },
  1427. rule: config.Rule{
  1428. RuleID: "windows-rule",
  1429. Regex: regexp.MustCompile(`s3cr3t`),
  1430. Allowlists: []*config.Allowlist{
  1431. {
  1432. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)},
  1433. },
  1434. },
  1435. },
  1436. expected: nil,
  1437. },
  1438. "windows path separator - unix rule - AND allowlist path+stopwords": {
  1439. fragment: Fragment{
  1440. Raw: `value: "f4k3s3cr3t"`,
  1441. FilePath: `ignoreme/unix.txt`,
  1442. WindowsFilePath: `ignoreme\windows.txt`,
  1443. },
  1444. rule: config.Rule{
  1445. RuleID: "unix-rule",
  1446. Regex: regexp.MustCompile(`value: "[^"]+"`),
  1447. Allowlists: []*config.Allowlist{
  1448. {
  1449. MatchCondition: config.AllowlistMatchAnd,
  1450. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)},
  1451. StopWords: []string{"f4k3"},
  1452. },
  1453. },
  1454. },
  1455. expected: nil,
  1456. },
  1457. "windows path separator - windows rule - AND allowlist path+stopwords": {
  1458. fragment: Fragment{
  1459. Raw: `value: "f4k3s3cr3t"`,
  1460. FilePath: `ignoreme/unix.txt`,
  1461. WindowsFilePath: `ignoreme\windows.txt`,
  1462. },
  1463. rule: config.Rule{
  1464. RuleID: "windows-rule",
  1465. Regex: regexp.MustCompile(`value: "[^"]+"`),
  1466. Allowlists: []*config.Allowlist{
  1467. {
  1468. MatchCondition: config.AllowlistMatchAnd,
  1469. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)},
  1470. StopWords: []string{"f4k3"},
  1471. },
  1472. },
  1473. },
  1474. expected: nil,
  1475. },
  1476. }
  1477. d, err := NewDetectorDefaultConfig()
  1478. require.NoError(t, err)
  1479. for name, test := range tests {
  1480. t.Run(name, func(t *testing.T) {
  1481. actual := d.detectRule(test.fragment, test.fragment.Raw, test.rule, []EncodedSegment{})
  1482. if diff := cmp.Diff(test.expected, actual); diff != "" {
  1483. t.Errorf("diff: (-want +got)\n%s", diff)
  1484. }
  1485. })
  1486. }
  1487. }
  1488. //endregion