detect_test.go 56 KB

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