detect_test.go 28 KB

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