detect_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  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/zricethezav/gitleaks/v8/config"
  10. "github.com/zricethezav/gitleaks/v8/report"
  11. )
  12. const configPath = "../testdata/config/"
  13. const repoBasePath = "../testdata/repos/"
  14. func TestDetect(t *testing.T) {
  15. tests := []struct {
  16. cfgName string
  17. fragment Fragment
  18. expectedFindings []report.Finding
  19. wantError error
  20. }{
  21. {
  22. cfgName: "simple",
  23. fragment: Fragment{
  24. Raw: `awsToken := \"AKIALALEMEL33243OKIA\ // gitleaks:allow"`,
  25. FilePath: "tmp.go",
  26. },
  27. expectedFindings: []report.Finding{},
  28. },
  29. {
  30. cfgName: "simple",
  31. fragment: Fragment{
  32. Raw: `awsToken := \
  33. \"AKIALALEMEL33243OKIA\ // gitleaks:allow"
  34. `,
  35. FilePath: "tmp.go",
  36. },
  37. expectedFindings: []report.Finding{},
  38. },
  39. {
  40. cfgName: "simple",
  41. fragment: Fragment{
  42. Raw: `awsToken := \"AKIALALEMEL33243OKIA\"
  43. // gitleaks:allow"
  44. `,
  45. FilePath: "tmp.go",
  46. },
  47. expectedFindings: []report.Finding{
  48. {
  49. Description: "AWS Access Key",
  50. Secret: "AKIALALEMEL33243OKIA",
  51. Match: "AKIALALEMEL33243OKIA",
  52. File: "tmp.go",
  53. RuleID: "aws-access-key",
  54. Tags: []string{"key", "AWS"},
  55. StartLine: 0,
  56. EndLine: 0,
  57. StartColumn: 15,
  58. EndColumn: 34,
  59. Entropy: 3.1464393,
  60. },
  61. },
  62. },
  63. {
  64. cfgName: "escaped_character_group",
  65. fragment: Fragment{
  66. Raw: `pypi-AgEIcHlwaS5vcmcAAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAAB`,
  67. FilePath: "tmp.go",
  68. },
  69. expectedFindings: []report.Finding{
  70. {
  71. Description: "PyPI upload token",
  72. Secret: "pypi-AgEIcHlwaS5vcmcAAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAAB",
  73. Match: "pypi-AgEIcHlwaS5vcmcAAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAAB",
  74. File: "tmp.go",
  75. RuleID: "pypi-upload-token",
  76. Tags: []string{"key", "pypi"},
  77. StartLine: 1,
  78. EndLine: 1,
  79. StartColumn: 1,
  80. EndColumn: 86,
  81. Entropy: 1.9606875,
  82. },
  83. },
  84. },
  85. {
  86. cfgName: "simple",
  87. fragment: Fragment{
  88. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  89. FilePath: "tmp.go",
  90. },
  91. expectedFindings: []report.Finding{
  92. {
  93. Description: "AWS Access Key",
  94. Secret: "AKIALALEMEL33243OLIA",
  95. Match: "AKIALALEMEL33243OLIA",
  96. File: "tmp.go",
  97. RuleID: "aws-access-key",
  98. Tags: []string{"key", "AWS"},
  99. StartLine: 1,
  100. EndLine: 1,
  101. StartColumn: 15,
  102. EndColumn: 34,
  103. Entropy: 3.0841837,
  104. },
  105. },
  106. },
  107. {
  108. cfgName: "simple",
  109. fragment: Fragment{
  110. Raw: `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
  111. FilePath: "tmp.sh",
  112. },
  113. expectedFindings: []report.Finding{
  114. {
  115. Description: "Sidekiq Secret",
  116. Match: "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;",
  117. Secret: "cafebabe:deadbeef",
  118. File: "tmp.sh",
  119. RuleID: "sidekiq-secret",
  120. Tags: []string{},
  121. Entropy: 2.6098502,
  122. StartLine: 1,
  123. EndLine: 1,
  124. StartColumn: 8,
  125. EndColumn: 60,
  126. },
  127. },
  128. },
  129. {
  130. cfgName: "simple",
  131. fragment: Fragment{
  132. Raw: `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
  133. FilePath: "tmp.sh",
  134. },
  135. expectedFindings: []report.Finding{
  136. {
  137. Description: "Sidekiq Secret",
  138. Match: "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=\"cafebabe:deadbeef\"",
  139. Secret: "cafebabe:deadbeef",
  140. File: "tmp.sh",
  141. RuleID: "sidekiq-secret",
  142. Tags: []string{},
  143. Entropy: 2.6098502,
  144. StartLine: 1,
  145. EndLine: 1,
  146. StartColumn: 21,
  147. EndColumn: 74,
  148. },
  149. },
  150. },
  151. {
  152. cfgName: "simple",
  153. fragment: Fragment{
  154. Raw: `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
  155. FilePath: "tmp.sh",
  156. },
  157. expectedFindings: []report.Finding{
  158. {
  159. Description: "Sidekiq Sensitive URL",
  160. Match: "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:",
  161. Secret: "cafeb4b3:d3adb33f",
  162. File: "tmp.sh",
  163. RuleID: "sidekiq-sensitive-url",
  164. Tags: []string{},
  165. Entropy: 2.984234,
  166. StartLine: 1,
  167. EndLine: 1,
  168. StartColumn: 8,
  169. EndColumn: 58,
  170. },
  171. },
  172. },
  173. {
  174. cfgName: "allow_aws_re",
  175. fragment: Fragment{
  176. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  177. FilePath: "tmp.go",
  178. },
  179. expectedFindings: []report.Finding{},
  180. },
  181. {
  182. cfgName: "allow_path",
  183. fragment: Fragment{
  184. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  185. FilePath: "tmp.go",
  186. },
  187. expectedFindings: []report.Finding{},
  188. },
  189. {
  190. cfgName: "allow_commit",
  191. fragment: Fragment{
  192. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  193. FilePath: "tmp.go",
  194. CommitSHA: "allowthiscommit",
  195. },
  196. expectedFindings: []report.Finding{},
  197. },
  198. {
  199. cfgName: "entropy_group",
  200. fragment: Fragment{
  201. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  202. FilePath: "tmp.go",
  203. },
  204. expectedFindings: []report.Finding{
  205. {
  206. Description: "Discord API key",
  207. Match: "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
  208. Secret: "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
  209. File: "tmp.go",
  210. RuleID: "discord-api-key",
  211. Tags: []string{},
  212. Entropy: 3.7906237,
  213. StartLine: 1,
  214. EndLine: 1,
  215. StartColumn: 7,
  216. EndColumn: 93,
  217. },
  218. },
  219. },
  220. {
  221. cfgName: "generic_with_py_path",
  222. fragment: Fragment{
  223. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  224. FilePath: "tmp.go",
  225. },
  226. expectedFindings: []report.Finding{},
  227. },
  228. {
  229. cfgName: "generic_with_py_path",
  230. fragment: Fragment{
  231. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  232. FilePath: "tmp.py",
  233. },
  234. expectedFindings: []report.Finding{
  235. {
  236. Description: "Generic API Key",
  237. Match: "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
  238. Secret: "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
  239. File: "tmp.py",
  240. RuleID: "generic-api-key",
  241. Tags: []string{},
  242. Entropy: 3.7906237,
  243. StartLine: 1,
  244. EndLine: 1,
  245. StartColumn: 22,
  246. EndColumn: 93,
  247. },
  248. },
  249. },
  250. {
  251. cfgName: "path_only",
  252. fragment: Fragment{
  253. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  254. FilePath: "tmp.py",
  255. },
  256. expectedFindings: []report.Finding{
  257. {
  258. Description: "Python Files",
  259. Match: "file detected: tmp.py",
  260. File: "tmp.py",
  261. RuleID: "python-files-only",
  262. Tags: []string{},
  263. },
  264. },
  265. },
  266. {
  267. cfgName: "bad_entropy_group",
  268. fragment: Fragment{
  269. Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  270. FilePath: "tmp.go",
  271. },
  272. expectedFindings: []report.Finding{},
  273. wantError: fmt.Errorf("Discord API key invalid regex secret group 5, max regex secret group 3"),
  274. },
  275. {
  276. cfgName: "simple",
  277. fragment: Fragment{
  278. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  279. FilePath: filepath.Join(configPath, "simple.toml"),
  280. },
  281. expectedFindings: []report.Finding{},
  282. },
  283. {
  284. cfgName: "allow_global_aws_re",
  285. fragment: Fragment{
  286. Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
  287. FilePath: "tmp.go",
  288. },
  289. expectedFindings: []report.Finding{},
  290. },
  291. {
  292. cfgName: "generic_with_py_path",
  293. fragment: Fragment{
  294. Raw: `const Discord_Public_Key = "load2523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
  295. FilePath: "tmp.py",
  296. },
  297. expectedFindings: []report.Finding{},
  298. },
  299. }
  300. for _, tt := range tests {
  301. viper.Reset()
  302. viper.AddConfigPath(configPath)
  303. viper.SetConfigName(tt.cfgName)
  304. viper.SetConfigType("toml")
  305. err := viper.ReadInConfig()
  306. if err != nil {
  307. t.Error(err)
  308. }
  309. var vc config.ViperConfig
  310. err = viper.Unmarshal(&vc)
  311. if err != nil {
  312. t.Error(err)
  313. }
  314. cfg, err := vc.Translate()
  315. cfg.Path = filepath.Join(configPath, tt.cfgName+".toml")
  316. if tt.wantError != nil {
  317. if err == nil {
  318. t.Errorf("expected error")
  319. }
  320. assert.Equal(t, tt.wantError, err)
  321. }
  322. d := NewDetector(cfg)
  323. findings := d.Detect(tt.fragment)
  324. assert.ElementsMatch(t, tt.expectedFindings, findings)
  325. }
  326. }
  327. // TestFromGit tests the FromGit function
  328. func TestFromGit(t *testing.T) {
  329. tests := []struct {
  330. cfgName string
  331. source string
  332. logOpts string
  333. expectedFindings []report.Finding
  334. }{
  335. {
  336. source: filepath.Join(repoBasePath, "small"),
  337. cfgName: "simple",
  338. expectedFindings: []report.Finding{
  339. {
  340. Description: "AWS Access Key",
  341. StartLine: 20,
  342. EndLine: 20,
  343. StartColumn: 19,
  344. EndColumn: 38,
  345. Secret: "AKIALALEMEL33243OLIA",
  346. Match: "AKIALALEMEL33243OLIA",
  347. File: "main.go",
  348. Date: "2021-11-02T23:37:53Z",
  349. Commit: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587",
  350. Author: "Zachary Rice",
  351. Email: "zricer@protonmail.com",
  352. Message: "Accidentally add a secret",
  353. RuleID: "aws-access-key",
  354. Tags: []string{"key", "AWS"},
  355. Entropy: 3.0841837,
  356. Fingerprint: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587:main.go:aws-access-key:20",
  357. },
  358. {
  359. Description: "AWS Access Key",
  360. StartLine: 9,
  361. EndLine: 9,
  362. StartColumn: 17,
  363. EndColumn: 36,
  364. Secret: "AKIALALEMEL33243OLIA",
  365. Match: "AKIALALEMEL33243OLIA",
  366. File: "foo/foo.go",
  367. Date: "2021-11-02T23:48:06Z",
  368. Commit: "491504d5a31946ce75e22554cc34203d8e5ff3ca",
  369. Author: "Zach Rice",
  370. Email: "zricer@protonmail.com",
  371. Message: "adding foo package with secret",
  372. RuleID: "aws-access-key",
  373. Tags: []string{"key", "AWS"},
  374. Entropy: 3.0841837,
  375. Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9",
  376. },
  377. },
  378. },
  379. {
  380. source: filepath.Join(repoBasePath, "small"),
  381. logOpts: "--all foo...",
  382. cfgName: "simple",
  383. expectedFindings: []report.Finding{
  384. {
  385. Description: "AWS Access Key",
  386. StartLine: 9,
  387. EndLine: 9,
  388. StartColumn: 17,
  389. EndColumn: 36,
  390. Secret: "AKIALALEMEL33243OLIA",
  391. Match: "AKIALALEMEL33243OLIA",
  392. Date: "2021-11-02T23:48:06Z",
  393. File: "foo/foo.go",
  394. Commit: "491504d5a31946ce75e22554cc34203d8e5ff3ca",
  395. Author: "Zach Rice",
  396. Email: "zricer@protonmail.com",
  397. Message: "adding foo package with secret",
  398. RuleID: "aws-access-key",
  399. Tags: []string{"key", "AWS"},
  400. Entropy: 3.0841837,
  401. Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9",
  402. },
  403. },
  404. },
  405. }
  406. err := moveDotGit("dotGit", ".git")
  407. if err != nil {
  408. t.Fatal(err)
  409. }
  410. defer func() {
  411. if err := moveDotGit(".git", "dotGit"); err != nil {
  412. t.Error(err)
  413. }
  414. }()
  415. for _, tt := range tests {
  416. viper.AddConfigPath(configPath)
  417. viper.SetConfigName("simple")
  418. viper.SetConfigType("toml")
  419. err = viper.ReadInConfig()
  420. if err != nil {
  421. t.Error(err)
  422. }
  423. var vc config.ViperConfig
  424. err = viper.Unmarshal(&vc)
  425. if err != nil {
  426. t.Error(err)
  427. }
  428. cfg, err := vc.Translate()
  429. if err != nil {
  430. t.Error(err)
  431. }
  432. detector := NewDetector(cfg)
  433. findings, err := detector.DetectGit(tt.source, tt.logOpts, DetectType)
  434. if err != nil {
  435. t.Error(err)
  436. }
  437. for _, f := range findings {
  438. f.Match = "" // remove lines cause copying and pasting them has some wack formatting
  439. }
  440. assert.ElementsMatch(t, tt.expectedFindings, findings)
  441. }
  442. }
  443. // TestFromGit tests the FromGit function
  444. func TestFromFiles(t *testing.T) {
  445. tests := []struct {
  446. cfgName string
  447. source string
  448. expectedFindings []report.Finding
  449. }{
  450. {
  451. source: filepath.Join(repoBasePath, "nogit"),
  452. cfgName: "simple",
  453. expectedFindings: []report.Finding{
  454. {
  455. Description: "AWS Access Key",
  456. StartLine: 20,
  457. EndLine: 20,
  458. StartColumn: 16,
  459. EndColumn: 35,
  460. Match: "AKIALALEMEL33243OLIA",
  461. Secret: "AKIALALEMEL33243OLIA",
  462. File: "../testdata/repos/nogit/main.go",
  463. RuleID: "aws-access-key",
  464. Tags: []string{"key", "AWS"},
  465. Entropy: 3.0841837,
  466. },
  467. },
  468. },
  469. {
  470. source: filepath.Join(repoBasePath, "nogit", "main.go"),
  471. cfgName: "simple",
  472. expectedFindings: []report.Finding{
  473. {
  474. Description: "AWS Access Key",
  475. StartLine: 20,
  476. EndLine: 20,
  477. StartColumn: 16,
  478. EndColumn: 35,
  479. Match: "AKIALALEMEL33243OLIA",
  480. Secret: "AKIALALEMEL33243OLIA",
  481. File: "../testdata/repos/nogit/main.go",
  482. RuleID: "aws-access-key",
  483. Tags: []string{"key", "AWS"},
  484. Entropy: 3.0841837,
  485. },
  486. },
  487. },
  488. }
  489. for _, tt := range tests {
  490. viper.AddConfigPath(configPath)
  491. viper.SetConfigName("simple")
  492. viper.SetConfigType("toml")
  493. err := viper.ReadInConfig()
  494. if err != nil {
  495. t.Error(err)
  496. }
  497. var vc config.ViperConfig
  498. err = viper.Unmarshal(&vc)
  499. if err != nil {
  500. t.Error(err)
  501. }
  502. cfg, _ := vc.Translate()
  503. detector := NewDetector(cfg)
  504. findings, err := detector.DetectFiles(tt.source)
  505. if err != nil {
  506. t.Error(err)
  507. }
  508. assert.ElementsMatch(t, tt.expectedFindings, findings)
  509. }
  510. }
  511. func moveDotGit(from, to string) error {
  512. repoDirs, err := os.ReadDir("../testdata/repos")
  513. if err != nil {
  514. return err
  515. }
  516. for _, dir := range repoDirs {
  517. if to == ".git" {
  518. _, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), "dotGit"))
  519. if os.IsNotExist(err) {
  520. // dont want to delete the only copy of .git accidentally
  521. continue
  522. }
  523. os.RemoveAll(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), ".git"))
  524. }
  525. if !dir.IsDir() {
  526. continue
  527. }
  528. _, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from))
  529. if os.IsNotExist(err) {
  530. continue
  531. }
  532. err = os.Rename(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from),
  533. fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), to))
  534. if err != nil {
  535. return err
  536. }
  537. }
  538. return nil
  539. }