detect_test.go 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743
  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. source: filepath.Join(repoBasePath, "archive"),
  990. cfgName: "simple",
  991. expectedFindings: []report.Finding{
  992. {
  993. RuleID: "aws-access-key",
  994. Description: "AWS Access Key",
  995. StartLine: 20,
  996. EndLine: 20,
  997. StartColumn: 16,
  998. EndColumn: 35,
  999. Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
  1000. Match: "AKIALALEMEL33243OLIA",
  1001. Secret: "AKIALALEMEL33243OLIA",
  1002. File: "archive.zip/main.go",
  1003. SymlinkFile: "",
  1004. Tags: []string{"key", "AWS"},
  1005. Entropy: 3.0841837,
  1006. Fingerprint: "archive.zip/main.go:aws-access-key:20",
  1007. },
  1008. },
  1009. },
  1010. }
  1011. for _, tt := range tests {
  1012. t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) {
  1013. if strings.Contains(tt.source, "archive") && runtime.GOOS == "windows" {
  1014. t.Skipf("TODO: this test fails on windows")
  1015. }
  1016. viper.AddConfigPath(configPath)
  1017. viper.SetConfigName(tt.cfgName)
  1018. viper.SetConfigType("toml")
  1019. err := viper.ReadInConfig()
  1020. require.NoError(t, err)
  1021. var vc config.ViperConfig
  1022. err = viper.Unmarshal(&vc)
  1023. require.NoError(t, err)
  1024. cfg, _ := vc.Translate()
  1025. detector := NewDetector(cfg)
  1026. info, err := os.Stat(tt.source)
  1027. require.NoError(t, err)
  1028. var ignorePath string
  1029. if info.IsDir() {
  1030. ignorePath = filepath.Join(tt.source, ".gitleaksignore")
  1031. } else {
  1032. ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore")
  1033. }
  1034. err = detector.AddGitleaksIgnore(ignorePath)
  1035. require.NoError(t, err)
  1036. detector.FollowSymlinks = true
  1037. paths, err := sources.DirectoryTargets(tt.source, detector.Sema, true, cfg.Allowlists)
  1038. require.NoError(t, err)
  1039. findings, err := detector.DetectFiles(paths)
  1040. require.NoError(t, err)
  1041. // TODO: Temporary mitigation.
  1042. // https://github.com/gitleaks/gitleaks/issues/1641
  1043. normalizedFindings := make([]report.Finding, len(findings))
  1044. for i, f := range findings {
  1045. if strings.HasSuffix(f.Line, "\r") {
  1046. f.Line = strings.ReplaceAll(f.Line, "\r", "")
  1047. }
  1048. if strings.HasSuffix(f.Match, "\r") {
  1049. f.EndColumn = f.EndColumn - 1
  1050. f.Match = strings.ReplaceAll(f.Match, "\r", "")
  1051. }
  1052. normalizedFindings[i] = f
  1053. }
  1054. assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings)
  1055. })
  1056. }
  1057. }
  1058. func TestDetectWithSymlinks(t *testing.T) {
  1059. // TODO: Fix this test on windows.
  1060. if runtime.GOOS == "windows" {
  1061. t.Skipf("TODO: this returns no results on windows, I'm not sure why.")
  1062. return
  1063. }
  1064. tests := []struct {
  1065. cfgName string
  1066. source string
  1067. expectedFindings []report.Finding
  1068. }{
  1069. {
  1070. source: filepath.Join(repoBasePath, "symlinks/file_symlink"),
  1071. cfgName: "simple",
  1072. expectedFindings: []report.Finding{
  1073. {
  1074. RuleID: "apkey",
  1075. Description: "Asymmetric Private Key",
  1076. StartLine: 1,
  1077. EndLine: 1,
  1078. StartColumn: 1,
  1079. EndColumn: 35,
  1080. Match: "-----BEGIN OPENSSH PRIVATE KEY-----",
  1081. Secret: "-----BEGIN OPENSSH PRIVATE KEY-----",
  1082. Line: "-----BEGIN OPENSSH PRIVATE KEY-----",
  1083. File: "../testdata/repos/symlinks/source_file/id_ed25519",
  1084. SymlinkFile: "../testdata/repos/symlinks/file_symlink/symlinked_id_ed25519",
  1085. Tags: []string{"key", "AsymmetricPrivateKey"},
  1086. Entropy: 3.587164,
  1087. Fingerprint: "../testdata/repos/symlinks/source_file/id_ed25519:apkey:1",
  1088. },
  1089. },
  1090. },
  1091. }
  1092. for _, tt := range tests {
  1093. viper.AddConfigPath(configPath)
  1094. viper.SetConfigName("simple")
  1095. viper.SetConfigType("toml")
  1096. err := viper.ReadInConfig()
  1097. require.NoError(t, err)
  1098. var vc config.ViperConfig
  1099. err = viper.Unmarshal(&vc)
  1100. require.NoError(t, err)
  1101. cfg, _ := vc.Translate()
  1102. detector := NewDetector(cfg)
  1103. detector.FollowSymlinks = true
  1104. paths, err := sources.DirectoryTargets(tt.source, detector.Sema, true, cfg.Allowlists)
  1105. require.NoError(t, err)
  1106. findings, err := detector.DetectFiles(paths)
  1107. require.NoError(t, err)
  1108. assert.ElementsMatch(t, tt.expectedFindings, findings)
  1109. }
  1110. }
  1111. func TestDetectRuleAllowlist(t *testing.T) {
  1112. cases := map[string]struct {
  1113. fragment Fragment
  1114. allowlist *config.Allowlist
  1115. expected []report.Finding
  1116. }{
  1117. // Commit / path
  1118. "commit allowed": {
  1119. fragment: Fragment{
  1120. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  1121. },
  1122. allowlist: &config.Allowlist{
  1123. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  1124. },
  1125. },
  1126. "path allowed": {
  1127. fragment: Fragment{
  1128. FilePath: "package-lock.json",
  1129. },
  1130. allowlist: &config.Allowlist{
  1131. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  1132. },
  1133. },
  1134. "commit AND path allowed": {
  1135. fragment: Fragment{
  1136. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  1137. FilePath: "package-lock.json",
  1138. },
  1139. allowlist: &config.Allowlist{
  1140. MatchCondition: config.AllowlistMatchAnd,
  1141. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  1142. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  1143. },
  1144. },
  1145. "commit AND path NOT allowed": {
  1146. fragment: Fragment{
  1147. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  1148. FilePath: "package.json",
  1149. },
  1150. allowlist: &config.Allowlist{
  1151. MatchCondition: config.AllowlistMatchAnd,
  1152. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  1153. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  1154. },
  1155. expected: []report.Finding{
  1156. {
  1157. StartColumn: 50,
  1158. EndColumn: 60,
  1159. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  1160. Match: "Summer2024!",
  1161. Secret: "Summer2024!",
  1162. File: "package.json",
  1163. Entropy: 3.095795154571533,
  1164. RuleID: "test-rule",
  1165. },
  1166. },
  1167. },
  1168. "commit AND path NOT allowed - other conditions": {
  1169. fragment: Fragment{
  1170. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  1171. FilePath: "package-lock.json",
  1172. },
  1173. allowlist: &config.Allowlist{
  1174. MatchCondition: config.AllowlistMatchAnd,
  1175. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  1176. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  1177. Regexes: []*regexp.Regexp{regexp.MustCompile("password")},
  1178. },
  1179. expected: []report.Finding{
  1180. {
  1181. StartColumn: 50,
  1182. EndColumn: 60,
  1183. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  1184. Match: "Summer2024!",
  1185. Secret: "Summer2024!",
  1186. File: "package-lock.json",
  1187. Entropy: 3.095795154571533,
  1188. RuleID: "test-rule",
  1189. },
  1190. },
  1191. },
  1192. "commit OR path allowed": {
  1193. fragment: Fragment{
  1194. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  1195. FilePath: "package-lock.json",
  1196. },
  1197. allowlist: &config.Allowlist{
  1198. MatchCondition: config.AllowlistMatchOr,
  1199. Commits: []string{"704178e7dca77ff143778a31cff0fc192d59b030"},
  1200. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  1201. },
  1202. },
  1203. // Regex / stopwords
  1204. "regex allowed": {
  1205. fragment: Fragment{},
  1206. allowlist: &config.Allowlist{
  1207. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  1208. },
  1209. },
  1210. "stopwords allowed": {
  1211. fragment: Fragment{},
  1212. allowlist: &config.Allowlist{
  1213. StopWords: []string{"summer"},
  1214. },
  1215. },
  1216. "regex AND stopword allowed": {
  1217. fragment: Fragment{},
  1218. allowlist: &config.Allowlist{
  1219. MatchCondition: config.AllowlistMatchAnd,
  1220. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  1221. StopWords: []string{"2024"},
  1222. },
  1223. },
  1224. "regex AND stopword allowed - other conditions": {
  1225. fragment: Fragment{
  1226. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  1227. FilePath: "config.js",
  1228. },
  1229. allowlist: &config.Allowlist{
  1230. MatchCondition: config.AllowlistMatchAnd,
  1231. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  1232. Paths: []*regexp.Regexp{regexp.MustCompile(`config.js`)},
  1233. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  1234. StopWords: []string{"2024"},
  1235. },
  1236. },
  1237. "regex AND stopword NOT allowed - non-git, other conditions": {
  1238. fragment: Fragment{
  1239. FilePath: "config.js",
  1240. },
  1241. allowlist: &config.Allowlist{
  1242. MatchCondition: config.AllowlistMatchAnd,
  1243. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  1244. Paths: []*regexp.Regexp{regexp.MustCompile(`config.js`)},
  1245. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  1246. StopWords: []string{"2024"},
  1247. },
  1248. expected: []report.Finding{
  1249. {
  1250. StartColumn: 50,
  1251. EndColumn: 60,
  1252. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  1253. Match: "Summer2024!",
  1254. Secret: "Summer2024!",
  1255. File: "config.js",
  1256. Entropy: 3.095795154571533,
  1257. RuleID: "test-rule",
  1258. },
  1259. },
  1260. },
  1261. "regex AND stopword NOT allowed": {
  1262. fragment: Fragment{},
  1263. allowlist: &config.Allowlist{
  1264. MatchCondition: config.AllowlistMatchAnd,
  1265. Regexes: []*regexp.Regexp{
  1266. regexp.MustCompile(`(?i)winter.+`),
  1267. },
  1268. StopWords: []string{"2024"},
  1269. },
  1270. expected: []report.Finding{
  1271. {
  1272. StartColumn: 50,
  1273. EndColumn: 60,
  1274. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  1275. Match: "Summer2024!",
  1276. Secret: "Summer2024!",
  1277. Entropy: 3.095795154571533,
  1278. RuleID: "test-rule",
  1279. },
  1280. },
  1281. },
  1282. "regex AND stopword NOT allowed - other conditions": {
  1283. fragment: Fragment{
  1284. CommitSHA: "a060c9d2d5e90c992763f1bd4c3cd2a6f121241b",
  1285. FilePath: "config.js",
  1286. },
  1287. allowlist: &config.Allowlist{
  1288. MatchCondition: config.AllowlistMatchAnd,
  1289. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  1290. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  1291. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)winter.+`)},
  1292. StopWords: []string{"2024"},
  1293. },
  1294. expected: []report.Finding{
  1295. {
  1296. StartColumn: 50,
  1297. EndColumn: 60,
  1298. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  1299. Match: "Summer2024!",
  1300. Secret: "Summer2024!",
  1301. File: "config.js",
  1302. Entropy: 3.095795154571533,
  1303. RuleID: "test-rule",
  1304. },
  1305. },
  1306. },
  1307. "regex OR stopword allowed": {
  1308. fragment: Fragment{},
  1309. allowlist: &config.Allowlist{
  1310. MatchCondition: config.AllowlistMatchOr,
  1311. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  1312. StopWords: []string{"winter"},
  1313. },
  1314. },
  1315. }
  1316. raw := `let username = 'james@mail.com';
  1317. let password = 'Summer2024!';`
  1318. for name, tc := range cases {
  1319. t.Run(name, func(t *testing.T) {
  1320. rule := config.Rule{
  1321. RuleID: "test-rule",
  1322. Regex: regexp.MustCompile(`Summer2024!`),
  1323. Allowlists: []*config.Allowlist{
  1324. tc.allowlist,
  1325. },
  1326. }
  1327. d, err := NewDetectorDefaultConfig()
  1328. require.NoError(t, err)
  1329. f := tc.fragment
  1330. f.Raw = raw
  1331. actual := d.detectRule(f, raw, rule, []*codec.EncodedSegment{})
  1332. if diff := cmp.Diff(tc.expected, actual); diff != "" {
  1333. t.Errorf("diff: (-want +got)\n%s", diff)
  1334. }
  1335. })
  1336. }
  1337. }
  1338. func moveDotGit(t *testing.T, from, to string) {
  1339. t.Helper()
  1340. repoDirs, err := os.ReadDir("../testdata/repos")
  1341. require.NoError(t, err)
  1342. for _, dir := range repoDirs {
  1343. if to == ".git" {
  1344. _, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), "dotGit"))
  1345. if os.IsNotExist(err) {
  1346. // dont want to delete the only copy of .git accidentally
  1347. continue
  1348. }
  1349. os.RemoveAll(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), ".git"))
  1350. }
  1351. if !dir.IsDir() {
  1352. continue
  1353. }
  1354. _, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from))
  1355. if os.IsNotExist(err) {
  1356. continue
  1357. }
  1358. err = os.Rename(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from),
  1359. fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), to))
  1360. require.NoError(t, err)
  1361. }
  1362. }
  1363. // region Windows-specific tests[]
  1364. func TestNormalizeGitleaksIgnorePaths(t *testing.T) {
  1365. d, err := NewDetectorDefaultConfig()
  1366. require.NoError(t, err)
  1367. err = d.AddGitleaksIgnore("../testdata/gitleaksignore/.windowspaths")
  1368. require.NoError(t, err)
  1369. assert.Len(t, d.gitleaksIgnore, 3)
  1370. expected := map[string]struct{}{
  1371. "foo/bar/gitleaks-false-positive.yaml:aws-access-token:4": {},
  1372. "foo/bar/gitleaks-false-positive.yaml:aws-access-token:5": {},
  1373. "b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:73": {},
  1374. }
  1375. assert.ElementsMatch(t, maps.Keys(d.gitleaksIgnore), maps.Keys(expected))
  1376. }
  1377. func TestWindowsFileSeparator_RulePath(t *testing.T) {
  1378. unixRule := config.Rule{
  1379. RuleID: "test-rule",
  1380. Path: regexp.MustCompile(`(^|/)\.m2/settings\.xml`),
  1381. }
  1382. windowsRule := config.Rule{
  1383. RuleID: "test-rule",
  1384. Path: regexp.MustCompile(`(^|\\)\.m2\\settings\.xml`),
  1385. }
  1386. expected := []report.Finding{
  1387. {
  1388. RuleID: "test-rule",
  1389. Match: "file detected: .m2/settings.xml",
  1390. File: ".m2/settings.xml",
  1391. },
  1392. }
  1393. tests := map[string]struct {
  1394. fragment Fragment
  1395. rule config.Rule
  1396. expected []report.Finding
  1397. }{
  1398. // unix rule
  1399. "unix rule - unix path separator": {
  1400. fragment: Fragment{
  1401. FilePath: `.m2/settings.xml`,
  1402. },
  1403. rule: unixRule,
  1404. expected: expected,
  1405. },
  1406. "unix rule - windows path separator": {
  1407. fragment: Fragment{
  1408. FilePath: `.m2/settings.xml`,
  1409. WindowsFilePath: `.m2\settings.xml`,
  1410. },
  1411. rule: unixRule,
  1412. expected: expected,
  1413. },
  1414. "unix regex+path rule - windows path separator": {
  1415. fragment: Fragment{
  1416. Raw: `<password>s3cr3t</password>`,
  1417. FilePath: `.m2/settings.xml`,
  1418. },
  1419. rule: config.Rule{
  1420. RuleID: "test-rule",
  1421. Regex: regexp.MustCompile(`<password>(.+?)</password>`),
  1422. Path: regexp.MustCompile(`(^|/)\.m2/settings\.xml`),
  1423. },
  1424. expected: []report.Finding{
  1425. {
  1426. RuleID: "test-rule",
  1427. StartColumn: 1,
  1428. EndColumn: 27,
  1429. Line: "<password>s3cr3t</password>",
  1430. Match: "<password>s3cr3t</password>",
  1431. Secret: "s3cr3t",
  1432. Entropy: 2.251629114151001,
  1433. File: ".m2/settings.xml",
  1434. },
  1435. },
  1436. },
  1437. // windows rule
  1438. "windows rule - unix path separator": {
  1439. fragment: Fragment{
  1440. FilePath: `.m2/settings.xml`,
  1441. },
  1442. rule: windowsRule,
  1443. // This never worked, and continues not to work.
  1444. // Paths should be normalized to use Unix file separators.
  1445. expected: nil,
  1446. },
  1447. "windows rule - windows path separator": {
  1448. fragment: Fragment{
  1449. FilePath: `.m2/settings.xml`,
  1450. WindowsFilePath: `.m2\settings.xml`,
  1451. },
  1452. rule: windowsRule,
  1453. expected: expected,
  1454. },
  1455. "windows regex+path rule - windows path separator": {
  1456. fragment: Fragment{
  1457. Raw: `<password>s3cr3t</password>`,
  1458. FilePath: `.m2/settings.xml`,
  1459. WindowsFilePath: `.m2\settings.xml`,
  1460. },
  1461. rule: config.Rule{
  1462. RuleID: "test-rule",
  1463. Regex: regexp.MustCompile(`<password>(.+?)</password>`),
  1464. Path: regexp.MustCompile(`(^|\\)\.m2\\settings\.xml`),
  1465. },
  1466. expected: []report.Finding{
  1467. {
  1468. RuleID: "test-rule",
  1469. StartColumn: 1,
  1470. EndColumn: 27,
  1471. Line: "<password>s3cr3t</password>",
  1472. Match: "<password>s3cr3t</password>",
  1473. Secret: "s3cr3t",
  1474. Entropy: 2.251629114151001,
  1475. File: ".m2/settings.xml",
  1476. },
  1477. }},
  1478. }
  1479. d, err := NewDetectorDefaultConfig()
  1480. require.NoError(t, err)
  1481. for name, test := range tests {
  1482. t.Run(name, func(t *testing.T) {
  1483. actual := d.detectRule(test.fragment, test.fragment.Raw, test.rule, []*codec.EncodedSegment{})
  1484. if diff := cmp.Diff(test.expected, actual); diff != "" {
  1485. t.Errorf("diff: (-want +got)\n%s", diff)
  1486. }
  1487. })
  1488. }
  1489. }
  1490. func TestWindowsFileSeparator_RuleAllowlistPaths(t *testing.T) {
  1491. tests := map[string]struct {
  1492. fragment Fragment
  1493. rule config.Rule
  1494. expected []report.Finding
  1495. }{
  1496. // unix
  1497. "unix path separator - unix rule - OR allowlist path-only": {
  1498. fragment: Fragment{
  1499. Raw: `value: "s3cr3t"`,
  1500. FilePath: `ignoreme/unix.txt`,
  1501. },
  1502. rule: config.Rule{
  1503. RuleID: "unix-rule",
  1504. Regex: regexp.MustCompile(`s3cr3t`),
  1505. Allowlists: []*config.Allowlist{
  1506. {
  1507. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)},
  1508. },
  1509. },
  1510. },
  1511. expected: nil,
  1512. },
  1513. "unix path separator - windows rule - OR allowlist path-only": {
  1514. fragment: Fragment{
  1515. Raw: `value: "s3cr3t"`,
  1516. FilePath: `ignoreme/unix.txt`,
  1517. },
  1518. rule: config.Rule{
  1519. RuleID: "windows-rule",
  1520. Regex: regexp.MustCompile(`s3cr3t`),
  1521. Allowlists: []*config.Allowlist{
  1522. {
  1523. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)},
  1524. },
  1525. },
  1526. },
  1527. // Windows separators in regex don't work for unix.
  1528. expected: []report.Finding{
  1529. {
  1530. RuleID: "windows-rule",
  1531. StartColumn: 9,
  1532. EndColumn: 14,
  1533. Line: `value: "s3cr3t"`,
  1534. Match: `s3cr3t`,
  1535. Secret: `s3cr3t`,
  1536. File: "ignoreme/unix.txt",
  1537. Entropy: 2.251629114151001,
  1538. },
  1539. },
  1540. },
  1541. "unix path separator - unix rule - AND allowlist path+stopwords": {
  1542. fragment: Fragment{
  1543. Raw: `value: "f4k3s3cr3t"`,
  1544. FilePath: `ignoreme/unix.txt`,
  1545. },
  1546. rule: config.Rule{
  1547. RuleID: "unix-rule",
  1548. Regex: regexp.MustCompile(`value: "[^"]+"`),
  1549. Allowlists: []*config.Allowlist{
  1550. {
  1551. MatchCondition: config.AllowlistMatchAnd,
  1552. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)},
  1553. StopWords: []string{"f4k3"},
  1554. },
  1555. },
  1556. },
  1557. expected: nil,
  1558. },
  1559. "unix path separator - windows rule - AND allowlist path+stopwords": {
  1560. fragment: Fragment{
  1561. Raw: `value: "f4k3s3cr3t"`,
  1562. FilePath: `ignoreme/unix.txt`,
  1563. },
  1564. rule: config.Rule{
  1565. RuleID: "windows-rule",
  1566. Regex: regexp.MustCompile(`value: "[^"]+"`),
  1567. Allowlists: []*config.Allowlist{
  1568. {
  1569. MatchCondition: config.AllowlistMatchAnd,
  1570. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)},
  1571. StopWords: []string{"f4k3"},
  1572. },
  1573. },
  1574. },
  1575. expected: []report.Finding{
  1576. {
  1577. RuleID: "windows-rule",
  1578. StartColumn: 1,
  1579. EndColumn: 19,
  1580. Line: `value: "f4k3s3cr3t"`,
  1581. Match: `value: "f4k3s3cr3t"`,
  1582. Secret: `value: "f4k3s3cr3t"`,
  1583. File: "ignoreme/unix.txt",
  1584. Entropy: 3.892407178878784,
  1585. },
  1586. },
  1587. },
  1588. // windows
  1589. "windows path separator - unix rule - OR allowlist path-only": {
  1590. fragment: Fragment{
  1591. Raw: `value: "s3cr3t"`,
  1592. FilePath: `ignoreme/windows.txt`,
  1593. WindowsFilePath: `ignoreme\windows.txt`,
  1594. },
  1595. rule: config.Rule{
  1596. RuleID: "unix-rule",
  1597. Regex: regexp.MustCompile(`s3cr3t`),
  1598. Allowlists: []*config.Allowlist{
  1599. {
  1600. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)},
  1601. },
  1602. },
  1603. },
  1604. expected: nil,
  1605. },
  1606. "windows path separator - windows rule - OR allowlist path-only": {
  1607. fragment: Fragment{
  1608. Raw: `value: "s3cr3t"`,
  1609. FilePath: `ignoreme/windows.txt`,
  1610. WindowsFilePath: `ignoreme\windows.txt`,
  1611. },
  1612. rule: config.Rule{
  1613. RuleID: "windows-rule",
  1614. Regex: regexp.MustCompile(`s3cr3t`),
  1615. Allowlists: []*config.Allowlist{
  1616. {
  1617. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)},
  1618. },
  1619. },
  1620. },
  1621. expected: nil,
  1622. },
  1623. "windows path separator - unix rule - AND allowlist path+stopwords": {
  1624. fragment: Fragment{
  1625. Raw: `value: "f4k3s3cr3t"`,
  1626. FilePath: `ignoreme/unix.txt`,
  1627. WindowsFilePath: `ignoreme\windows.txt`,
  1628. },
  1629. rule: config.Rule{
  1630. RuleID: "unix-rule",
  1631. Regex: regexp.MustCompile(`value: "[^"]+"`),
  1632. Allowlists: []*config.Allowlist{
  1633. {
  1634. MatchCondition: config.AllowlistMatchAnd,
  1635. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)},
  1636. StopWords: []string{"f4k3"},
  1637. },
  1638. },
  1639. },
  1640. expected: nil,
  1641. },
  1642. "windows path separator - windows rule - AND allowlist path+stopwords": {
  1643. fragment: Fragment{
  1644. Raw: `value: "f4k3s3cr3t"`,
  1645. FilePath: `ignoreme/unix.txt`,
  1646. WindowsFilePath: `ignoreme\windows.txt`,
  1647. },
  1648. rule: config.Rule{
  1649. RuleID: "windows-rule",
  1650. Regex: regexp.MustCompile(`value: "[^"]+"`),
  1651. Allowlists: []*config.Allowlist{
  1652. {
  1653. MatchCondition: config.AllowlistMatchAnd,
  1654. Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)},
  1655. StopWords: []string{"f4k3"},
  1656. },
  1657. },
  1658. },
  1659. expected: nil,
  1660. },
  1661. }
  1662. d, err := NewDetectorDefaultConfig()
  1663. require.NoError(t, err)
  1664. for name, test := range tests {
  1665. t.Run(name, func(t *testing.T) {
  1666. actual := d.detectRule(test.fragment, test.fragment.Raw, test.rule, []*codec.EncodedSegment{})
  1667. if diff := cmp.Diff(test.expected, actual); diff != "" {
  1668. t.Errorf("diff: (-want +got)\n%s", diff)
  1669. }
  1670. })
  1671. }
  1672. }
  1673. //endregion