detect_test.go 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122
  1. package detect
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. "testing"
  8. "github.com/google/go-cmp/cmp"
  9. "github.com/spf13/viper"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. "github.com/zricethezav/gitleaks/v8/cmd/scm"
  13. "github.com/zricethezav/gitleaks/v8/config"
  14. "github.com/zricethezav/gitleaks/v8/regexp"
  15. "github.com/zricethezav/gitleaks/v8/report"
  16. "github.com/zricethezav/gitleaks/v8/sources"
  17. )
  18. const maxDecodeDepth = 8
  19. const configPath = "../testdata/config/"
  20. const repoBasePath = "../testdata/repos/"
  21. const b64TestValues = `
  22. # Decoded
  23. -----BEGIN PRIVATE KEY-----
  24. 135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb
  25. u+QDkg0spw==
  26. -----END PRIVATE KEY-----
  27. # Encoded
  28. private_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'
  29. # Double Encoded: b64 encoded aws config inside a jwt
  30. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA
  31. # A small secret at the end to make sure that as the other ones above shrink
  32. # when decoded, the positions are taken into consideratoin for overlaps
  33. c21hbGwtc2VjcmV0
  34. # This tests how it handles when the match bounds go outside the decoded value
  35. secret=ZGVjb2RlZC1zZWNyZXQtdmFsdWU=
  36. # The above encoded again
  37. c2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=
  38. `
  39. func TestDetect(t *testing.T) {
  40. tests := []struct {
  41. cfgName string
  42. baselinePath string
  43. fragment Fragment
  44. // NOTE: for expected findings, all line numbers will be 0
  45. // because line deltas are added _after_ the finding is created.
  46. // I.e., if the finding is from a --no-git file, the line number will be
  47. // increase by 1 in DetectFromFiles(). If the finding is from git,
  48. // the line number will be increased by the patch delta.
  49. expectedFindings []report.Finding
  50. wantError error
  51. }{
  52. {
  53. cfgName: "simple",
  54. fragment: Fragment{
  55. Raw: `awsToken := \"AKIALALEMEL33243OKIA\ // gitleaks:allow"`,
  56. FilePath: "tmp.go",
  57. },
  58. },
  59. {
  60. cfgName: "simple",
  61. fragment: Fragment{
  62. Raw: `awsToken := \
  63. \"AKIALALEMEL33243OKIA\ // gitleaks:allow"
  64. `,
  65. FilePath: "tmp.go",
  66. },
  67. },
  68. {
  69. cfgName: "simple",
  70. fragment: Fragment{
  71. Raw: `awsToken := \"AKIALALEMEL33243OKIA\"
  72. // gitleaks:allow"
  73. `,
  74. FilePath: "tmp.go",
  75. },
  76. expectedFindings: []report.Finding{
  77. {
  78. Description: "AWS Access Key",
  79. Secret: "AKIALALEMEL33243OKIA",
  80. Match: "AKIALALEMEL33243OKIA",
  81. File: "tmp.go",
  82. Line: `awsToken := \"AKIALALEMEL33243OKIA\"`,
  83. RuleID: "aws-access-key",
  84. Tags: []string{"key", "AWS"},
  85. StartLine: 0,
  86. EndLine: 0,
  87. StartColumn: 15,
  88. EndColumn: 34,
  89. Entropy: 3.1464393,
  90. },
  91. },
  92. },
  93. {
  94. cfgName: "escaped_character_group",
  95. fragment: Fragment{
  96. Raw: `pypi-AgEIcHlwaS5vcmcAAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAAB`,
  97. FilePath: "tmp.go",
  98. },
  99. expectedFindings: []report.Finding{
  100. {
  101. Description: "PyPI upload token",
  102. Secret: "pypi-AgEIcHlwaS5vcmcAAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAAB",
  103. Match: "pypi-AgEIcHlwaS5vcmcAAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAAB",
  104. Line: `pypi-AgEIcHlwaS5vcmcAAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAAB`,
  105. File: "tmp.go",
  106. RuleID: "pypi-upload-token",
  107. Tags: []string{"key", "pypi"},
  108. StartLine: 0,
  109. EndLine: 0,
  110. StartColumn: 1,
  111. EndColumn: 86,
  112. Entropy: 1.9606875,
  113. },
  114. },
  115. },
  116. {
  117. cfgName: "simple",
  118. fragment: Fragment{
  119. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  120. FilePath: "tmp.go",
  121. },
  122. expectedFindings: []report.Finding{
  123. {
  124. Description: "AWS Access Key",
  125. Secret: "AKIALALEMEL33243OLIA",
  126. Match: "AKIALALEMEL33243OLIA",
  127. Line: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  128. File: "tmp.go",
  129. RuleID: "aws-access-key",
  130. Tags: []string{"key", "AWS"},
  131. StartLine: 0,
  132. EndLine: 0,
  133. StartColumn: 15,
  134. EndColumn: 34,
  135. Entropy: 3.0841837,
  136. },
  137. },
  138. },
  139. {
  140. cfgName: "simple",
  141. fragment: Fragment{
  142. Raw: `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
  143. FilePath: "tmp.sh",
  144. },
  145. expectedFindings: []report.Finding{
  146. {
  147. Description: "Sidekiq Secret",
  148. Match: "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;",
  149. Secret: "cafebabe:deadbeef",
  150. Line: `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
  151. File: "tmp.sh",
  152. RuleID: "sidekiq-secret",
  153. Tags: []string{},
  154. Entropy: 2.6098502,
  155. StartLine: 0,
  156. EndLine: 0,
  157. StartColumn: 8,
  158. EndColumn: 60,
  159. },
  160. },
  161. },
  162. {
  163. cfgName: "simple",
  164. fragment: Fragment{
  165. Raw: `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
  166. FilePath: "tmp.sh",
  167. },
  168. expectedFindings: []report.Finding{
  169. {
  170. Description: "Sidekiq Secret",
  171. Match: "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=\"cafebabe:deadbeef\"",
  172. Secret: "cafebabe:deadbeef",
  173. File: "tmp.sh",
  174. Line: `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
  175. RuleID: "sidekiq-secret",
  176. Tags: []string{},
  177. Entropy: 2.6098502,
  178. StartLine: 0,
  179. EndLine: 0,
  180. StartColumn: 21,
  181. EndColumn: 74,
  182. },
  183. },
  184. },
  185. {
  186. cfgName: "simple",
  187. fragment: Fragment{
  188. Raw: `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
  189. FilePath: "tmp.sh",
  190. },
  191. expectedFindings: []report.Finding{
  192. {
  193. Description: "Sidekiq Sensitive URL",
  194. Match: "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:",
  195. Secret: "cafeb4b3:d3adb33f",
  196. File: "tmp.sh",
  197. Line: `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
  198. RuleID: "sidekiq-sensitive-url",
  199. Tags: []string{},
  200. Entropy: 2.984234,
  201. StartLine: 0,
  202. EndLine: 0,
  203. StartColumn: 8,
  204. EndColumn: 58,
  205. },
  206. },
  207. },
  208. {
  209. cfgName: "allow_aws_re",
  210. fragment: Fragment{
  211. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  212. FilePath: "tmp.go",
  213. },
  214. },
  215. {
  216. cfgName: "allow_path",
  217. fragment: Fragment{
  218. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  219. FilePath: "tmp.go",
  220. },
  221. },
  222. {
  223. cfgName: "allow_commit",
  224. fragment: Fragment{
  225. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  226. FilePath: "tmp.go",
  227. CommitSHA: "allowthiscommit",
  228. },
  229. },
  230. {
  231. cfgName: "entropy_group",
  232. fragment: Fragment{
  233. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  234. FilePath: "tmp.go",
  235. },
  236. expectedFindings: []report.Finding{
  237. {
  238. Description: "Discord API key",
  239. Match: "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
  240. Secret: "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
  241. Line: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  242. File: "tmp.go",
  243. RuleID: "discord-api-key",
  244. Tags: []string{},
  245. Entropy: 3.7906237,
  246. StartLine: 0,
  247. EndLine: 0,
  248. StartColumn: 7,
  249. EndColumn: 93,
  250. },
  251. },
  252. },
  253. {
  254. cfgName: "generic_with_py_path",
  255. fragment: Fragment{
  256. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  257. FilePath: "tmp.go",
  258. },
  259. },
  260. {
  261. cfgName: "generic_with_py_path",
  262. fragment: Fragment{
  263. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  264. FilePath: "tmp.py",
  265. },
  266. expectedFindings: []report.Finding{
  267. {
  268. Description: "Generic API Key",
  269. Match: "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
  270. Secret: "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
  271. Line: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  272. File: "tmp.py",
  273. RuleID: "generic-api-key",
  274. Tags: []string{},
  275. Entropy: 3.7906237,
  276. StartLine: 0,
  277. EndLine: 0,
  278. StartColumn: 22,
  279. EndColumn: 93,
  280. },
  281. },
  282. },
  283. {
  284. cfgName: "path_only",
  285. fragment: Fragment{
  286. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  287. FilePath: "tmp.py",
  288. },
  289. expectedFindings: []report.Finding{
  290. {
  291. Description: "Python Files",
  292. Match: "file detected: tmp.py",
  293. File: "tmp.py",
  294. RuleID: "python-files-only",
  295. Tags: []string{},
  296. },
  297. },
  298. },
  299. {
  300. cfgName: "bad_entropy_group",
  301. fragment: Fragment{
  302. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  303. FilePath: "tmp.go",
  304. },
  305. wantError: fmt.Errorf("discord-api-key: invalid regex secret group 5, max regex secret group 3"),
  306. },
  307. {
  308. cfgName: "simple",
  309. fragment: Fragment{
  310. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  311. FilePath: filepath.Join(configPath, "simple.toml"),
  312. },
  313. },
  314. {
  315. cfgName: "allow_global_aws_re",
  316. fragment: Fragment{
  317. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  318. FilePath: "tmp.go",
  319. },
  320. },
  321. {
  322. cfgName: "generic_with_py_path",
  323. fragment: Fragment{
  324. Raw: `const Discord_Public_Key = "load2523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  325. FilePath: "tmp.py",
  326. },
  327. },
  328. {
  329. cfgName: "path_only",
  330. baselinePath: ".baseline.json",
  331. fragment: Fragment{
  332. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  333. FilePath: ".baseline.json",
  334. },
  335. },
  336. {
  337. cfgName: "base64_encoded",
  338. fragment: Fragment{
  339. Raw: b64TestValues,
  340. FilePath: "tmp.go",
  341. },
  342. expectedFindings: []report.Finding{
  343. { // Plain text key captured by normal rule
  344. Description: "Private Key",
  345. Secret: "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
  346. Match: "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
  347. File: "tmp.go",
  348. Line: "\n-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
  349. RuleID: "private-key",
  350. Tags: []string{"key", "private"},
  351. StartLine: 2,
  352. EndLine: 5,
  353. StartColumn: 2,
  354. EndColumn: 26,
  355. Entropy: 5.350665,
  356. },
  357. { // Encoded key captured by custom b64 regex rule
  358. Description: "Private Key",
  359. Secret: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K",
  360. Match: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K",
  361. File: "tmp.go",
  362. Line: "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'",
  363. RuleID: "b64-encoded-private-key",
  364. Tags: []string{"key", "private"},
  365. StartLine: 8,
  366. EndLine: 8,
  367. StartColumn: 16,
  368. EndColumn: 207,
  369. Entropy: 5.3861146,
  370. },
  371. { // Encoded key captured by plain text rule using the decoder
  372. Description: "Private Key",
  373. Secret: "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
  374. Match: "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
  375. File: "tmp.go",
  376. Line: "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'",
  377. RuleID: "private-key",
  378. Tags: []string{"key", "private", "decoded:base64", "decode-depth:1"},
  379. StartLine: 8,
  380. EndLine: 8,
  381. StartColumn: 16,
  382. EndColumn: 207,
  383. Entropy: 5.350665,
  384. },
  385. { // Encoded AWS config with a access key id inside a JWT
  386. Description: "AWS IAM Unique Identifier",
  387. Secret: "ASIAIOSFODNN7LXM10JI",
  388. Match: " ASIAIOSFODNN7LXM10JI",
  389. File: "tmp.go",
  390. Line: "\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA",
  391. RuleID: "aws-iam-unique-identifier",
  392. Tags: []string{"aws", "identifier", "decoded:base64", "decode-depth:2"},
  393. StartLine: 11,
  394. EndLine: 11,
  395. StartColumn: 39,
  396. EndColumn: 344,
  397. Entropy: 3.6841838,
  398. },
  399. { // Encoded AWS config with a secret access key inside a JWT
  400. Description: "AWS Secret Access Key",
  401. Secret: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEFUCDlEX1A",
  402. Match: "aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEFUCDlEX1A",
  403. File: "tmp.go",
  404. Line: "\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA",
  405. RuleID: "aws-secret-access-key",
  406. Tags: []string{"aws", "secret", "decoded:base64", "decode-depth:2"},
  407. StartLine: 11,
  408. EndLine: 11,
  409. StartColumn: 39,
  410. EndColumn: 344,
  411. Entropy: 4.721928,
  412. },
  413. { // Encoded Small secret at the end to make sure it's picked up by the decoding
  414. Description: "Small Secret",
  415. Secret: "small-secret",
  416. Match: "small-secret",
  417. File: "tmp.go",
  418. Line: "\nc21hbGwtc2VjcmV0",
  419. RuleID: "small-secret",
  420. Tags: []string{"small", "secret", "decoded:base64", "decode-depth:1"},
  421. StartLine: 15,
  422. EndLine: 15,
  423. StartColumn: 2,
  424. EndColumn: 17,
  425. Entropy: 3.0849626,
  426. },
  427. { // Secret where the decoded match goes outside the encoded value
  428. Description: "Overlapping",
  429. Secret: "decoded-secret-value",
  430. Match: "secret=decoded-secret-value",
  431. File: "tmp.go",
  432. Line: "\nsecret=ZGVjb2RlZC1zZWNyZXQtdmFsdWU=",
  433. RuleID: "overlapping",
  434. Tags: []string{"overlapping", "decoded:base64", "decode-depth:1"},
  435. StartLine: 18,
  436. EndLine: 18,
  437. StartColumn: 2,
  438. EndColumn: 36,
  439. Entropy: 3.3037016,
  440. },
  441. { // Secret where the decoded match goes outside the encoded value and then encoded again
  442. Description: "Overlapping",
  443. Secret: "decoded-secret-value",
  444. Match: "secret=decoded-secret-value",
  445. File: "tmp.go",
  446. Line: "\nc2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=",
  447. RuleID: "overlapping",
  448. Tags: []string{"overlapping", "decoded:base64", "decode-depth:2"},
  449. StartLine: 20,
  450. EndLine: 20,
  451. StartColumn: 2,
  452. EndColumn: 49,
  453. Entropy: 3.3037016,
  454. },
  455. },
  456. },
  457. }
  458. for _, tt := range tests {
  459. t.Run(fmt.Sprintf("%s - %s", tt.cfgName, tt.fragment.FilePath), func(t *testing.T) {
  460. viper.Reset()
  461. viper.AddConfigPath(configPath)
  462. viper.SetConfigName(tt.cfgName)
  463. viper.SetConfigType("toml")
  464. err := viper.ReadInConfig()
  465. require.NoError(t, err)
  466. var vc config.ViperConfig
  467. err = viper.Unmarshal(&vc)
  468. require.NoError(t, err)
  469. cfg, err := vc.Translate()
  470. cfg.Path = filepath.Join(configPath, tt.cfgName+".toml")
  471. assert.Equal(t, tt.wantError, err)
  472. d := NewDetector(cfg)
  473. d.MaxDecodeDepth = maxDecodeDepth
  474. d.baselinePath = tt.baselinePath
  475. findings := d.Detect(tt.fragment)
  476. assert.ElementsMatch(t, tt.expectedFindings, findings)
  477. })
  478. }
  479. }
  480. // TestFromGit tests the FromGit function
  481. func TestFromGit(t *testing.T) {
  482. tests := []struct {
  483. cfgName string
  484. source string
  485. logOpts string
  486. expectedFindings []report.Finding
  487. }{
  488. {
  489. source: filepath.Join(repoBasePath, "small"),
  490. cfgName: "simple", // the remote url is `git@github.com:gitleaks/test.git`
  491. expectedFindings: []report.Finding{
  492. {
  493. RuleID: "aws-access-key",
  494. Description: "AWS Access Key",
  495. StartLine: 20,
  496. EndLine: 20,
  497. StartColumn: 19,
  498. EndColumn: 38,
  499. Line: "\n awsToken := \"AKIALALEMEL33243OLIA\"",
  500. Secret: "AKIALALEMEL33243OLIA",
  501. Match: "AKIALALEMEL33243OLIA",
  502. File: "main.go",
  503. Date: "2021-11-02T23:37:53Z",
  504. Commit: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587",
  505. Author: "Zachary Rice",
  506. Email: "zricer@protonmail.com",
  507. Message: "Accidentally add a secret",
  508. Tags: []string{"key", "AWS"},
  509. Entropy: 3.0841837,
  510. Fingerprint: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587:main.go:aws-access-key:20",
  511. Link: "https://github.com/gitleaks/test/blob/1b6da43b82b22e4eaa10bcf8ee591e91abbfc587/main.go#L20",
  512. },
  513. {
  514. RuleID: "aws-access-key",
  515. Description: "AWS Access Key",
  516. StartLine: 9,
  517. EndLine: 9,
  518. StartColumn: 17,
  519. EndColumn: 36,
  520. Secret: "AKIALALEMEL33243OLIA",
  521. Match: "AKIALALEMEL33243OLIA",
  522. Line: "\n\taws_token := \"AKIALALEMEL33243OLIA\"",
  523. File: "foo/foo.go",
  524. Date: "2021-11-02T23:48:06Z",
  525. Commit: "491504d5a31946ce75e22554cc34203d8e5ff3ca",
  526. Author: "Zach Rice",
  527. Email: "zricer@protonmail.com",
  528. Message: "adding foo package with secret",
  529. Tags: []string{"key", "AWS"},
  530. Entropy: 3.0841837,
  531. Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9",
  532. Link: "https://github.com/gitleaks/test/blob/491504d5a31946ce75e22554cc34203d8e5ff3ca/foo/foo.go#L9",
  533. },
  534. },
  535. },
  536. {
  537. source: filepath.Join(repoBasePath, "small"),
  538. logOpts: "--all foo...",
  539. cfgName: "simple",
  540. expectedFindings: []report.Finding{
  541. {
  542. RuleID: "aws-access-key",
  543. Description: "AWS Access Key",
  544. StartLine: 9,
  545. EndLine: 9,
  546. StartColumn: 17,
  547. EndColumn: 36,
  548. Secret: "AKIALALEMEL33243OLIA",
  549. Line: "\n\taws_token := \"AKIALALEMEL33243OLIA\"",
  550. Match: "AKIALALEMEL33243OLIA",
  551. Date: "2021-11-02T23:48:06Z",
  552. File: "foo/foo.go",
  553. Commit: "491504d5a31946ce75e22554cc34203d8e5ff3ca",
  554. Author: "Zach Rice",
  555. Email: "zricer@protonmail.com",
  556. Message: "adding foo package with secret",
  557. Tags: []string{"key", "AWS"},
  558. Entropy: 3.0841837,
  559. Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9",
  560. Link: "https://github.com/gitleaks/test/blob/491504d5a31946ce75e22554cc34203d8e5ff3ca/foo/foo.go#L9",
  561. },
  562. },
  563. },
  564. }
  565. moveDotGit(t, "dotGit", ".git")
  566. defer moveDotGit(t, ".git", "dotGit")
  567. for _, tt := range tests {
  568. t.Run(strings.Join([]string{tt.cfgName, tt.logOpts}, "/"), func(t *testing.T) {
  569. viper.AddConfigPath(configPath)
  570. viper.SetConfigName("simple")
  571. viper.SetConfigType("toml")
  572. err := viper.ReadInConfig()
  573. require.NoError(t, err)
  574. var vc config.ViperConfig
  575. err = viper.Unmarshal(&vc)
  576. require.NoError(t, err)
  577. cfg, err := vc.Translate()
  578. require.NoError(t, err)
  579. detector := NewDetector(cfg)
  580. var ignorePath string
  581. info, err := os.Stat(tt.source)
  582. require.NoError(t, err)
  583. if info.IsDir() {
  584. ignorePath = filepath.Join(tt.source, ".gitleaksignore")
  585. } else {
  586. ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore")
  587. }
  588. err = detector.AddGitleaksIgnore(ignorePath)
  589. require.NoError(t, err)
  590. gitCmd, err := sources.NewGitLogCmd(tt.source, tt.logOpts)
  591. require.NoError(t, err)
  592. remote, err := NewRemoteInfo(scm.NoPlatform, tt.source)
  593. require.NoError(t, err)
  594. findings, err := detector.DetectGit(gitCmd, remote)
  595. require.NoError(t, err)
  596. for _, f := range findings {
  597. f.Match = "" // remove lines cause copying and pasting them has some wack formatting
  598. }
  599. assert.ElementsMatch(t, tt.expectedFindings, findings)
  600. })
  601. }
  602. }
  603. func TestFromGitStaged(t *testing.T) {
  604. tests := []struct {
  605. cfgName string
  606. source string
  607. logOpts string
  608. expectedFindings []report.Finding
  609. }{
  610. {
  611. source: filepath.Join(repoBasePath, "staged"),
  612. cfgName: "simple",
  613. expectedFindings: []report.Finding{
  614. {
  615. RuleID: "aws-access-key",
  616. Description: "AWS Access Key",
  617. StartLine: 7,
  618. EndLine: 7,
  619. StartColumn: 18,
  620. EndColumn: 37,
  621. Line: "\n\taws_token2 := \"AKIALALEMEL33243OLIA\" // this one is not",
  622. Match: "AKIALALEMEL33243OLIA",
  623. Secret: "AKIALALEMEL33243OLIA",
  624. File: "api/api.go",
  625. SymlinkFile: "",
  626. Commit: "",
  627. Entropy: 3.0841837,
  628. Author: "",
  629. Email: "",
  630. Date: "0001-01-01T00:00:00Z",
  631. Message: "",
  632. Tags: []string{
  633. "key",
  634. "AWS",
  635. },
  636. Fingerprint: "api/api.go:aws-access-key:7",
  637. Link: "",
  638. },
  639. },
  640. },
  641. }
  642. moveDotGit(t, "dotGit", ".git")
  643. defer moveDotGit(t, ".git", "dotGit")
  644. for _, tt := range tests {
  645. viper.AddConfigPath(configPath)
  646. viper.SetConfigName("simple")
  647. viper.SetConfigType("toml")
  648. err := viper.ReadInConfig()
  649. require.NoError(t, err)
  650. var vc config.ViperConfig
  651. err = viper.Unmarshal(&vc)
  652. require.NoError(t, err)
  653. cfg, err := vc.Translate()
  654. require.NoError(t, err)
  655. detector := NewDetector(cfg)
  656. err = detector.AddGitleaksIgnore(filepath.Join(tt.source, ".gitleaksignore"))
  657. require.NoError(t, err)
  658. gitCmd, err := sources.NewGitDiffCmd(tt.source, true)
  659. require.NoError(t, err)
  660. remote, err := NewRemoteInfo(scm.NoPlatform, tt.source)
  661. require.NoError(t, err)
  662. findings, err := detector.DetectGit(gitCmd, remote)
  663. require.NoError(t, err)
  664. for _, f := range findings {
  665. f.Match = "" // remove lines cause copying and pasting them has some wack formatting
  666. }
  667. assert.ElementsMatch(t, tt.expectedFindings, findings)
  668. }
  669. }
  670. // TestFromFiles tests the FromFiles function
  671. func TestFromFiles(t *testing.T) {
  672. tests := []struct {
  673. cfgName string
  674. source string
  675. expectedFindings []report.Finding
  676. }{
  677. {
  678. source: filepath.Join(repoBasePath, "nogit"),
  679. cfgName: "simple",
  680. expectedFindings: []report.Finding{
  681. {
  682. Description: "AWS Access Key",
  683. StartLine: 20,
  684. EndLine: 20,
  685. StartColumn: 16,
  686. EndColumn: 35,
  687. Match: "AKIALALEMEL33243OLIA",
  688. Secret: "AKIALALEMEL33243OLIA",
  689. Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
  690. File: "../testdata/repos/nogit/main.go",
  691. SymlinkFile: "",
  692. RuleID: "aws-access-key",
  693. Tags: []string{"key", "AWS"},
  694. Entropy: 3.0841837,
  695. Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20",
  696. },
  697. },
  698. },
  699. {
  700. source: filepath.Join(repoBasePath, "nogit", "main.go"),
  701. cfgName: "simple",
  702. expectedFindings: []report.Finding{
  703. {
  704. Description: "AWS Access Key",
  705. StartLine: 20,
  706. EndLine: 20,
  707. StartColumn: 16,
  708. EndColumn: 35,
  709. Match: "AKIALALEMEL33243OLIA",
  710. Secret: "AKIALALEMEL33243OLIA",
  711. Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
  712. File: "../testdata/repos/nogit/main.go",
  713. RuleID: "aws-access-key",
  714. Tags: []string{"key", "AWS"},
  715. Entropy: 3.0841837,
  716. Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20",
  717. },
  718. },
  719. },
  720. {
  721. source: filepath.Join(repoBasePath, "nogit", "api.go"),
  722. cfgName: "simple",
  723. expectedFindings: []report.Finding{},
  724. },
  725. {
  726. source: filepath.Join(repoBasePath, "nogit", ".env.prod"),
  727. cfgName: "generic",
  728. expectedFindings: []report.Finding{
  729. {
  730. Description: "Generic API Key",
  731. StartLine: 4,
  732. EndLine: 4,
  733. StartColumn: 5,
  734. EndColumn: 35,
  735. Match: "PASSWORD=8ae31cacf141669ddfb5da",
  736. Secret: "8ae31cacf141669ddfb5da",
  737. Line: "\nDB_PASSWORD=8ae31cacf141669ddfb5da",
  738. File: "../testdata/repos/nogit/.env.prod",
  739. RuleID: "generic-api-key",
  740. Tags: []string{},
  741. Entropy: 3.5383105,
  742. Fingerprint: "../testdata/repos/nogit/.env.prod:generic-api-key:4",
  743. },
  744. },
  745. },
  746. }
  747. for _, tt := range tests {
  748. viper.AddConfigPath(configPath)
  749. viper.SetConfigName(tt.cfgName)
  750. viper.SetConfigType("toml")
  751. err := viper.ReadInConfig()
  752. require.NoError(t, err)
  753. var vc config.ViperConfig
  754. err = viper.Unmarshal(&vc)
  755. require.NoError(t, err)
  756. cfg, _ := vc.Translate()
  757. detector := NewDetector(cfg)
  758. var ignorePath string
  759. info, err := os.Stat(tt.source)
  760. require.NoError(t, err)
  761. if info.IsDir() {
  762. ignorePath = filepath.Join(tt.source, ".gitleaksignore")
  763. } else {
  764. ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore")
  765. }
  766. err = detector.AddGitleaksIgnore(ignorePath)
  767. require.NoError(t, err)
  768. detector.FollowSymlinks = true
  769. paths, err := sources.DirectoryTargets(tt.source, detector.Sema, true, cfg.Allowlist.PathAllowed)
  770. require.NoError(t, err)
  771. findings, err := detector.DetectFiles(paths)
  772. require.NoError(t, err)
  773. assert.ElementsMatch(t, tt.expectedFindings, findings)
  774. }
  775. }
  776. func TestDetectWithSymlinks(t *testing.T) {
  777. tests := []struct {
  778. cfgName string
  779. source string
  780. expectedFindings []report.Finding
  781. }{
  782. {
  783. source: filepath.Join(repoBasePath, "symlinks/file_symlink"),
  784. cfgName: "simple",
  785. expectedFindings: []report.Finding{
  786. {
  787. Description: "Asymmetric Private Key",
  788. StartLine: 1,
  789. EndLine: 1,
  790. StartColumn: 1,
  791. EndColumn: 35,
  792. Match: "-----BEGIN OPENSSH PRIVATE KEY-----",
  793. Secret: "-----BEGIN OPENSSH PRIVATE KEY-----",
  794. Line: "-----BEGIN OPENSSH PRIVATE KEY-----",
  795. File: "../testdata/repos/symlinks/source_file/id_ed25519",
  796. SymlinkFile: "../testdata/repos/symlinks/file_symlink/symlinked_id_ed25519",
  797. RuleID: "apkey",
  798. Tags: []string{"key", "AsymmetricPrivateKey"},
  799. Entropy: 3.587164,
  800. Fingerprint: "../testdata/repos/symlinks/source_file/id_ed25519:apkey:1",
  801. },
  802. },
  803. },
  804. }
  805. for _, tt := range tests {
  806. viper.AddConfigPath(configPath)
  807. viper.SetConfigName("simple")
  808. viper.SetConfigType("toml")
  809. err := viper.ReadInConfig()
  810. require.NoError(t, err)
  811. var vc config.ViperConfig
  812. err = viper.Unmarshal(&vc)
  813. require.NoError(t, err)
  814. cfg, _ := vc.Translate()
  815. detector := NewDetector(cfg)
  816. detector.FollowSymlinks = true
  817. paths, err := sources.DirectoryTargets(tt.source, detector.Sema, true, cfg.Allowlist.PathAllowed)
  818. require.NoError(t, err)
  819. findings, err := detector.DetectFiles(paths)
  820. require.NoError(t, err)
  821. assert.ElementsMatch(t, tt.expectedFindings, findings)
  822. }
  823. }
  824. func TestDetectRuleAllowlist(t *testing.T) {
  825. cases := map[string]struct {
  826. fragment Fragment
  827. allowlist config.Allowlist
  828. expected []report.Finding
  829. }{
  830. // Commit / path
  831. "commit allowed": {
  832. fragment: Fragment{
  833. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  834. },
  835. allowlist: config.Allowlist{
  836. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  837. },
  838. },
  839. "path allowed": {
  840. fragment: Fragment{
  841. FilePath: "package-lock.json",
  842. },
  843. allowlist: config.Allowlist{
  844. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  845. },
  846. },
  847. "commit AND path allowed": {
  848. fragment: Fragment{
  849. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  850. FilePath: "package-lock.json",
  851. },
  852. allowlist: config.Allowlist{
  853. MatchCondition: config.AllowlistMatchAnd,
  854. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  855. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  856. },
  857. },
  858. "commit AND path NOT allowed": {
  859. fragment: Fragment{
  860. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  861. FilePath: "package.json",
  862. },
  863. allowlist: config.Allowlist{
  864. MatchCondition: config.AllowlistMatchAnd,
  865. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  866. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  867. },
  868. expected: []report.Finding{
  869. {
  870. StartColumn: 50,
  871. EndColumn: 60,
  872. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  873. Match: "Summer2024!",
  874. Secret: "Summer2024!",
  875. File: "package.json",
  876. Entropy: 3.095795154571533,
  877. RuleID: "test-rule",
  878. },
  879. },
  880. },
  881. "commit AND path NOT allowed - other conditions": {
  882. fragment: Fragment{
  883. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  884. FilePath: "package-lock.json",
  885. },
  886. allowlist: config.Allowlist{
  887. MatchCondition: config.AllowlistMatchAnd,
  888. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  889. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  890. Regexes: []*regexp.Regexp{regexp.MustCompile("password")},
  891. },
  892. expected: []report.Finding{
  893. {
  894. StartColumn: 50,
  895. EndColumn: 60,
  896. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  897. Match: "Summer2024!",
  898. Secret: "Summer2024!",
  899. File: "package-lock.json",
  900. Entropy: 3.095795154571533,
  901. RuleID: "test-rule",
  902. },
  903. },
  904. },
  905. "commit OR path allowed": {
  906. fragment: Fragment{
  907. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  908. FilePath: "package-lock.json",
  909. },
  910. allowlist: config.Allowlist{
  911. MatchCondition: config.AllowlistMatchOr,
  912. Commits: []string{"704178e7dca77ff143778a31cff0fc192d59b030"},
  913. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  914. },
  915. },
  916. // Regex / stopwords
  917. "regex allowed": {
  918. fragment: Fragment{},
  919. allowlist: config.Allowlist{
  920. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  921. },
  922. },
  923. "stopwords allowed": {
  924. fragment: Fragment{},
  925. allowlist: config.Allowlist{
  926. StopWords: []string{"summer"},
  927. },
  928. },
  929. "regex AND stopword allowed": {
  930. fragment: Fragment{},
  931. allowlist: config.Allowlist{
  932. MatchCondition: config.AllowlistMatchAnd,
  933. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  934. StopWords: []string{"2024"},
  935. },
  936. },
  937. "regex AND stopword allowed - other conditions": {
  938. fragment: Fragment{
  939. CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
  940. FilePath: "config.js",
  941. },
  942. allowlist: config.Allowlist{
  943. MatchCondition: config.AllowlistMatchAnd,
  944. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  945. Paths: []*regexp.Regexp{regexp.MustCompile(`config.js`)},
  946. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  947. StopWords: []string{"2024"},
  948. },
  949. },
  950. "regex AND stopword NOT allowed - non-git, other conditions": {
  951. fragment: Fragment{
  952. FilePath: "config.js",
  953. },
  954. allowlist: config.Allowlist{
  955. MatchCondition: config.AllowlistMatchAnd,
  956. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  957. Paths: []*regexp.Regexp{regexp.MustCompile(`config.js`)},
  958. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  959. StopWords: []string{"2024"},
  960. },
  961. expected: []report.Finding{
  962. {
  963. StartColumn: 50,
  964. EndColumn: 60,
  965. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  966. Match: "Summer2024!",
  967. Secret: "Summer2024!",
  968. File: "config.js",
  969. Entropy: 3.095795154571533,
  970. RuleID: "test-rule",
  971. },
  972. },
  973. },
  974. "regex AND stopword NOT allowed": {
  975. fragment: Fragment{},
  976. allowlist: config.Allowlist{
  977. MatchCondition: config.AllowlistMatchAnd,
  978. Regexes: []*regexp.Regexp{
  979. regexp.MustCompile(`(?i)winter.+`),
  980. },
  981. StopWords: []string{"2024"},
  982. },
  983. expected: []report.Finding{
  984. {
  985. StartColumn: 50,
  986. EndColumn: 60,
  987. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  988. Match: "Summer2024!",
  989. Secret: "Summer2024!",
  990. Entropy: 3.095795154571533,
  991. RuleID: "test-rule",
  992. },
  993. },
  994. },
  995. "regex AND stopword NOT allowed - other conditions": {
  996. fragment: Fragment{
  997. CommitSHA: "a060c9d2d5e90c992763f1bd4c3cd2a6f121241b",
  998. FilePath: "config.js",
  999. },
  1000. allowlist: config.Allowlist{
  1001. MatchCondition: config.AllowlistMatchAnd,
  1002. Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
  1003. Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
  1004. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)winter.+`)},
  1005. StopWords: []string{"2024"},
  1006. },
  1007. expected: []report.Finding{
  1008. {
  1009. StartColumn: 50,
  1010. EndColumn: 60,
  1011. Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
  1012. Match: "Summer2024!",
  1013. Secret: "Summer2024!",
  1014. File: "config.js",
  1015. Entropy: 3.095795154571533,
  1016. RuleID: "test-rule",
  1017. },
  1018. },
  1019. },
  1020. "regex OR stopword allowed": {
  1021. fragment: Fragment{},
  1022. allowlist: config.Allowlist{
  1023. MatchCondition: config.AllowlistMatchOr,
  1024. Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
  1025. StopWords: []string{"winter"},
  1026. },
  1027. },
  1028. }
  1029. raw := `let username = 'james@mail.com';
  1030. let password = 'Summer2024!';`
  1031. for name, tc := range cases {
  1032. t.Run(name, func(t *testing.T) {
  1033. rule := config.Rule{
  1034. RuleID: "test-rule",
  1035. Regex: regexp.MustCompile(`Summer2024!`),
  1036. Allowlists: []config.Allowlist{
  1037. tc.allowlist,
  1038. },
  1039. }
  1040. d, err := NewDetectorDefaultConfig()
  1041. require.NoError(t, err)
  1042. f := tc.fragment
  1043. f.Raw = raw
  1044. actual := d.detectRule(f, raw, rule, []EncodedSegment{})
  1045. if diff := cmp.Diff(tc.expected, actual); diff != "" {
  1046. t.Errorf("diff: (-want +got)\n%s", diff)
  1047. }
  1048. })
  1049. }
  1050. }
  1051. func moveDotGit(t *testing.T, from, to string) {
  1052. t.Helper()
  1053. repoDirs, err := os.ReadDir("../testdata/repos")
  1054. require.NoError(t, err)
  1055. for _, dir := range repoDirs {
  1056. if to == ".git" {
  1057. _, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), "dotGit"))
  1058. if os.IsNotExist(err) {
  1059. // dont want to delete the only copy of .git accidentally
  1060. continue
  1061. }
  1062. os.RemoveAll(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), ".git"))
  1063. }
  1064. if !dir.IsDir() {
  1065. continue
  1066. }
  1067. _, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from))
  1068. if os.IsNotExist(err) {
  1069. continue
  1070. }
  1071. err = os.Rename(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from),
  1072. fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), to))
  1073. require.NoError(t, err)
  1074. }
  1075. }