Explorar el Código

Introducing v8.0.0 changes (#701)

* Introducing v8.0.0 changes
Zachary Rice hace 4 años
padre
commit
93f292c3df
Se han modificado 100 ficheros con 3923 adiciones y 4502 borrados
  1. 49 0
      .github/workflows/release.yml
  2. 3 0
      .gitignore
  3. 22 0
      .goreleaser.yml
  4. 2 2
      Dockerfile
  5. 6 27
      Makefile
  6. 158 253
      README.md
  7. 88 0
      cmd/detect.go
  8. 67 0
      cmd/protect.go
  9. 112 0
      cmd/root.go
  10. 23 0
      cmd/version.go
  11. 18 42
      config/allowlist.go
  12. 93 0
      config/allowlist_test.go
  13. 79 291
      config/config.go
  14. 136 245
      config/config_test.go
  15. 380 107
      config/gitleaks.toml
  16. 35 173
      config/rule.go
  17. 60 0
      config/utils.go
  18. 105 0
      detect/detect.go
  19. 143 0
      detect/detect_test.go
  20. 74 0
      detect/files.go
  21. 80 0
      detect/files_test.go
  22. 89 0
      detect/git.go
  23. 158 0
      detect/git_test.go
  24. 38 0
      detect/location.go
  25. 60 0
      detect/location_test.go
  26. 0 195
      examples/leaky-repo.toml
  27. 0 14
      examples/pre-commit-config-example.yaml
  28. 0 19
      examples/pre-commit.example
  29. 0 17
      examples/regex_and_entropy_config.toml
  30. 0 13
      examples/simple_regex_and_allowlist_config.toml
  31. 85 0
      git/git.go
  32. 157 0
      git/git_test.go
  33. 10 10
      go.mod
  34. 572 82
      go.sum
  35. 8 54
      main.go
  36. 0 230
      options/options.go
  37. 0 1
      options/options_test.go
  38. 5 0
      report/constants.go
  39. 55 0
      report/csv.go
  40. 84 0
      report/csv_test.go
  41. 42 0
      report/finding.go
  42. 27 0
      report/finding_test.go
  43. 15 0
      report/json.go
  44. 89 0
      report/json_test.go
  45. 36 0
      report/report.go
  46. 111 0
      report/report_test.go
  47. 199 0
      report/sarif.go
  48. 105 0
      report/sarif_test.go
  49. 0 158
      scan/commit.go
  50. 0 90
      scan/commit_test.go
  51. 0 50
      scan/commits.go
  52. 0 102
      scan/commits_test.go
  53. 0 135
      scan/filesatcommit.go
  54. 0 81
      scan/filesatcommit_test.go
  55. 0 94
      scan/leak.go
  56. 0 152
      scan/nogit.go
  57. 0 81
      scan/nogit_test.go
  58. 0 75
      scan/parent.go
  59. 0 115
      scan/repo.go
  60. 0 88
      scan/repo_test.go
  61. 0 102
      scan/report.go
  62. 0 158
      scan/sarif.go
  63. 0 164
      scan/scan.go
  64. 0 192
      scan/scan_test.go
  65. 0 43
      scan/throttle.go
  66. 0 298
      scan/unstaged.go
  67. 0 111
      scan/unstaged_test.go
  68. 0 229
      scan/utils.go
  69. 22 0
      scripts/pre-commit.py
  70. 5 5
      testdata/config/allow_aws_re.toml
  71. 4 4
      testdata/config/allow_commit.toml
  72. 4 1
      testdata/config/allow_path.toml
  73. 8 0
      testdata/config/bad_entropy_group.toml
  74. 8 0
      testdata/config/entropy_group.toml
  75. 8 0
      testdata/config/generic.toml
  76. 9 0
      testdata/config/generic_with_py_path.toml
  77. 6 0
      testdata/config/path_only.toml
  78. 171 0
      testdata/config/simple.toml
  79. 0 3
      testdata/configs/allowlist_allow_all_repo_1.toml
  80. 0 3
      testdata/configs/allowlist_bad_docx_10.toml
  81. 0 15
      testdata/configs/allowlist_commit.toml
  82. 0 11
      testdata/configs/allowlist_docx.toml
  83. 0 11
      testdata/configs/allowlist_files.toml
  84. 0 8
      testdata/configs/aws_key_allowlist_files.toml
  85. 0 7
      testdata/configs/aws_key_allowlist_python_files.toml
  86. 0 7
      testdata/configs/aws_key_aws_allowlisted.toml
  87. 0 14
      testdata/configs/aws_key_file_regex.toml
  88. 0 10
      testdata/configs/aws_key_global_allowlist_file.toml
  89. 0 10
      testdata/configs/aws_key_global_allowlist_path.toml
  90. 0 9
      testdata/configs/aws_key_local_owner_allowlist_repo.toml
  91. 0 10
      testdata/configs/aws_key_with_report_groups.toml
  92. 0 9
      testdata/configs/bad_aws_key.toml
  93. 0 13
      testdata/configs/bad_aws_key_file_regex.toml
  94. 0 8
      testdata/configs/bad_aws_key_global_allowlist_file.toml
  95. 0 8
      testdata/configs/bad_entropy_1.toml
  96. 0 8
      testdata/configs/bad_entropy_2.toml
  97. 0 8
      testdata/configs/bad_entropy_3.toml
  98. 0 9
      testdata/configs/bad_entropy_4.toml
  99. 0 9
      testdata/configs/bad_entropy_5.toml
  100. 0 9
      testdata/configs/bad_entropy_6.toml

+ 49 - 0
.github/workflows/release.yml

@@ -0,0 +1,49 @@
+name: Create and publish a Docker image
+
+on:
+  release:
+    types: [published]
+
+env:
+  REGISTRY: ghcr.io
+  IMAGE_NAME: ${{ github.repository }}
+
+jobs:
+  build-and-push-image:
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      packages: write
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v2
+
+      - name: Log in to Docker Hub
+        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
+        with:
+          username: ${{ secrets.DOCKER_USERNAME }}
+          password: ${{ secrets.DOCKER_PASSWORD }}
+
+      - name: Log in to the Container registry
+        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
+        with:
+          registry: ${{ env.REGISTRY }}
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Extract metadata (tags, labels) for Docker
+        id: meta
+        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
+        with:
+          images: |
+            zricethezav/gitleaks
+            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+
+      - name: Build and push Docker image
+        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
+        with:
+          context: .
+          push: true
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}

+ 3 - 0
.gitignore

@@ -9,6 +9,9 @@
 *.got
 gitleaks
 build
+.gitleaks.toml
 
 # Test binary
 *.out
+
+dist/

+ 22 - 0
.goreleaser.yml

@@ -0,0 +1,22 @@
+project_name: gitleaks
+
+builds:
+  - main: main.go
+    binary: gitleaks
+    goos:
+      - darwin
+      - linux
+      - windows
+    goarch:
+      - amd64
+      - arm64
+archives:
+  - builds: [gitleaks]
+    format_overrides:
+      - goos: windows
+        format: zip
+    replacements:
+      amd64: x64
+      386: x32
+release:
+  prerelease: true

+ 2 - 2
Dockerfile

@@ -1,8 +1,8 @@
 FROM golang:1.17 AS build
 WORKDIR /go/src/github.com/zricethezav/gitleaks
-ARG ldflags
 COPY . .
-RUN GO111MODULE=on CGO_ENABLED=0 go build -o bin/gitleaks -ldflags "-X="${ldflags} *.go 
+RUN VERSION=$(git fetch --tags https://github.com/zricethezav/gitleaks.git && git tag | sort -V | tail -1) && \
+GO111MODULE=on CGO_ENABLED=0 go build -o bin/gitleaks -ldflags "-X="github.com/zricethezav/gitleaks/v8/cmd.Version=${VERSION}
 
 FROM alpine:3.14.2
 RUN adduser -D gitleaks && \

+ 6 - 27
Makefile

@@ -1,9 +1,8 @@
-.PHONY: test test-cover build release-builds
+.PHONY: test test-cover
 
-VERSION := `git fetch --tags && git tag | sort -V | tail -1`
 PKG=github.com/zricethezav/gitleaks
-LDFLAGS=-ldflags "-X=github.com/zricethezav/gitleaks/v7/version.Version=$(VERSION)"
-_LDFLAGS="github.com/zricethezav/gitleaks/v7/version.Version=$(VERSION)"
+VERSION := `git fetch --tags && git tag | sort -V | tail -1`
+LDFLAGS=-ldflags "-X=github.com/zricethezav/gitleaks/v8/cmd.Version=$(VERSION)"
 COVER=--cover --coverprofile=cover.out
 
 test-cover:
@@ -14,34 +13,14 @@ format:
 	go fmt ./...
 
 test: format
-	go get golang.org/x/lint/golint
 	go vet ./...
-	golint ./...
 	go test ./... --race $(PKG) -v
 
 build: format
-	golint ./...
 	go vet ./...
 	go mod tidy
 	go build $(LDFLAGS)
 
-release-builds:
-	rm -rf build
-	mkdir build
-	env GOOS="windows" GOARCH="amd64" go build -o "build/gitleaks-windows-amd64.exe" $(LDFLAGS)
-	env GOOS="windows" GOARCH="386" go build -o "build/gitleaks-windows-386.exe" $(LDFLAGS)
-	env GOOS="linux" GOARCH="amd64" go build -o "build/gitleaks-linux-amd64" $(LDFLAGS)
-	env GOOS="linux" GOARCH="arm" go build -o "build/gitleaks-linux-arm" $(LDFLAGS)
-	env GOOS="linux" GOARCH="mips" go build -o "build/gitleaks-linux-mips" $(LDFLAGS)
-	env GOOS="linux" GOARCH="mips" go build -o "build/gitleaks-linux-mips" $(LDFLAGS)
-	env GOOS="darwin" GOARCH="amd64" go build -o "build/gitleaks-darwin-amd64" $(LDFLAGS)
-	env GOOS="darwin" GOARCH="arm64" go build -o "build/gitleaks-darwin-arm64" $(LDFLAGS)
-
-deploy:
-	@echo "$(DOCKER_PASSWORD)" | docker login -u "$(DOCKER_USERNAME)" --password-stdin
-	docker build --build-arg ldflags=$(_LDFLAGS) -f Dockerfile -t zricethezav/gitleaks:latest -t zricethezav/gitleaks:$(VERSION) .
-	echo "Pushing zricethezav/gitleaks:$(VERSION) and zricethezav/gitleaks:latest"
-	docker push zricethezav/gitleaks
-
-dockerbuild:
-	docker build --build-arg ldflags=$(_LDFLAGS) -f Dockerfile -t zricethezav/gitleaks:latest -t zricethezav/gitleaks:$(VERSION) .
+clean:
+	find . -type f -name '*.got.*' -delete
+	find . -type f -name '*.out' -delete

+ 158 - 253
README.md

@@ -4,7 +4,7 @@
 │╲
 │ ○
 ○ ░
-░    gitleaks 
+░    gitleaks
 ```
 
 
@@ -22,289 +22,194 @@
   </p>
 </p>
 
-Gitleaks is a SAST tool for detecting hardcoded secrets like passwords, api keys, and tokens in git repos. Gitleaks is an **easy-to-use, all-in-one solution** for finding secrets, past or present, in your code.
+Gitleaks is a SAST tool for detecting hardcoded secrets like passwords, api keys, and tokens in git repos. Gitleaks is an **easy-to-use, all-in-one solution** for detecting secrets, past or present, in your code.
 
-### [Introduction Video](https://www.youtube.com/watch?v=VUq2eII20S4)
-
-
-### Features:
-- Scan for [commited](https://github.com/zricethezav/gitleaks#Scanning) secrets
-- Scan for [unstaged](https://github.com/zricethezav/gitleaks#scan-unstaged-changes) secrets to shift security left
-- Scan [directories and files](https://github.com/zricethezav/gitleaks#scan-local-directory)
-- Run [Gitleaks Action](https://github.com/marketplace/actions/gitleaks) in your CI/CD pipeline
-- [Custom rules](https://github.com/zricethezav/gitleaks#configuration) via toml configuration
-- Increased performance using [go-git](https://github.com/go-git/go-git)
-- json, sarif, and csv reporting
-- Private repo scans using key or password based authentication
-
-### Installation
+## Getting Started
 Gitleaks can be installed using Homebrew, Docker, or Go. Gitleaks is also available in binary form for many popular platforms and OS types on the [releases page](https://github.com/zricethezav/gitleaks/releases). In addition, Gitleaks can be implemented as a pre-commit hook directly in your repo.
 
-##### MacOS
+### MacOS
 
 ```bash
 brew install gitleaks
 ```
 
-##### Docker
-
-Building the image after cloning the repo:
-```bash
-make dockerbuild
-```
-
+### Docker
 Using the image from DockerHub:
 ```bash
-# To just pull the image
 docker pull zricethezav/gitleaks:latest
-# To run it from your cloned repo
-cd to/your/repo/
-docker run -v ${PWD}:/my-repo zricethezav/gitleaks:latest --path="/my-repo" [OPTIONS]
+docker run -v ${path_to_host_folder_to_scan}:/path zricethezav/gitleaks:latest [COMMAND] --source="/path" [OPTIONS]
 ```
 
-##### Go
-Go 1.16+ required.
+### From Source
+1. Download and install Go from https://golang.org/dl/
+2. Clone the repo
 ```bash
-GO111MODULE=on go get github.com/zricethezav/gitleaks/v7
+git clone https://github.com/zricethezav/gitleaks.git
 ```
-
-##### From bin:
-
+3. Build the binary
 ```bash
-GITLEAKS_VERSION=$(curl -s https://api.github.com/repos/zricethezav/gitleaks/releases/latest |  grep -oP '"tag_name": "\K(.*)(?=")') && wget https://github.com/zricethezav/gitleaks/releases/download/$GITLEAKS_VERSION/gitleaks-linux-amd64
-mv gitleaks-linux-amd64 gitleaks
-chmod +x gitleaks
-sudo mv gitleaks /usr/local/bin/
+cd gitleaks
+make build
 ```
 
-##### As a pre-commit hook
 
-See [pre-commit](https://github.com/pre-commit/pre-commit) for instructions.
-
-Sample `.pre-commit-config.yaml`
-
-```yaml
-# The revision doesn't get updated manually
-# check this https://github.com/zricethezav/gitleaks/releases
-# to see if there are newer versions
-- repo: https://github.com/zricethezav/gitleaks
-  rev: v7.6.1
-  hooks:
-    - id: gitleaks
-```
-
-### Usage and Options
+## Usage
 ```
 Usage:
-  gitleaks [OPTIONS]
-
-Application Options:
-  -v, --verbose             Show verbose output from scan
-  -q, --quiet               Sets log level to error and only output leaks, one json object per line
-  -r, --repo-url=           Repository URL
-  -p, --path=               Path to directory (repo if contains .git) or file
-  -c, --config-path=        Path to config
-      --repo-config-path=   Path to gitleaks config relative to repo root
-      --clone-path=         Path to clone repo to disk
-      --version             Version number
-      --username=           Username for git repo
-      --password=           Password for git repo
-      --access-token=       Access token for git repo
-      --threads=            Maximum number of threads gitleaks spawns
-      --ssh-key=            Path to ssh key used for auth
-      --unstaged            Run gitleaks on unstaged code
-      --branch=             Branch to scan
-      --redact              Redact secrets from log messages and leaks
-      --debug               Log debug messages
-      --no-git              Treat git repos as plain directories and scan those files
-      --leaks-exit-code=    Exit code when leaks have been encountered (default: 1)
-      --append-repo-config  Append the provided or default config with the repo config.
-      --additional-config=  Path to an additional gitleaks config to append with an existing config. Can be used with --append-repo-config to append up to three configurations
-  -o, --report=             Report output path
-  -f, --format=             json, csv, sarif (default: json)
-      --files-at-commit=    Sha of commit to scan all files at commit
-      --commit=             Sha of commit to scan or "latest" to scan the last commit of the repository
-      --commits=            Comma separated list of a commits to scan
-      --commits-file=       Path to file of line separated list of commits to scan
-      --commit-since=       Scan commits more recent than a specific date. Ex: '2006-01-02' or '2006-01-02T15:04:05-0700' format.
-      --commit-until=       Scan commits older than a specific date. Ex: '2006-01-02' or '2006-01-02T15:04:05-0700' format.
-      --depth=              Number of commits to scan
-
-Help Options:
-  -h, --help                Show this help message
-```
-
-
-### [Scanning](https://www.youtube.com/watch?v=WUzpRL8mKCk)
-
-#### Basic repo-url scan:
-This scans the entire history of tests/secrets and logs leaks as they are encountered `-v`/`--verbose` being set.
-```bash
-gitleaks --repo-url=https://github.com/my-insecure/repo -v
-```
-
-#### Basic repo-url scan output to a report:
-If you want the report in sarif or csv you can set the `-f/--format` option
-```bash
-gitleaks --repo-url=https://github.com/my-insecure/repo -v --report=my-report.json
-```
-
-#### Scan specific commit:
-```bash
-gitleaks --repo-url=https://github.com/my-insecure/repo --commit=commit-sha -v
-```
-
-#### Scan local repo:
-```bash
-gitleaks --path=path/to/local/repo -v
-```
-
-#### Scan repos contained in a parent directory:
-If you have `repo1`, `repo2`, `repo3` all under `path/to/local`, gitleaks will discover and scan those repos.
-```bash
-gitleaks --path=path/to/local/ -v
-```
-
-#### Scan local directory:
-If you want to scan the current contents of a repo, ignoring git alltogether. You can use the `--no-git` option to do this.
-```bash
-gitleaks --path=path/to/local/repo -v --no-git
-```
-
-#### Scan a file:
-Or if you want to scan a single file using gitleaks rules. You can do this by specifying the file in `--path` and including the `--no-git` option.
-```bash
-gitleaks --path=path/to/local/repo/main.go -v --no-git
-```
-
-#### Scan unstaged changes:
-If you have unstaged changes that are currently at the root of the repo, you can run `gitleaks` with no `--path` or `--repo-url` specified which will run a scan on your uncommitted changes.  
-Or if you want to specify a path, you can run:
-```bash
-gitleaks --path=path/to/local/repo -v --unstaged
-```
-
-
-### Configuration
-Provide your own gitleaks configurations with `--config-path` or `--repo-config-path`. `--config-path` loads a local gitleaks configuration whereas `--repo-config-path` will load a configuration present just in the repo you want to scan. For example, `gitleaks --repo-config-path=".github/gitleaks.config"`.
-The default configuration Gitleaks uses is located [here](https://github.com/zricethezav/gitleaks/blob/master/config/gitleaks.toml). More configuration examples can be seen [here](https://github.com/zricethezav/gitleaks/tree/master/examples). Configuration files will contain a few different toml tables. Further explanation is provided below.
-
-### Rules summary
-
-The rules are written in [TOML](https://github.com/toml-lang/toml) as defined in [TomlLoader struct](https://github.com/zricethezav/gitleaks/blob/master/config/config.go#L57-L87), and can be summarized as:
-
-```toml
-[[rules]]
-  description = "a string describing one of many rules in this config"
-  regex = '''one-go-style-regex-for-this-rule'''
-  file = '''a-file-name-regex'''
-  path = '''a-file-path-regex'''
-  tags = ["tag","another tag"]
-  [[rules.entropies]] # note these are strings, not floats
-    Min = "3.5"
-    Max = "4.5"
-    Group = "1"
-  [rules.allowlist]
-    description = "a string"
-    files = ['''one-file-name-regex''']
-    commits = [ "commit-A", "commit-B"]
-    paths = ['''one-file-path-regex''']
-    regexes = ['''one-regex-within-the-already-matched-regex''']
-
-[allowlist]
-  description = "a description string for a global allowlist config"
-  commits = [ "commit-A", "commit-B"]
-  files = [ '''file-regex-a''', '''file-regex-b''']
-  paths = [ '''path-regex-a''', '''path-regex-b''']
-  repos = [ '''repo-regex-a''', '''repo-regex-b''']
-  regexes = ['''one-regex-within-the-already-matched-regex''']
-```
+  gitleaks [command]
 
-Regular expressions are _NOT_ the full Perl set, so there are no look-aheads or look-behinds.
+Available Commands:
+  completion  generate the autocompletion script for the specified shell
+  detect      Detect secrets in code
+  help        Help about any command
+  protect     Protect secrets in code
+  version     Display gitleaks version
+
+Flags:
+  -c, --config string          config file path
+                               order of precedence:
+                               1. --config/-c
+                               2. (--source/-s)/.gitleaks.toml
+                               if --config/-c is not set and no .gitleaks.toml/gitleaks.toml present
+                               then .gitleaks.toml will be written to (--source/-s)/.gitleaks.toml for future use
+      --exit-code string       exit code when leaks have been encountered (default: 1)
+  -h, --help                   help for gitleaks
+  -l, --log-level string       log level (debug, info, warn, error, fatal) (default "info")
+      --redact                 redact secrets from logs and stdout
+  -f, --report-format string   output format (json, csv, sarif)
+  -r, --report-path string     report file
+  -s, --source string          path to source (git repo, directory, file)
+  -v, --verbose                show verbose output from scan
+
+Use "gitleaks [command] --help" for more information about a command.
+```
+
+### Commands
+There are two commands you will use to detect secrets; `detect` and `protect`.
+#### Detect
+The `detect` command is used to scan repos, directories, and files.  This comand can be used on developer machines and in CI environments. 
+
+When running `detect` on a git repository, gitleaks will parse the output of a `git log -p` command (you can see how this executed 
+[here](https://github.com/zricethezav/gitleaks/blob/7240e16769b92d2a1b137c17d6bf9d55a8562899/git/git.go#L17-L25)). 
+[`git log -p` generates patches](https://git-scm.com/docs/git-log#_generating_patch_text_with_p) which gitleaks will use to detect secrets. 
+You can configure what commits `git log` will range over by using the `--log-opts` flag. `--log-opts` accepts any option for `git log -p`. 
+For example, if you wanted to run gitleaks on a range of commits you could use the following command: `gitleaks --source . --log-opts="--all commitA..commitB"`. 
+See the `git log` [documentation](https://git-scm.com/docs/git-log) for more information.
+
+You can scan files and directories by using the `--no-git` option.
+
+#### Protect
+The `protect` command is used to uncommitted changes in a git repo. This command should be used on developer machines in accordance with 
+[shifting left on security](https://cloud.google.com/architecture/devops/devops-tech-shifting-left-on-security). 
+When running `detect` on a git repository, gitleaks will parse the output of a `git diff` command (you can see how this executed 
+[here](https://github.com/zricethezav/gitleaks/blob/7240e16769b92d2a1b137c17d6bf9d55a8562899/git/git.go#L48-L49)). You can set the 
+`--staged` flag to check for changes in commits that have been `git add`ed. The `--staged` flag should be used when running Gitleaks
+as a pre-commit.
+
+**NOTE**: the `protect` command can only be used on git repos, running `protect` on files or directories will result in an error message.
+
+### Verify Findings
+You can verify a finding found by gitleaks using a `git log` command.
+Example output:
+```
+{
+        "Description": "AWS",
+        "StartLine": 37,
+        "EndLine": 37,
+        "StartColumn": 19,
+        "EndColumn": 38,
+        "Context": "\t\t\"aws_secret= \\\"AKIAIMNOJVGFDXXXE4OA\\\"\":          true,",
+        "Secret": "AKIAIMNOJVGFDXXXE4OA",
+        "File": "checks_test.go",
+        "Commit": "ec2fc9d6cb0954fb3b57201cf6133c48d8ca0d29",
+        "Entropy": 0,
+        "Author": "zricethezav",
+        "Email": "thisispublicanyways@gmail.com",
+        "Date": "2018-01-28 17:39:00 -0500 -0500",
+        "Message": "[update] entropy check",
+        "Tags": [],
+        "RuleID": "aws-access-token"
+}
+
+```
+We can use the following format to verify the leak:
+
+```
+git log -L {StartLine,EndLine}:{File} {Commit}
+```
+So in this example it would look like:
+```
+git log -L 37,37:checks_test.go ec2fc9d6cb0954fb3b57201cf6133c48d8ca0d29
+```
+Which gives us:
+
+```
+commit ec2fc9d6cb0954fb3b57201cf6133c48d8ca0d29
+Author: zricethezav <thisispublicanyways@gmail.com>
+Date:   Sun Jan 28 17:39:00 2018 -0500
+
+    [update] entropy check
+
+diff --git a/checks_test.go b/checks_test.go
+--- a/checks_test.go
++++ b/checks_test.go
+@@ -28,0 +37,1 @@
++               "aws_secret= \"AKIAIMNOJVGFDXXXE4OA\"":          true,
+
+```
 
+## Pre-Commit hook
+You can run Gitleaks as a pre-commit hook by copying the example `pre-commit.py` script into 
+your `.git/hooks/` directory.
 
-### Examples
-#### Example 1
-The first and most commonly edited array of tables is `[[rules]]`. This is where you can define your own custom rules for Gitleaks to use while scanning repos. Example keys/values within the `[[rules]]` table:
-```toml
-[[rules]]
-  description = "generic secret regex"
-  regex = '''secret(.{0,20})([0-9a-zA-Z-._{}$\/\+=]{20,120})'''
-  tags = ["secret", "example"]
-```
-#### Example 2
-We can also **combine** regular expressions AND entropy:
-```toml
-[[rules]]
-  description = "entropy and regex example"
-  regex = '''secret(.{0,20})['|"]([0-9a-zA-Z-._{}$\/\+=]{20,120})['|"]'''
-  [[rules.Entropies]]
-    Min = "4.5"
-    Max = "4.7"
-```
-Translating this rule to English, this rule states: "if we encounter a line of code that matches *regex* AND the line falls within the bounds of a [Shannon entropy](https://en.wikipedia.org/wiki/Entropy_(information_theory)) of 4.5 to 4.7, then the line must be a leak"
-
-#### Example 3
-Let's compare two lines of code:
-```
-aws_secret='ABCDEF+c2L7yXeGvUyrPgYsDnWRRC1AYEXAMPLE'
-```
-and
-```
-aws_secret=os.getenv('AWS_SECRET_ACCESS_KEY')
-```
-The first line of code is an example of a hardcoded secret being assigned to the variable `aws_secret`. The second line of code is an example of a secret being assigned via env variables to `aws_secret`. Both would be caught by the rule defined in *example 2* but only the first line is actually a leak. Let's define a new rule that will capture only the first line of code. We can do this by combining regular expression **groups** and entropy.
+## Configuration
+Gitleaks offers a configuration format you can follow to write your own secret detection rules:
 ```toml
-[[rules]]
-  description = "entropy and regex example"
-  regex = '''secret(.{0,20})['|"]([0-9a-zA-Z-._{}$\/\+=]{20,120})['|"]'''
-  [[rules.Entropies]]
-    Min = "4.5"
-    Max = "4.7"
-    Group = "2"
-```
-Notice how we added `Group = "2"` to this rule. We can translate this rule to English: "if we encounter a line of code that matches regex AND the entropy of the *second regex group* falls within the bounds of a [Shannon entropy](https://en.wikipedia.org/wiki/Entropy_(information_theory)) of 4.5 to 4.7, then the line must be a leak"
+# Title for the gitleaks configuration file. 
+title = "Gitleaks title"
 
-### Example 4: Using allowlist regex
-
-The proper Perl regex for AWS secret keys is
-`(?<![A-Za-z0-9\\+])[A-Za-z0-9\\+=]{40}(?![A-Za-z0-9\\+=])`
-but the Go library doesn't do lookahead/lookbehind, so
-we'll look for 40 base64 characters, then allowlist
-if they're embedded in a string of 41 base64 characters, that is,
-without any delimiters. This will make a false negative for, say:
-```
-    foo=+awsSecretAccessKeyisBase64=40characters
-```
-So you can use the following to effectively create the proper Perl regex:
-```toml
+# An array of tables that contain information that define instructions
+# on how to detect secrets 
 [[rules]]
-  description = "AWS secret key regardless of labeling"
-  regex = '''.?[A-Za-z0-9\\+=]{40}.?'''
-  [rules.allowlist]
-    description = "41 base64 characters is not an AWS secret key"
-    regexes = ['''[A-Za-z0-9\\+=]{41}''']
+# Unique identifier for this rule
+id = "awesome-rule-1"
+# Short human readable description of the rule.
+description = "awsome rule 1" 
+# Golang regular expression used to detect secrets. Note Golang's regex engine
+# does not support lookaheads.
+regex = '''one-go-style-regex-for-this-rule''' 
+# Golang regular expression used to match paths. This can be used as a standalone rule or it can be used
+# in conjunction with a valid `regex` entry.
+path = '''a-file-path-regex'''
+# Array of strings used for metadata and reporting purposes.
+tags = ["tag","another tag"]
+# Int used to check shannon entropy of a specific group in a regex match. 
+entropyGroup = 3
+# Float representing the minimum shannon entropy a regex group must have to be considered a secret. 
+entropy = 3.5
+# You can include an allowlist table for a single rule to reduce false positives or ignore commits
+# with known/rotated secrets
+[rules.allowlist]
+description = "ignore commit A"
+commits = [ "commit-A", "commit-B"]
+paths = ['''one-file-path-regex''']
+regexes = ['''one-regex-within-the-already-matched-regex''']
+
+# This is a global allowlist which has a higher order of precendence than rule-specific allowlists.
+# If a commit listed in the `commits` field below is encountered then that commit will be skipped and no 
+# secrets will be detected for said commit. The same logic applies for regexes and paths.
+[allowlist]
+description = "ignore commit A"
+commits = [ "commit-A", "commit-B"]
+paths = ['''one-file-path-regex''']
+regexes = ['''one-regex-within-the-already-matched-regex''']
 ```
+Refer to the default [gitleaks config](https://github.com/zricethezav/gitleaks/blob/v8/config/gitleaks.toml) for examples and advice on writing regular expressions for secret detection.
 
 
-### Exit Codes
-You can always set the exit code when leaks are encountered with the `--leaks-exit-code` flag. Default exit codes below:
+## Exit Codes
+You can always set the exit code when leaks are encountered with the --exit-code flag. Default exit codes below:
 ```
 0 - no leaks present
 1 - leaks or error encountered
 ```
-
-###  Sponsors ❤️
-#### Organization Sponsors
-Sir, ehm, this is uhh... this is empty [😭](https://www.youtube.com/watch?v=w1o4O2SfQ5g)
-
-#### Individual Sponsors
-These users are [sponsors](https://github.com/sponsors/zricethezav) of gitleaks:
-
-- [Adam Shannon](https://github.com/adamdecaf)
-- [ProjectDiscovery](https://projectdiscovery.io/#/)
-- [Ben "Ihavespoons"](https://github.com/ihavespoons)
-- [Henry Sachs](https://github.com/henrysachs)
-
-#### Logo Attribution
-The Gitleaks logo uses the Git Logo created by [Jason Long](https://twitter.com/jasonlong) and is licensed under the [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/).

+ 88 - 0
cmd/detect.go

@@ -0,0 +1,88 @@
+package cmd
+
+import (
+	"os"
+	"time"
+
+	"github.com/rs/zerolog/log"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+
+	"github.com/zricethezav/gitleaks/v8/config"
+	"github.com/zricethezav/gitleaks/v8/detect"
+	"github.com/zricethezav/gitleaks/v8/git"
+	"github.com/zricethezav/gitleaks/v8/report"
+)
+
+func init() {
+	rootCmd.AddCommand(detectCmd)
+	detectCmd.Flags().String("log-opts", "", "git log options")
+	detectCmd.Flags().Bool("no-git", false, "treat git repo as a regular directory and scan those files, --log-opts has no effect on the scan when --no-git is set")
+}
+
+var detectCmd = &cobra.Command{
+	Use:   "detect",
+	Short: "detect secrets in code",
+	Run:   runDetect,
+}
+
+func runDetect(cmd *cobra.Command, args []string) {
+	initConfig()
+	var (
+		vc       config.ViperConfig
+		findings []*report.Finding
+		err      error
+	)
+
+	viper.Unmarshal(&vc)
+	cfg, err := vc.Translate()
+	if err != nil {
+		log.Fatal().Err(err).Msg("Failed to load config")
+	}
+
+	source, _ := cmd.Flags().GetString("source")
+	logOpts, _ := cmd.Flags().GetString("log-opts")
+	verbose, _ := cmd.Flags().GetBool("verbose")
+	redact, _ := cmd.Flags().GetBool("redact")
+	noGit, _ := cmd.Flags().GetBool("no-git")
+	exitCode, _ := cmd.Flags().GetInt("exit-code")
+	start := time.Now()
+
+	if noGit {
+		if logOpts != "" {
+			log.Fatal().Err(err).Msg("--log-opts cannot be used with --no-git")
+		}
+		findings, err = detect.FromFiles(source, cfg, detect.Options{
+			Verbose: verbose,
+			Redact:  redact,
+		})
+		if err != nil {
+			log.Fatal().Err(err).Msg("Failed to scan files")
+		}
+	} else {
+		files, err := git.GitLog(source, logOpts)
+		if err != nil {
+			log.Fatal().Err(err).Msg("Failed to get git log")
+		}
+
+		findings = detect.FromGit(files, cfg, detect.Options{Verbose: verbose, Redact: redact})
+	}
+
+	if len(findings) != 0 {
+		log.Warn().Msgf("leaks found: %d", len(findings))
+	} else {
+		log.Info().Msg("no leaks found")
+	}
+
+	log.Info().Msgf("scan completed in %s", time.Since(start))
+
+	reportPath, _ := cmd.Flags().GetString("report-path")
+	ext, _ := cmd.Flags().GetString("report-format")
+	if reportPath != "" {
+		report.Write(findings, cfg, ext, reportPath)
+	}
+
+	if len(findings) != 0 {
+		os.Exit(exitCode)
+	}
+}

+ 67 - 0
cmd/protect.go

@@ -0,0 +1,67 @@
+package cmd
+
+import (
+	"os"
+	"time"
+
+	"github.com/rs/zerolog/log"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+
+	"github.com/zricethezav/gitleaks/v8/config"
+	"github.com/zricethezav/gitleaks/v8/detect"
+	"github.com/zricethezav/gitleaks/v8/git"
+	"github.com/zricethezav/gitleaks/v8/report"
+)
+
+func init() {
+	protectCmd.Flags().Bool("staged", false, "detect secrets in a --staged state")
+	rootCmd.AddCommand(protectCmd)
+}
+
+var protectCmd = &cobra.Command{
+	Use:   "protect",
+	Short: "protect secrets in code",
+	Run:   runProtect,
+}
+
+func runProtect(cmd *cobra.Command, args []string) {
+	initConfig()
+	var vc config.ViperConfig
+
+	viper.Unmarshal(&vc)
+	cfg, err := vc.Translate()
+	if err != nil {
+		log.Fatal().Err(err).Msg("Failed to load config")
+	}
+
+	source, _ := cmd.Flags().GetString("source")
+	verbose, _ := cmd.Flags().GetBool("verbose")
+	redact, _ := cmd.Flags().GetBool("redact")
+	exitCode, _ := cmd.Flags().GetInt("exit-code")
+	staged, _ := cmd.Flags().GetBool("staged")
+	start := time.Now()
+
+	files, err := git.GitDiff(source, staged)
+	if err != nil {
+		log.Fatal().Err(err).Msg("Failed to get git log")
+	}
+
+	findings := detect.FromGit(files, cfg, detect.Options{Verbose: verbose, Redact: redact})
+	if len(findings) != 0 {
+		log.Warn().Msgf("leaks found: %d", len(findings))
+	} else {
+		log.Info().Msg("no leaks found")
+	}
+
+	log.Info().Msgf("scan duration: %s", time.Since(start))
+
+	reportPath, _ := cmd.Flags().GetString("report-path")
+	ext, _ := cmd.Flags().GetString("report-format")
+	if reportPath != "" {
+		report.Write(findings, cfg, ext, reportPath)
+	}
+	if len(findings) != 0 {
+		os.Exit(exitCode)
+	}
+}

+ 112 - 0
cmd/root.go

@@ -0,0 +1,112 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+
+	"github.com/zricethezav/gitleaks/v8/config"
+)
+
+const banner = `
+    ○
+    │╲
+    │ ○
+    ○ ░
+    ░    gitleaks 
+
+`
+
+const configDescription = `config file path
+order of precedence: 
+1. --config/-c 
+2. (--source/-s)/.gitleaks.toml
+if --config/-c is not set and no .gitleaks.toml/gitleaks.toml present 
+then .gitleaks.toml will be written to (--source/-s)/.gitleaks.toml for future use`
+
+var rootCmd = &cobra.Command{
+	Use:   "gitleaks",
+	Short: "Gitleaks scans code, past or present, for secrets",
+}
+
+func init() {
+	cobra.OnInitialize(initLog)
+	rootCmd.PersistentFlags().StringP("config", "c", "", configDescription)
+	rootCmd.PersistentFlags().Int("exit-code", 1, "exit code when leaks have been encountered (default: 1)")
+	rootCmd.PersistentFlags().StringP("source", "s", ".", "path to source (default: $PWD)")
+	rootCmd.PersistentFlags().StringP("report-path", "r", "", "report file")
+	rootCmd.PersistentFlags().StringP("report-format", "f", "", "output format (json, csv, sarif)")
+	rootCmd.PersistentFlags().StringP("log-level", "l", "info", "log level (debug, info, warn, error, fatal)")
+	rootCmd.PersistentFlags().BoolP("verbose", "v", false, "show verbose output from scan")
+	rootCmd.PersistentFlags().Bool("redact", false, "redact secrets from logs and stdout")
+	viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
+}
+
+func initLog() {
+	zerolog.SetGlobalLevel(zerolog.InfoLevel)
+	ll, err := rootCmd.Flags().GetString("log-level")
+	if err != nil {
+		log.Fatal().Err(err)
+	}
+	switch strings.ToLower(ll) {
+	case "debug":
+		zerolog.SetGlobalLevel(zerolog.DebugLevel)
+	case "info":
+		zerolog.SetGlobalLevel(zerolog.InfoLevel)
+	case "warn":
+		zerolog.SetGlobalLevel(zerolog.WarnLevel)
+	case "err", "error":
+		zerolog.SetGlobalLevel(zerolog.ErrorLevel)
+	case "fatal":
+		zerolog.SetGlobalLevel(zerolog.FatalLevel)
+	default:
+		zerolog.SetGlobalLevel(zerolog.InfoLevel)
+	}
+}
+
+func initConfig() {
+	fmt.Fprintf(os.Stderr, banner)
+	cfgPath, err := rootCmd.Flags().GetString("config")
+	if err != nil {
+		log.Fatal().Err(err)
+	}
+	if cfgPath != "" {
+		viper.SetConfigFile(cfgPath)
+		log.Debug().Msgf("Using gitleaks config %s", cfgPath)
+	} else {
+		source, err := rootCmd.Flags().GetString("source")
+		if err != nil {
+			log.Fatal().Err(err)
+		}
+		if _, err := os.Stat(filepath.Join(source, ".gitleaks.toml")); os.IsNotExist(err) {
+			log.Debug().Msgf("No gitleaks config found, writing default gitleaks config to %s", filepath.Join(source, ".gitleaks.toml"))
+			if err := os.WriteFile(filepath.Join(source, ".gitleaks.toml"), []byte(config.DefaultConfig), os.ModePerm); err != nil {
+				log.Debug().Msgf("Unable to write default gitleaks config to %s", filepath.Join(source, ".gitleaks.toml, using default config"))
+				viper.SetConfigType("toml")
+				viper.ReadConfig(strings.NewReader(config.DefaultConfig))
+				return
+			}
+		} else {
+			log.Debug().Msgf("Using existing gitleaks config %s", filepath.Join(source, ".gitleaks.toml"))
+		}
+
+		viper.AddConfigPath(source)
+		viper.SetConfigName(".gitleaks")
+		viper.SetConfigType("toml")
+	}
+	if err := viper.ReadInConfig(); err != nil {
+		log.Fatal().Msgf("Unable to load gitleaks config, err: %s", err)
+	}
+}
+
+func Execute() {
+	if err := rootCmd.Execute(); err != nil {
+		log.Fatal().Err(err)
+	}
+}

+ 23 - 0
cmd/version.go

@@ -0,0 +1,23 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+var Version = "version is set by build process"
+
+func init() {
+	rootCmd.AddCommand(versionCmd)
+}
+
+var versionCmd = &cobra.Command{
+	Use:   "version",
+	Short: "display gitleaks version",
+	Run:   runVersion,
+}
+
+func runVersion(cmd *cobra.Command, args []string) {
+	fmt.Println(Version)
+}

+ 18 - 42
config/allowlist.go

@@ -1,60 +1,36 @@
 package config
 
-import (
-	"regexp"
-)
+import "regexp"
 
-// used for ignoring .git directories when the --no-git flag is set
-// related issue: https://github.com/zricethezav/gitleaks/issues/486
-const dotGit = `/\.git/`
-
-// AllowList is struct containing items that if encountered will allowlist
-// a commit/line of code that would be considered a leak.
-type AllowList struct {
+type Allowlist struct {
 	Description string
 	Regexes     []*regexp.Regexp
-	Commits     []string
-	Files       []*regexp.Regexp
 	Paths       []*regexp.Regexp
-	Repos       []*regexp.Regexp
+	Commits     []string
 }
 
-// CommitAllowed checks if a commit is allowlisted
-func (a *AllowList) CommitAllowed(commit string) bool {
-	for _, hash := range a.Commits {
-		if commit == hash {
+func (a *Allowlist) CommitAllowed(c string) bool {
+	if c == "" {
+		return false
+	}
+	for _, commit := range a.Commits {
+		if commit == c {
 			return true
 		}
 	}
 	return false
 }
 
-// FileAllowed checks if a file is allowlisted
-func (a *AllowList) FileAllowed(fileName string) bool {
-	return anyRegexMatch(fileName, a.Files)
-}
-
-// PathAllowed checks if a path is allowlisted
-func (a *AllowList) PathAllowed(filePath string) bool {
-	return anyRegexMatch(filePath, a.Paths)
-}
-
-// RegexAllowed checks if a regex is allowlisted
-func (a *AllowList) RegexAllowed(content string) bool {
-	return anyRegexMatch(content, a.Regexes)
-}
-
-// RepoAllowed checks if a regex is allowlisted
-func (a *AllowList) RepoAllowed(repo string) bool {
-	return anyRegexMatch(repo, a.Repos)
+func (a *Allowlist) PathAllowed(path string) bool {
+	if anyRegexMatch(path, a.Paths) {
+		return true
+	}
+	return false
 }
 
-// IgnoreDotGit appends a `\.git` rule to ignore all .git paths. This is used for --no-git scans
-func (a *AllowList) IgnoreDotGit() error {
-	re, err := regexp.Compile(dotGit)
-	if err != nil {
-		return err
+func (a *Allowlist) RegexAllowed(s string) bool {
+	if anyRegexMatch(s, a.Regexes) {
+		return true
 	}
-	a.Paths = append(a.Paths, re)
-	return nil
+	return false
 }

+ 93 - 0
config/allowlist_test.go

@@ -0,0 +1,93 @@
+package config
+
+import (
+	"regexp"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCommitAllowed(t *testing.T) {
+	tests := []struct {
+		allowlist     Allowlist
+		commit        string
+		commitAllowed bool
+	}{
+		{
+			allowlist: Allowlist{
+				Commits: []string{"commitA"},
+			},
+			commit:        "commitA",
+			commitAllowed: true,
+		},
+		{
+			allowlist: Allowlist{
+				Commits: []string{"commitB"},
+			},
+			commit:        "commitA",
+			commitAllowed: false,
+		},
+		{
+			allowlist: Allowlist{
+				Commits: []string{"commitB"},
+			},
+			commit:        "",
+			commitAllowed: false,
+		},
+	}
+	for _, tt := range tests {
+		assert.Equal(t, tt.commitAllowed, tt.allowlist.CommitAllowed(tt.commit))
+	}
+}
+
+func TestRegexAllowed(t *testing.T) {
+	tests := []struct {
+		allowlist    Allowlist
+		secret       string
+		regexAllowed bool
+	}{
+		{
+			allowlist: Allowlist{
+				Regexes: []*regexp.Regexp{regexp.MustCompile("matchthis")},
+			},
+			secret:       "a secret: matchthis, done",
+			regexAllowed: true,
+		},
+		{
+			allowlist: Allowlist{
+				Regexes: []*regexp.Regexp{regexp.MustCompile("matchthis")},
+			},
+			secret:       "a secret",
+			regexAllowed: false,
+		},
+	}
+	for _, tt := range tests {
+		assert.Equal(t, tt.regexAllowed, tt.allowlist.RegexAllowed(tt.secret))
+	}
+}
+
+func TestPathAllowed(t *testing.T) {
+	tests := []struct {
+		allowlist   Allowlist
+		path        string
+		pathAllowed bool
+	}{
+		{
+			allowlist: Allowlist{
+				Paths: []*regexp.Regexp{regexp.MustCompile("path")},
+			},
+			path:        "a path",
+			pathAllowed: true,
+		},
+		{
+			allowlist: Allowlist{
+				Paths: []*regexp.Regexp{regexp.MustCompile("path")},
+			},
+			path:        "a ???",
+			pathAllowed: false,
+		},
+	}
+	for _, tt := range tests {
+		assert.Equal(t, tt.pathAllowed, tt.allowlist.PathAllowed(tt.path))
+	}
+}

+ 79 - 291
config/config.go

@@ -3,321 +3,109 @@ package config
 import (
 	_ "embed"
 	"fmt"
-	"io"
-	"os"
-	"path"
-	"path/filepath"
 	"regexp"
-	"strconv"
-
-	"github.com/zricethezav/gitleaks/v7/options"
-
-	"github.com/BurntSushi/toml"
-	"github.com/go-git/go-git/v5"
-	log "github.com/sirupsen/logrus"
 )
 
 //go:embed gitleaks.toml
 var DefaultConfig string
 
-// Config is a composite struct of Rules and Allowlists
-// Each Rule contains a description, regular expression, tags, and allowlists if available
-type Config struct {
-	Rules     []Rule
-	Allowlist AllowList
-}
-
-// Entropy represents an entropy range
-type Entropy struct {
-	Min   float64
-	Max   float64
-	Group int
-}
-
-// TomlAllowList is a struct used in the TomlLoader that loads in allowlists from
-// specific rules or globally at the top level config
-type TomlAllowList struct {
+// ViperConfig is the config struct used by the Viper config package
+// to parse the config file. This struct does not include regular expressions.
+// It is used as an intermediary to convert the Viper config to the Config struct.
+type ViperConfig struct {
 	Description string
-	Regexes     []string
-	Commits     []string
-	Files       []string
-	Paths       []string
-	Repos       []string
-}
-
-// TomlLoader gets loaded with the values from a gitleaks toml config
-// see the config in config/gitleaks.toml for an example. TomlLoader is used
-// to generate Config values (compiling regexes, etc).
-type TomlLoader struct {
-	AllowList TomlAllowList
-	Rules     []struct {
-		Description string
-		Regex       string
-		File        string
-		Path        string
-		ReportGroup int
-		Tags        []string
-		Entropies   []struct {
-			Min   string
-			Max   string
-			Group string
-		}
-		AllowList TomlAllowList
+	Rules       []struct {
+		ID           string
+		Description  string
+		Entropy      float64
+		EntropyGroup int
+		Regex        string
+		Path         string
+		Tags         []string
+
+		Allowlist struct {
+			Regexes []string
+			Paths   []string
+			Commits []string
+		}
+	}
+	Allowlist struct {
+		Regexes []string
+		Paths   []string
+		Commits []string
 	}
 }
 
-// NewConfig will create a new config struct which contains
-// rules on how gitleaks will proceed with its scan.
-// If no options are passed via cli then NewConfig will return
-// a default config which can be seen in config.go
-func NewConfig(options options.Options) (Config, error) {
-	var cfg Config
-	tomlLoader := TomlLoader{}
-
-	var err error
-	if options.ConfigPath != "" {
-		_, err = toml.DecodeFile(options.ConfigPath, &tomlLoader)
-		// append a allowlist rule for allowlisting the config
-		tomlLoader.AllowList.Files = append(tomlLoader.AllowList.Files, path.Base(options.ConfigPath))
-	} else {
-		_, err = toml.Decode(DefaultConfig, &tomlLoader)
-	}
-	if err != nil {
-		return cfg, err
-	}
-
-	cfg, err = tomlLoader.Parse()
-	if err != nil {
-		return cfg, err
-	}
-
-	return cfg, nil
+// Config is a configuration struct that contains rules and an allowlist if present.
+type Config struct {
+	Description string
+	Rules       []*Rule
+	Allowlist   Allowlist
 }
 
-// Parse will parse the values set in a TomlLoader and use those values
-// to create compiled regular expressions and rules used in scans
-func (tomlLoader TomlLoader) Parse() (Config, error) {
-	var cfg Config
-	for _, rule := range tomlLoader.Rules {
-		// check and make sure the rule is valid
-		if rule.Regex == "" && rule.Path == "" && rule.File == "" && len(rule.Entropies) == 0 {
-			log.Warnf("Rule %s does not define any actionable data", rule.Description)
-			continue
-		}
-		re, err := regexp.Compile(rule.Regex)
-		if err != nil {
-			return cfg, fmt.Errorf("problem loading config: %v", err)
-		}
-		fileNameRe, err := regexp.Compile(rule.File)
-		if err != nil {
-			return cfg, fmt.Errorf("problem loading config: %v", err)
-		}
-		filePathRe, err := regexp.Compile(rule.Path)
-		if err != nil {
-			return cfg, fmt.Errorf("problem loading config: %v", err)
-		}
-
-		// rule specific allowlists
-		var allowList AllowList
-
-		allowList.Description = rule.AllowList.Description
-
-		// rule specific regexes
-		for _, re := range rule.AllowList.Regexes {
-			allowListedRegex, err := regexp.Compile(re)
-			if err != nil {
-				return cfg, fmt.Errorf("problem loading config: %v", err)
-			}
-			allowList.Regexes = append(allowList.Regexes, allowListedRegex)
-		}
-
-		// rule specific filenames
-		for _, re := range rule.AllowList.Files {
-			allowListedRegex, err := regexp.Compile(re)
-			if err != nil {
-				return cfg, fmt.Errorf("problem loading config: %v", err)
-			}
-			allowList.Files = append(allowList.Files, allowListedRegex)
-		}
-
-		// rule specific paths
-		for _, re := range rule.AllowList.Paths {
-			allowListedRegex, err := regexp.Compile(re)
-			if err != nil {
-				return cfg, fmt.Errorf("problem loading config: %v", err)
-			}
-			allowList.Paths = append(allowList.Paths, allowListedRegex)
-		}
-
-		// rule specific commits
-		allowList.Commits = rule.AllowList.Commits
-
-		var entropies []Entropy
-		for _, e := range rule.Entropies {
-			min, err := strconv.ParseFloat(e.Min, 64)
-			if err != nil {
-				return cfg, err
-			}
-			max, err := strconv.ParseFloat(e.Max, 64)
-			if err != nil {
-				return cfg, err
-			}
-			if e.Group == "" {
-				e.Group = "0"
-			}
-			group, err := strconv.ParseInt(e.Group, 10, 64)
-			if err != nil {
-				return cfg, err
-			} else if int(group) >= len(re.SubexpNames()) {
-				return cfg, fmt.Errorf("problem loading config: group cannot be higher than number of groups in regexp")
-			} else if group < 0 {
-				return cfg, fmt.Errorf("problem loading config: group cannot be lower than 0")
-			} else if min > 8.0 || min < 0.0 || max > 8.0 || max < 0.0 {
-				return cfg, fmt.Errorf("problem loading config: invalid entropy ranges, must be within 0.0-8.0")
-			} else if min > max {
-				return cfg, fmt.Errorf("problem loading config: entropy Min value cannot be higher than Max value")
-			}
-
-			entropies = append(entropies, Entropy{Min: min, Max: max, Group: int(group)})
-		}
-
-		r := Rule{
-			Description: rule.Description,
-			Regex:       re,
-			File:        fileNameRe,
-			Path:        filePathRe,
-			ReportGroup: rule.ReportGroup,
-			Tags:        rule.Tags,
-			AllowList:   allowList,
-			Entropies:   entropies,
+func (vc *ViperConfig) Translate() (Config, error) {
+	var rules []*Rule
+	for _, r := range vc.Rules {
+		var allowlistRegexes []*regexp.Regexp
+		for _, a := range r.Allowlist.Regexes {
+			allowlistRegexes = append(allowlistRegexes, regexp.MustCompile(a))
 		}
-
-		cfg.Rules = append(cfg.Rules, r)
-	}
-
-	// global regex allowLists
-	for _, allowListRegex := range tomlLoader.AllowList.Regexes {
-		re, err := regexp.Compile(allowListRegex)
-		if err != nil {
-			return cfg, fmt.Errorf("problem loading config: %v", err)
+		var allowlistPaths []*regexp.Regexp
+		for _, a := range r.Allowlist.Paths {
+			allowlistPaths = append(allowlistPaths, regexp.MustCompile(a))
 		}
-		cfg.Allowlist.Regexes = append(cfg.Allowlist.Regexes, re)
-	}
 
-	// global file name allowLists
-	for _, allowListFileName := range tomlLoader.AllowList.Files {
-		re, err := regexp.Compile(allowListFileName)
-		if err != nil {
-			return cfg, fmt.Errorf("problem loading config: %v", err)
+		if r.Tags == nil {
+			r.Tags = []string{}
 		}
-		cfg.Allowlist.Files = append(cfg.Allowlist.Files, re)
-	}
 
-	// global file path allowLists
-	for _, allowListFilePath := range tomlLoader.AllowList.Paths {
-		re, err := regexp.Compile(allowListFilePath)
-		if err != nil {
-			return cfg, fmt.Errorf("problem loading config: %v", err)
+		var configRegex *regexp.Regexp
+		var configPathRegex *regexp.Regexp
+		if r.Regex == "" {
+			configRegex = nil
+		} else {
+			configRegex = regexp.MustCompile(r.Regex)
 		}
-		cfg.Allowlist.Paths = append(cfg.Allowlist.Paths, re)
-	}
-
-	// global repo allowLists
-	for _, allowListRepo := range tomlLoader.AllowList.Repos {
-		re, err := regexp.Compile(allowListRepo)
-		if err != nil {
-			return cfg, fmt.Errorf("problem loading config: %v", err)
+		if r.Path == "" {
+			configPathRegex = nil
+		} else {
+			configPathRegex = regexp.MustCompile(r.Path)
 		}
-		cfg.Allowlist.Repos = append(cfg.Allowlist.Repos, re)
-	}
-
-	cfg.Allowlist.Commits = tomlLoader.AllowList.Commits
-	cfg.Allowlist.Description = tomlLoader.AllowList.Description
-
-	return cfg, nil
-}
-
-// LoadRepoConfig accepts a repo and config path related to the target repo's root.
-func LoadRepoConfig(repo *git.Repository, repoConfig string) (Config, error) {
-	gitRepoConfig, err := repo.Config()
-	if err != nil {
-		return Config{}, err
-	}
-	if !gitRepoConfig.Core.IsBare {
-		wt, err := repo.Worktree()
-		if err != nil {
-			return Config{}, err
-		}
-		_, err = wt.Filesystem.Stat(repoConfig)
-		if err != nil {
-			return Config{}, err
+		r := &Rule{
+			Description:    r.Description,
+			RuleID:         r.ID,
+			Regex:          configRegex,
+			Path:           configPathRegex,
+			EntropyReGroup: r.EntropyGroup,
+			Entropy:        r.Entropy,
+			Tags:           r.Tags,
+			Allowlist: Allowlist{
+				Regexes: allowlistRegexes,
+				Paths:   allowlistPaths,
+				Commits: r.Allowlist.Commits,
+			},
 		}
-		r, err := wt.Filesystem.Open(repoConfig)
-		if err != nil {
-			return Config{}, err
+		if r.Regex != nil && r.EntropyReGroup > r.Regex.NumSubexp() {
+			return Config{}, fmt.Errorf("%s invalid regex entropy group %d, max regex entropy group %d", r.Description, r.EntropyReGroup, r.Regex.NumSubexp())
 		}
-		return parseTomlFile(r)
-	}
-
-	log.Debug("attempting to load repo config from bare worktree, this may use an old config")
-	ref, err := repo.Head()
-	if err != nil {
-		return Config{}, err
-	}
+		rules = append(rules, r)
 
-	c, err := repo.CommitObject(ref.Hash())
-	if err != nil {
-		return Config{}, err
 	}
-
-	f, err := c.File(repoConfig)
-	if err != nil {
-		return Config{}, err
+	var allowlistRegexes []*regexp.Regexp
+	for _, a := range vc.Allowlist.Regexes {
+		allowlistRegexes = append(allowlistRegexes, regexp.MustCompile(a))
 	}
-
-	r, err := f.Reader()
-
-	if err != nil {
-		return Config{}, err
+	var allowlistPaths []*regexp.Regexp
+	for _, a := range vc.Allowlist.Paths {
+		allowlistPaths = append(allowlistPaths, regexp.MustCompile(a))
 	}
-
-	return parseTomlFile(r)
-}
-
-// LoadAdditionalConfig Accepts a path to a gitleaks config and returns a Config struct
-func LoadAdditionalConfig(repoConfig string) (Config, error) {
-	file, err := os.Open(filepath.Clean(repoConfig))
-	if err != nil {
-		return Config{}, err
-	}
-
-	return parseTomlFile(file)
-}
-
-// AppendConfig Accepts a Config struct and will append those fields to this Config Struct's fields
-func (config *Config) AppendConfig(configToBeAppended Config) Config {
-	newAllowList := AllowList{
-		Description: "Appended Configuration",
-		Commits:     append(config.Allowlist.Commits, configToBeAppended.Allowlist.Commits...),
-		Files:       append(config.Allowlist.Files, configToBeAppended.Allowlist.Files...),
-		Paths:       append(config.Allowlist.Paths, configToBeAppended.Allowlist.Paths...),
-		Regexes:     append(config.Allowlist.Regexes, configToBeAppended.Allowlist.Regexes...),
-		Repos:       append(config.Allowlist.Repos, configToBeAppended.Allowlist.Repos...),
-	}
-
 	return Config{
-		Rules:     append(config.Rules, configToBeAppended.Rules...),
-		Allowlist: newAllowList,
-	}
-}
-
-// takes a File, makes sure it is a valid config, and parses it
-func parseTomlFile(f io.Reader) (Config, error) {
-	var tomlLoader TomlLoader
-	_, err := toml.DecodeReader(f, &tomlLoader)
-	if err != nil {
-		log.Errorf("Unable to read gitleaks config. Using defaults. Error: %s", err)
-		return Config{}, err
-	}
-	return tomlLoader.Parse()
+		Description: vc.Description,
+		Rules:       rules,
+		Allowlist: Allowlist{
+			Regexes: allowlistRegexes,
+			Paths:   allowlistPaths,
+			Commits: vc.Allowlist.Commits,
+		},
+	}, nil
 }

+ 136 - 245
config/config_test.go

@@ -1,287 +1,178 @@
-package config_test
+package config
 
 import (
 	"fmt"
-	"io/ioutil"
-	"os"
-	"path/filepath"
 	"regexp"
 	"testing"
 
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
+	"github.com/spf13/viper"
+	"github.com/stretchr/testify/assert"
 )
 
-const configPath = "../testdata/configs/"
+const configPath = "../testdata/config/"
 
-func TestParse(t *testing.T) {
+func TestTranslate(t *testing.T) {
 	tests := []struct {
-		description   string
-		opts          options.Options
-		wantErr       error
-		wantFileRegex *regexp.Regexp
-		wantMessages  *regexp.Regexp
-		wantAllowlist config.AllowList
+		cfgName   string
+		cfg       Config
+		wantError error
 	}{
 		{
-			description: "default config",
-			opts:        options.Options{},
-		},
-		{
-			description: "test successful load",
-			opts: options.Options{
-				ConfigPath: filepath.ToSlash(filepath.Join(configPath, "aws_key.toml")),
-			},
-		},
-		{
-			description: "test bad toml",
-			opts: options.Options{
-				ConfigPath: filepath.ToSlash(filepath.Join(configPath, "bad_aws_key.toml")),
-			},
-			wantErr: fmt.Errorf("Near line 7 (last key parsed 'rules.description'): expected value but found \"AWS\" instead"),
-		},
-		{
-			description: "test bad regex",
-			opts: options.Options{
-				ConfigPath: filepath.ToSlash(filepath.Join(configPath, "bad_regex_aws_key.toml")),
-			},
-			wantErr: fmt.Errorf("problem loading config: error parsing regexp: invalid nested repetition operator: `???`"),
-		},
-		{
-			description: "test bad global allowlist file regex",
-			opts: options.Options{
-				ConfigPath: filepath.ToSlash(filepath.Join(configPath, "bad_aws_key_global_allowlist_file.toml")),
-			},
-			wantErr: fmt.Errorf("problem loading config: error parsing regexp: missing argument to repetition operator: `??`"),
-		},
-		{
-			description: "test bad global file regex",
-			opts: options.Options{
-				ConfigPath: filepath.ToSlash(filepath.Join(configPath, "bad_aws_key_file_regex.toml")),
-			},
-			wantErr: fmt.Errorf("problem loading config: error parsing regexp: missing argument to repetition operator: `??`"),
-		},
-		{
-			description: "test successful load big ol thing",
-			opts: options.Options{
-				ConfigPath: filepath.ToSlash(filepath.Join(configPath, "large.toml")),
-			},
-		},
-		{
-			description: "test load entropy",
-			opts: options.Options{
-				ConfigPath: filepath.ToSlash(filepath.Join(configPath, "entropy.toml")),
-			},
-		},
-		{
-			description: "test entropy bad range",
-			opts: options.Options{
-				ConfigPath: filepath.ToSlash(filepath.Join(configPath, "bad_entropy_1.toml")),
+			cfgName: "allow_aws_re",
+			cfg: Config{
+				Rules: []*Rule{
+					{
+						Description: "AWS Access Key",
+						Regex:       regexp.MustCompile("(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}"),
+						Tags:        []string{"key", "AWS"},
+						RuleID:      "aws-access-key",
+						Allowlist: Allowlist{
+							Regexes: []*regexp.Regexp{
+								regexp.MustCompile("AKIALALEMEL33243OLIA"),
+							},
+						},
+					},
+				},
 			},
-			wantErr: fmt.Errorf("problem loading config: entropy Min value cannot be higher than Max value"),
 		},
 		{
-			description: "test entropy value max",
-			opts: options.Options{
-				ConfigPath: filepath.ToSlash(filepath.Join(configPath, "bad_entropy_2.toml")),
+			cfgName: "allow_commit",
+			cfg: Config{
+				Rules: []*Rule{
+					{
+						Description: "AWS Access Key",
+						Regex:       regexp.MustCompile("(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}"),
+						Tags:        []string{"key", "AWS"},
+						RuleID:      "aws-access-key",
+						Allowlist: Allowlist{
+							Commits: []string{"allowthiscommit"},
+						},
+					},
+				},
 			},
-			wantErr: fmt.Errorf("strconv.ParseFloat: parsing \"x\": invalid syntax"),
 		},
 		{
-			description: "test entropy value min",
-			opts: options.Options{
-				ConfigPath: filepath.ToSlash(filepath.Join(configPath, "bad_entropy_3.toml")),
+			cfgName: "allow_path",
+			cfg: Config{
+				Rules: []*Rule{
+					{
+						Description: "AWS Access Key",
+						Regex:       regexp.MustCompile("(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}"),
+						Tags:        []string{"key", "AWS"},
+						RuleID:      "aws-access-key",
+						Allowlist: Allowlist{
+							Paths: []*regexp.Regexp{
+								regexp.MustCompile(".go"),
+							},
+						},
+					},
+				},
 			},
-			wantErr: fmt.Errorf("strconv.ParseFloat: parsing \"x\": invalid syntax"),
 		},
 		{
-			description: "test entropy value group",
-			opts: options.Options{
-				ConfigPath: filepath.ToSlash(filepath.Join(configPath, "bad_entropy_4.toml")),
+			cfgName: "entropy_group",
+			cfg: Config{
+				Rules: []*Rule{
+					{
+						Description:    "Discord API key",
+						Regex:          regexp.MustCompile(`(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{64})['\"]`),
+						RuleID:         "discord-api-key",
+						Allowlist:      Allowlist{},
+						Entropy:        3.5,
+						EntropyReGroup: 3,
+						Tags:           []string{},
+					},
+				},
 			},
-			wantErr: fmt.Errorf("strconv.ParseInt: parsing \"x\": invalid syntax"),
 		},
 		{
-			description: "test entropy value group",
-			opts: options.Options{
-				ConfigPath: filepath.ToSlash(filepath.Join(configPath, "bad_entropy_5.toml")),
-			},
-			wantErr: fmt.Errorf("problem loading config: group cannot be lower than 0"),
-		},
-		{
-			description: "test entropy value group",
-			opts: options.Options{
-				ConfigPath: filepath.ToSlash(filepath.Join(configPath, "bad_entropy_6.toml")),
-			},
-			wantErr: fmt.Errorf("problem loading config: group cannot be higher than number of groups in regexp"),
-		},
-		{
-			description: "test entropy range limits",
-			opts: options.Options{
-				ConfigPath: filepath.ToSlash(filepath.Join(configPath, "bad_entropy_7.toml")),
-			},
-			wantErr: fmt.Errorf("problem loading config: invalid entropy ranges, must be within 0.0-8.0"),
+			cfgName:   "bad_entropy_group",
+			cfg:       Config{},
+			wantError: fmt.Errorf("Discord API key invalid regex entropy group 5, max regex entropy group 3"),
 		},
 	}
 
-	for _, test := range tests {
-		_, err := config.NewConfig(test.opts)
+	for _, tt := range tests {
+		viper.Reset()
+		viper.AddConfigPath(configPath)
+		viper.SetConfigName(tt.cfgName)
+		viper.SetConfigType("toml")
+		err := viper.ReadInConfig()
 		if err != nil {
-			if test.wantErr == nil {
-				t.Error(test.description, err)
-			} else if test.wantErr.Error() != err.Error() {
-				t.Errorf("expected err: %s, got %s", test.wantErr, err)
-			}
+			t.Error(err)
 		}
-	}
-}
 
-// TestParseFields will test that fields are properly parsed from a config. As fields are added, then please
-// add tests here.
-func TestParseFields(t *testing.T) {
-	tomlConfig := `
-[[rules]]
-	description = "Some Groups without a reportGroup"
-	regex = '(.)(.)'
-
-[[rules]]
-	description = "Some Groups"
-	regex = '(.)(.)'
-  reportGroup = 1
-`
-	configPath, err := writeTestConfig(tomlConfig)
-	defer os.Remove(configPath)
-	if err != nil {
-		t.Fatal(err)
-	}
+		var vc ViperConfig
+		viper.Unmarshal(&vc)
+		cfg, err := vc.Translate()
+		if tt.wantError != nil {
+			if err == nil {
+				t.Errorf("expected error")
+			}
+			assert.Equal(t, tt.wantError, err)
+		}
 
-	config, err := config.NewConfig(options.Options{ConfigPath: configPath})
-	if err != nil {
-		t.Fatalf("Couldn't parse config: %v", err)
+		assert.Equal(t, cfg.Rules, tt.cfg.Rules)
 	}
+}
 
-	expectedRuleFields := []struct {
-		Description string
-		ReportGroup int
+func TestIncludeEntropy(t *testing.T) {
+	tests := []struct {
+		rule    Rule
+		secret  string
+		entropy float32
+		include bool
 	}{
 		{
-			Description: "Some Groups without a reportGroup",
-			ReportGroup: 0,
+			rule: Rule{
+				RuleID:         "generic-api-key",
+				EntropyReGroup: 4,
+				Entropy:        3.5,
+				Regex:          regexp.MustCompile(`(?i)((key|api|token|secret|password)[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9a-zA-Z\-_=]{8,64})['\"]`),
+			},
+			secret:  `Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
+			entropy: 3.7906235872459746,
+			include: true,
 		},
 		{
-			Description: "Some Groups",
-			ReportGroup: 1,
+			rule: Rule{
+				RuleID:         "generic-api-key",
+				EntropyReGroup: 4,
+				Entropy:        4,
+				Regex:          regexp.MustCompile(`(?i)((key|api|token|secret|password)[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9a-zA-Z\-_=]{8,64})['\"]`),
+			},
+			secret:  `Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
+			entropy: 3.7906235872459746,
+			include: false,
+		},
+		{
+			rule: Rule{
+				RuleID:         "generic-api-key",
+				EntropyReGroup: 4,
+				Entropy:        3.0,
+				Regex:          regexp.MustCompile(`(?i)((key|api|token|secret|password)[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9a-zA-Z\-_=]{8,64})['\"]`),
+			},
+			secret:  `KeyboardInteractiveName = "ssh-keyboard-interactive"`,
+			entropy: 0,
+			include: false,
+		},
+		{
+			rule: Rule{
+				RuleID:         "generic-api-key",
+				EntropyReGroup: 4,
+				Entropy:        3.0,
+				Regex:          regexp.MustCompile(`(?i)((key|api|token|secret|password)[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9a-zA-Z\-_=]{8,64})['\"]`),
+			},
+			secret:  `KeyboardInteractiveName = "ssh-keyboard-interactive"`,
+			entropy: 0,
+			include: false,
 		},
 	}
 
-	if len(config.Rules) != len(expectedRuleFields) {
-		t.Fatalf("expected %v rules", len(expectedRuleFields))
-	}
-
-	for _, expected := range expectedRuleFields {
-		rule, err := findRuleByDescription(config.Rules, expected.Description)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if rule.ReportGroup != expected.ReportGroup {
-			t.Errorf("expected the rule with description '%v' to have a ReportGroup of %v", expected.Description, expected.ReportGroup)
-		}
-	}
-}
-
-func findRuleByDescription(rules []config.Rule, description string) (*config.Rule, error) {
-	for _, rule := range rules {
-		if rule.Description == description {
-			return &rule, nil
-		}
-	}
-
-	return nil, fmt.Errorf("Couldn't find rule with the description: %s", description)
-}
-
-func writeTestConfig(toml string) (string, error) {
-	tmpfile, err := ioutil.TempFile(".", "testConfig")
-	if err != nil {
-		return "", fmt.Errorf("Couldn't create test config got: %w", err)
-	}
-
-	if _, err := tmpfile.Write([]byte(toml)); err != nil {
-		return "", fmt.Errorf("Couldn't create test config got: %w", err)
-	}
-
-	if err := tmpfile.Close(); err != nil {
-		return "", fmt.Errorf("Couldn't create test config got: %w", err)
-	}
-
-	return tmpfile.Name(), nil
-}
-
-func TestAppendingConfiguration(t *testing.T) {
-	testRegexA, _ := regexp.Compile("a")
-	testRegexB, _ := regexp.Compile("b")
-
-	allowListA := config.AllowList{
-		Description: "Test Description",
-		Commits:     []string{"a"},
-		Files:       []*regexp.Regexp{testRegexA},
-		Paths:       []*regexp.Regexp{testRegexA},
-		Regexes:     []*regexp.Regexp{testRegexA},
-		Repos:       []*regexp.Regexp{testRegexA},
-	}
-
-	allowListB := config.AllowList{
-		Description: "Test Description",
-		Commits:     []string{"b"},
-		Files:       []*regexp.Regexp{testRegexB},
-		Paths:       []*regexp.Regexp{testRegexB},
-		Regexes:     []*regexp.Regexp{testRegexB},
-		Repos:       []*regexp.Regexp{testRegexB},
-	}
-
-	ruleA := config.Rule{Description: "a"}
-	ruleB := config.Rule{Description: "b"}
-
-	rulesA := []config.Rule{ruleA}
-	rulesB := []config.Rule{ruleB}
-
-	cfgA := config.Config{
-		Rules:     rulesA,
-		Allowlist: allowListA,
-	}
-
-	cfgB := config.Config{
-		Rules:     rulesB,
-		Allowlist: allowListB,
-	}
-
-	cfgAppended := cfgA.AppendConfig(cfgB)
-
-	if !(len(cfgAppended.Rules) == 2) {
-		t.Errorf("Length of Appended Rules = %d; want 2", len(cfgAppended.Rules))
-	}
-
-	if !(len(cfgAppended.Allowlist.Commits) == 2) {
-		t.Errorf("Length of Appended Allowed Commits = %d; want 2", len(cfgAppended.Allowlist.Commits))
-	}
-
-	if !(len(cfgAppended.Allowlist.Files) == 2) {
-		t.Errorf("Length of Appended Allowed Files = %d; want 2", len(cfgAppended.Allowlist.Files))
-	}
-
-	if !(len(cfgAppended.Allowlist.Paths) == 2) {
-		t.Errorf("Length of Appended Allowed Paths = %d; want 2", len(cfgAppended.Allowlist.Paths))
-	}
-
-	if !(len(cfgAppended.Allowlist.Regexes) == 2) {
-		t.Errorf("Length of Appended Allowed Regexes = %d; want 2", len(cfgAppended.Allowlist.Regexes))
-	}
-
-	if !(len(cfgAppended.Allowlist.Repos) == 2) {
-		t.Errorf("Length of Appended Allowed Repos = %d; want 2", len(cfgAppended.Allowlist.Repos))
-	}
-
-	if cfgAppended.Allowlist.Description != "Appended Configuration" {
-		t.Errorf("Allow List Description is = \"%s\"; want \"Appended Configuration\"", cfgAppended.Allowlist.Description)
+	for _, tt := range tests {
+		include, entropy := tt.rule.IncludeEntropy(tt.secret)
+		assert.Equal(t, true, tt.rule.EntropySet())
+		assert.Equal(t, tt.entropy, float32(entropy))
+		assert.Equal(t, tt.include, include)
 	}
-
 }

+ 380 - 107
config/gitleaks.toml

@@ -1,175 +1,448 @@
-
 title = "gitleaks config"
 
+# Gitleaks rules are defined by regular expressions and entropy ranges.
+# Some secrets have unique signatures which make detecting those secrets easy.
+# Examples of those secrets would be Gitlab Personal Access Tokens, AWS keys, and Github Access Tokens. 
+# All these examples have defined prefixes like `glpat`, `AKIA`, `ghp_`, etc.
+# 
+# Other secrets might just be a hash which means we need to write more complex rules to verify
+# that what we are matching is a secret.
+# 
+# Here is an example of a semi-generic secret
+#
+#   discord_client_secret = "8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ"
+# 
+# We can write a regular expression to capture the variable name (identifier), 
+# the assignment symbol (like '=' or ':='), and finally the actual secret.
+# The structure of a rule to match this example secret is below:
+#
+#                                                           Beginning string                           
+#                                                               quotation                              
+#                                                                   │            End string quotation  
+#                                                                   │                      │           
+#                                                                   ▼                      ▼           
+#    (?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9=_\-]{32})['\"]         
+#                                                                                                      
+#                   ▲                              ▲                                ▲                  
+#                   │                              │                                │                  
+#                   │                              │                                │                  
+#              identifier                  assignment symbol                                           
+#                                                                                Secret                
+#                                                                                                      
+[[rules]]
+id = "gitlab-pat"
+description = "GitLab Personal Access Token"
+regex = '''glpat-[0-9a-zA-Z\-]{20}'''
+
+[[rules]]
+id = "aws-access-token"
+description = "AWS"
+regex = '''AKIA[0-9A-Z]{16}'''
+
+# Cryptographic keys
+[[rules]]
+id = "PKCS8-PK"
+description = "PKCS8 private key"
+regex = '''-----BEGIN PRIVATE KEY-----'''
+
+[[rules]]
+id = "RSA-PK"
+description = "RSA private key"
+regex = '''-----BEGIN RSA PRIVATE KEY-----'''
+
+[[rules]]
+id = "OPENSSH-PK"
+description = "SSH private key"
+regex = '''-----BEGIN OPENSSH PRIVATE KEY-----'''
+
+[[rules]]
+id = "PGP-PK"
+description = "PGP private key"
+regex = '''-----BEGIN PGP PRIVATE KEY BLOCK-----'''
+
+[[rules]]
+id = "github-pat"
+description = "Github Personal Access Token"
+regex = '''ghp_[0-9a-zA-Z]{36}'''
+
+[[rules]]
+id = "github-oauth"
+description = "Github OAuth Access Token"
+regex = '''gho_[0-9a-zA-Z]{36}'''
+
+[[rules]]
+id = "SSH-DSA-PK"
+description = "SSH (DSA) private key"
+regex = '''-----BEGIN DSA PRIVATE KEY-----'''
+
+[[rules]]
+id = "SSH-EC-PK"
+description = "SSH (EC) private key"
+regex = '''-----BEGIN EC PRIVATE KEY-----'''
+
+
+[[rules]]
+id = "github-app-token"
+description = "Github App Token"
+regex = '''(ghu|ghs)_[0-9a-zA-Z]{36}'''
+
+[[rules]]
+id = "github-refresh-token"
+description = "Github Refresh Token"
+regex = '''ghr_[0-9a-zA-Z]{76}'''
+
+[[rules]]
+id = "shopify-shared-secret"
+description = "Shopify shared secret"
+regex = '''shpss_[a-fA-F0-9]{32}'''
+
+[[rules]]
+id = "shopify-access-token"
+description = "Shopify access token"
+regex = '''shpat_[a-fA-F0-9]{32}'''
+
+[[rules]]
+id = "shopify-custom-access-token"
+description = "Shopify custom app access token"
+regex = '''shpca_[a-fA-F0-9]{32}'''
+
+[[rules]]
+id = "shopify-private-app-access-token"
+description = "Shopify private app access token"
+regex = '''shppa_[a-fA-F0-9]{32}'''
+
+[[rules]]
+id = "slack-access-token"
+description = "Slack token"
+regex = '''xox[baprs]-([0-9a-zA-Z]{10,48})?'''
+
+[[rules]]
+id = "stripe-access-token"
+description = "Stripe"
+regex = '''(?i)(sk|pk)_(test|live)_[0-9a-z]{10,32}'''
+
+[[rules]]
+id = "pypi-upload-token"
+description = "PyPI upload token"
+regex = '''pypi-AgEIcHlwaS5vcmc[A-Za-z0-9-_]{50,1000}'''
+
+[[rules]]
+id = "generic-api-key"
+description = "Generic API Key"
+regex = '''(?i)((key|api|token|secret|password)[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9a-zA-Z\-_=]{8,64})['\"]'''
+entropy = 3.7
+entropyGroup = 4
+
+# ➜  ~/code/gitleaks (v8) git show ec2fc9d6cb0954fb3b57201cf6133c48d8ca0d29 -- checks_test.go
+[[rules]]
+id = "gcp-service-account"
+description = "Google (GCP) Service-account"
+regex = '''\"type\": \"service_account\"'''
+
+[[rules]]
+id = "heroku-api-key"
+description = "Heroku API Key"
+regex = ''' (?i)(heroku[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})['\"]'''
+
+[[rules]]
+id = "slack-web-hook"
+description = "Slack Webhook"
+regex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}'''
+
+[[rules]]
+id = "twilio-api-key"
+description = "Twilio API Key"
+regex = '''SK[0-9a-fA-F]{32}'''
+
+[[rules]]
+id = "age-secret-key"
+description = "Age secret key"
+regex = '''AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}'''
+
+[[rules]]
+id = "facebook-token"
+description = "Facebook token"
+regex = '''(?i)(facebook[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]'''
+
+[[rules]]
+id = "twitter-token"
+description = "Twitter token"
+regex = '''(?i)(twitter[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{35,44})['\"]'''
+
+[[rules]]
+id = "adobe-client-id"
+description = "Adobe Client ID (Oauth Web)"
+regex = '''(?i)(adobe[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]'''
+
+[[rules]]
+id = "adobe-client-secret"
+description = "Adobe Client Secret"
+regex = '''(p8e-)(?i)[a-z0-9]{32}'''
+
+[[rules]]
+id = "alibaba-access-key-id"
+description = "Alibaba AccessKey ID"
+regex = '''(LTAI)(?i)[a-z0-9]{20}'''
+
 [[rules]]
-    description = "AWS Access Key"
-    regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-    tags = ["key", "AWS"]
+id = "alibaba-secret-key"
+description = "Alibaba Secret Key"
+regex = '''(?i)(alibaba[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{30})['\"]'''
 
 [[rules]]
-    description = "AWS Secret Key"
-    regex = '''(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}'''
-    tags = ["key", "AWS"]
+id = "asana-client-id"
+description = "Asana Client ID"
+regex = '''(?i)(asana[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9]{16})['\"]'''
 
 [[rules]]
-    description = "AWS MWS key"
-    regex = '''amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'''
-    tags = ["key", "AWS", "MWS"]
+id = "asana-client-secret"
+description = "Asana Client Secret"
+regex = '''(?i)(asana[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{32})['\"]'''
 
 [[rules]]
-    description = "Facebook Secret Key"
-    regex = '''(?i)(facebook|fb)(.{0,20})?(?-i)['\"][0-9a-f]{32}['\"]'''
-    tags = ["key", "Facebook"]
+id = "atlassian-api-token"
+description = "Atlassian API token"
+regex = '''(?i)(atlassian[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{24})['\"]'''
 
 [[rules]]
-    description = "Facebook Client ID"
-    regex = '''(?i)(facebook|fb)(.{0,20})?['\"][0-9]{13,17}['\"]'''
-    tags = ["key", "Facebook"]
+id = "bitbucket-client-id"
+description = "Bitbucket client ID"
+regex = '''(?i)(bitbucket[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{32})['\"]'''
 
 [[rules]]
-    description = "Twitter Secret Key"
-    regex = '''(?i)twitter(.{0,20})?['\"][0-9a-z]{35,44}['\"]'''
-    tags = ["key", "Twitter"]
+description = "Bitbucket client secret"
+regex = '''(?i)(bitbucket[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9_\-]{64})['\"]'''
 
 [[rules]]
-    description = "Twitter Client ID"
-    regex = '''(?i)twitter(.{0,20})?['\"][0-9a-z]{18,25}['\"]'''
-    tags = ["client", "Twitter"]
+description = "Beamer API token"
+regex = '''(?i)(beamer[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](b_[a-z0-9=_\-]{44})['\"]'''
 
 [[rules]]
-    description = "Github Personal Access Token"
-    regex = '''ghp_[0-9a-zA-Z]{36}'''
-    tags = ["key", "Github"]
+description = "Clojars API token"
+regex = '''(CLOJARS_)(?i)[a-z0-9]{60}'''
+
+[[rules]]
+description = "Contentful delivery API token"
+regex = '''(?i)(contentful[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9\-=_]{43})['\"]'''
+
+[[rules]]
+description = "Contentful preview API token"
+regex = '''(?i)(contentful[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9\-=_]{43})['\"]'''
+
+[[rules]]
+description = "Databricks API token"
+regex = '''dapi[a-h0-9]{32}'''
+
+[[rules]]
+description = "Discord API key"
+regex = '''(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{64})['\"]'''
+
+[[rules]]
+description = "Discord client ID"
+regex = '''(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9]{18})['\"]'''
+
+[[rules]]
+description = "Discord client secret"
+regex = '''(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9=_\-]{32})['\"]'''
+
+[[rules]]
+description = "Doppler API token"
+regex = '''['\"](dp\.pt\.)(?i)[a-z0-9]{43}['\"]'''
+
+[[rules]]
+description = "Dropbox API secret/key"
+regex = '''(?i)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{15})['\"]'''
+
+[[rules]]
+description = "Dropbox short lived API token"
+regex = '''(?i)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](sl\.[a-z0-9\-=_]{135})['\"]'''
+
+[[rules]]
+description = "Dropbox long lived API token"
+regex = '''(?i)(dropbox)(.{0,20})['\"](?i)[a-z0-9]{11}(AAAAAAAAAA)[a-z0-9-_=]{43}['\"]'''
+
+[[rules]]
+description = "Duffel API token"
+regex = '''['\"]duffel_(test|live)_(?i)[a-z0-9_-]{43}['\"]'''
+
+[[rules]]
+description = "Dynatrace API token"
+regex = '''['\"]dt0c01\.(?i)[a-z0-9]{24}\.[a-z0-9]{64}['\"]'''
+
+[[rules]]
+description = "EasyPost API token"
+regex = '''['\"]EZAK(?i)[a-z0-9]{54}['\"]'''
+
+[[rules]]
+description = "EasyPost test API token"
+regex = '''['\"]EZTK(?i)[a-z0-9]{54}['\"]'''
+
+[[rules]]
+description = "Fastly API token"
+regex = '''(?i)(fastly[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9\-=_]{32})['\"]'''
+
 [[rules]]
-    description = "Github OAuth Access Token"
-    regex = '''gho_[0-9a-zA-Z]{36}'''
-    tags = ["key", "Github"]
+description = "Finicity client secret"
+regex = '''(?i)(finicity[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{20})['\"]'''
+
+[[rules]]
+description = "Finicity API token"
+regex = '''(?i)(finicity[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]'''
+
+[[rules]]
+description = "Flutterweave public key"
+regex = '''FLWPUBK_TEST-(?i)[a-h0-9]{32}-X'''
+
+[[rules]]
+description = "Flutterweave secret key"
+regex = '''FLWSECK_TEST-(?i)[a-h0-9]{32}-X'''
+
+[[rules]]
+description = "Flutterweave encrypted key"
+regex = '''FLWSECK_TEST[a-h0-9]{12}'''
+
+[[rules]]
+description = "Frame.io API token"
+regex = '''fio-u-(?i)[a-z0-9-_=]{64}'''
+
+[[rules]]
+description = "GoCardless API token"
+regex = '''['\"]live_(?i)[a-z0-9-_=]{40}['\"]'''
+
+[[rules]]
+description = "Grafana API token"
+regex = '''['\"]eyJrIjoi(?i)[a-z0-9-_=]{72,92}['\"]'''
+
 [[rules]]
-    description = "Github App Token"
-    regex = '''(ghu|ghs)_[0-9a-zA-Z]{36}'''
-    tags = ["key", "Github"]
+description = "Hashicorp Terraform user/org API token"
+regex = '''['\"](?i)[a-z0-9]{14}\.atlasv1\.[a-z0-9-_=]{60,70}['\"]'''
+
 [[rules]]
-    description = "Github Refresh Token"
-    regex = '''ghr_[0-9a-zA-Z]{76}'''
-    tags = ["key", "Github"]
+description = "Hubspot API token"
+regex = '''(?i)(hubspot[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]'''
 
 [[rules]]
-    description = "LinkedIn Client ID"
-    regex = '''(?i)linkedin(.{0,20})?(?-i)[0-9a-z]{12}'''
-    tags = ["client", "LinkedIn"]
+description = "Intercom API token"
+regex = '''(?i)(intercom[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9=_]{60})['\"]'''
 
 [[rules]]
-    description = "LinkedIn Secret Key"
-    regex = '''(?i)linkedin(.{0,20})?[0-9a-z]{16}'''
-    tags = ["secret", "LinkedIn"]
+description = "Intercom client secret/ID"
+regex = '''(?i)(intercom[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]'''
 
 [[rules]]
-    description = "Slack"
-    regex = '''xox[baprs]-([0-9a-zA-Z]{10,48})?'''
-    tags = ["key", "Slack"]
+description = "Ionic API token"
+regex = '''ion_(?i)[a-z0-9]{42}'''
 
 [[rules]]
-    description = "Asymmetric Private Key"
-    regex = '''-----BEGIN ((EC|PGP|DSA|RSA|OPENSSH) )?PRIVATE KEY( BLOCK)?-----'''
-    tags = ["key", "AsymmetricPrivateKey"]
+description = "Linear API token"
+regex = '''lin_api_(?i)[a-z0-9]{40}'''
 
 [[rules]]
-    description = "Google API key"
-    regex = '''AIza[0-9A-Za-z\-_]{35}'''
-    tags = ["key", "Google"]
+description = "Linear client secret/ID"
+regex = '''(?i)(linear[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]'''
 
 [[rules]]
-    description = "Google (GCP) Service Account"
-    regex = '''"type": "service_account"'''
-    tags = ["key", "Google"]
+description = "Lob API Key"
+regex = '''(?i)(lob[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]((live|test)_[a-f0-9]{35})['\"]'''
 
 [[rules]]
-    description = "Heroku API key"
-    regex = '''(?i)heroku(.{0,20})?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'''
-    tags = ["key", "Heroku"]
+description = "Lob Publishable API Key"
+regex = '''(?i)(lob[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]((test|live)_pub_[a-f0-9]{31})['\"]'''
 
 [[rules]]
-    description = "MailChimp API key"
-    regex = '''(?i)(mailchimp|mc)(.{0,20})?[0-9a-f]{32}-us[0-9]{1,2}'''
-    tags = ["key", "Mailchimp"]
+description = "Mailchimp API key"
+regex = '''(?i)(mailchimp[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32}-us20)['\"]'''
 
 [[rules]]
-    description = "Mailgun API key"
-    regex = '''((?i)(mailgun|mg)(.{0,20})?)?key-[0-9a-z]{32}'''
-    tags = ["key", "Mailgun"]
+description = "Mailgun private API token"
+regex = '''(?i)(mailgun[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](key-[a-f0-9]{32})['\"]'''
 
 [[rules]]
-    description = "PayPal Braintree access token"
-    regex = '''access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}'''
-    tags = ["key", "Paypal"]
+description = "Mailgun public validation key"
+regex = '''(?i)(mailgun[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](pubkey-[a-f0-9]{32})['\"]'''
 
 [[rules]]
-    description = "Picatic API key"
-    regex = '''sk_live_[0-9a-z]{32}'''
-    tags = ["key", "Picatic"]
+description = "Mailgun webhook signing key"
+regex = '''(?i)(mailgun[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{32}-[a-h0-9]{8}-[a-h0-9]{8})['\"]'''
 
 [[rules]]
-    description = "SendGrid API Key"
-    regex = '''SG\.[\w_]{16,32}\.[\w_]{16,64}'''
-    tags = ["key", "SendGrid"]
+description = "Mapbox API token"
+regex = '''(?i)(pk\.[a-z0-9]{60}\.[a-z0-9]{22})'''
 
 [[rules]]
-    description = "Slack Webhook"
-    regex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8,12}/[a-zA-Z0-9_]{24}'''
-    tags = ["key", "slack"]
+description = "MessageBird API token"
+regex = '''(?i)(messagebird[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{25})['\"]'''
 
 [[rules]]
-    description = "Stripe API key"
-    regex = '''(?i)stripe(.{0,20})?[sr]k_live_[0-9a-zA-Z]{24}'''
-    tags = ["key", "Stripe"]
+description = "MessageBird API client ID"
+regex = '''(?i)(messagebird[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]'''
 
 [[rules]]
-    description = "Square access token"
-    regex = '''sq0atp-[0-9A-Za-z\-_]{22}'''
-    tags = ["key", "square"]
+description = "New Relic user API Key"
+regex = '''['\"](NRAK-[A-Z0-9]{27})['\"]'''
 
 [[rules]]
-    description = "Square OAuth secret"
-    regex = '''sq0csp-[0-9A-Za-z\-_]{43}'''
-    tags = ["key", "square"]
+description = "New Relic user API ID"
+regex = '''(?i)(newrelic[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([A-Z0-9]{64})['\"]'''
 
 [[rules]]
-    description = "Twilio API key"
-    regex = '''(?i)twilio(.{0,20})?SK[0-9a-f]{32}'''
-    tags = ["key", "twilio"]
+description = "New Relic ingest browser API token"
+regex = '''['\"](NRJS-[a-f0-9]{19})['\"]'''
 
 [[rules]]
-    description = "Dynatrace ttoken"
-    regex = '''dt0[a-zA-Z]{1}[0-9]{2}\.[A-Z0-9]{24}\.[A-Z0-9]{64}'''
-    tags = ["key", "Dynatrace"]
+description = "npm access token"
+regex = '''['\"](npm_(?i)[a-z0-9]{36})['\"]'''
 
 [[rules]]
-    description = "Shopify shared secret"
-    regex = '''shpss_[a-fA-F0-9]{32}'''
-    tags = ["key", "Shopify"]
+description = "Planetscale password"
+regex = '''pscale_pw_(?i)[a-z0-9\-_\.]{43}'''
 
 [[rules]]
-    description = "Shopify access token"
-    regex = '''shpat_[a-fA-F0-9]{32}'''
-    tags = ["key", "Shopify"]
+description = "Planetscale API token"
+regex = '''pscale_tkn_(?i)[a-z0-9\-_\.]{43}'''
 
 [[rules]]
-    description = "Shopify custom app access token"
-    regex = '''shpca_[a-fA-F0-9]{32}'''
-    tags = ["key", "Shopify"]
+description = "Postman API token"
+regex = '''PMAK-(?i)[a-f0-9]{24}\-[a-f0-9]{34}'''
 
 [[rules]]
-    description = "Shopify private app access token"
-    regex = '''shppa_[a-fA-F0-9]{32}'''
-    tags = ["key", "Shopify"]
+description = "Pulumi API token"
+regex = '''pul-[a-f0-9]{40}'''
 
 [[rules]]
-    description = "PyPI upload token"
-    regex = '''pypi-AgEIcHlwaS5vcmc[A-Za-z0-9-_]{50,1000}'''
-    tags = ["key", "pypi"]
+description = "Rubygem API token"
+regex = '''rubygems_[a-f0-9]{48}'''
+
+[[rules]]
+description = "Sendgrid API token"
+regex = '''SG\.(?i)[a-z0-9_\-\.]{66}'''
+
+[[rules]]
+description = "Sendinblue API token"
+regex = '''xkeysib-[a-f0-9]{64}\-(?i)[a-z0-9]{16}'''
+
+[[rules]]
+description = "Shippo API token"
+regex = '''shippo_(live|test)_[a-f0-9]{40}'''
+
+[[rules]]
+description = "Linkedin Client secret"
+regex = '''(?i)(linkedin[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z]{16})['\"]'''
+
+[[rules]]
+description = "Linkedin Client ID"
+regex = '''(?i)(linkedin[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{14})['\"]'''
+
+[[rules]]
+description = "Twitch API token"
+regex = '''(?i)(twitch[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{30})['\"]'''
+
+[[rules]]
+description = "Typeform API token"
+regex = '''(?i)(typeform[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}(tfp_[a-z0-9\-_\.=]{59})'''
+
 
 [allowlist]
-    description = "Allowlisted files"
-    files = ['''^\.?gitleaks.toml$''',
-    '''(.*?)(png|jpg|gif|doc|docx|pdf|bin|xls|pyc|zip)$''',
-    '''(go.mod|go.sum)$''']
+description = "global allow lists"
+regexes = ['''219-09-9999''', '''078-05-1120''', '''(9[0-9]{2}|666)-\d{2}-\d{4}''']
+files = ['''(.*?)(jpg|gif|doc|pdf|bin|svg|socket)$''']

+ 35 - 173
config/rule.go

@@ -1,189 +1,51 @@
 package config
 
 import (
-	"math"
-	"path/filepath"
 	"regexp"
+	"strings"
 )
 
-// Offender is a struct that contains the information matched when searching
-// content and information on why it matched (i.e. the EntropyLevel)
-type Offender struct {
-	Match        string
-	EntropyLevel float64
-}
-
-// IsEmpty checks to see if nothing was found in the match
-func (o *Offender) IsEmpty() bool {
-	return o.Match == ""
-}
-
-// ToString the contents of the match
-func (o *Offender) ToString() string {
-	return o.Match
-}
-
-// Rule is a struct that contains information that is loaded from a gitleaks config.
-// This struct is used in the Config struct as an array of Rules and is iterated
-// over during an scan. Each rule will be checked. If a regex match is found AND
-// that match is not allowlisted (globally or locally), then a leak will be appended
-// to the final scan report.
 type Rule struct {
-	Description string
-	Regex       *regexp.Regexp
-	File        *regexp.Regexp
-	Path        *regexp.Regexp
-	ReportGroup int
-	Tags        []string
-	AllowList   AllowList
-	Entropies   []Entropy
-}
-
-// Inspect checks the content of a line for a leak
-func (r *Rule) Inspect(line string) *Offender {
-	match := r.Regex.FindString(line)
-
-	// EntropyLevel -1 means not checked
-	if match == "" {
-		return &Offender{
-			Match:        "",
-			EntropyLevel: -1,
-		}
-	}
-
-	// check if offender is allowed
-	// EntropyLevel -1 means not checked
-	if r.RegexAllowed(line) {
-		return &Offender{
-			Match:        "",
-			EntropyLevel: -1,
-		}
-	}
-
-	// check entropy
-	groups := r.Regex.FindStringSubmatch(match)
-	entropyWithinRange, entropyLevel := r.CheckEntropy(groups)
-
-	if len(r.Entropies) != 0 && !entropyWithinRange {
-		return &Offender{
-			Match:        "",
-			EntropyLevel: entropyLevel,
+	Description    string
+	RuleID         string
+	Entropy        float64
+	EntropyReGroup int
+	Regex          *regexp.Regexp
+	Path           *regexp.Regexp
+	Tags           []string
+	Allowlist      Allowlist
+}
+
+func (r *Rule) IncludeEntropy(secret string) (bool, float64) {
+	groups := r.Regex.FindStringSubmatch(secret)
+	if len(groups)-1 > r.EntropyReGroup || len(groups) == 0 {
+		// Config validation should prevent this
+		return false, 0.0
+	}
+
+	// NOTE: this is a goofy hack to get around the fact there golang's regex engine
+	// does not support positive lookaheads. Ideally we would want to add a
+	// restriction on generic rules regex that requires the secret match group
+	// contains both numbers and alphabetical characters. What this bit of code does is
+	// check if the ruleid is prepended with "generic" and enforces the
+	// secret contains both digits and alphabetical characters.
+	if strings.HasPrefix(r.RuleID, "generic") {
+		if !containsDigit(groups[r.EntropyReGroup]) {
+			return false, 0.0
 		}
 	}
-
-	// 0 is a match for the full regex pattern
-	if 0 < r.ReportGroup && r.ReportGroup < len(groups) {
-		match = groups[r.ReportGroup]
+	// group = 0 will check the entropy of the whole regex match
+	e := shannonEntropy(groups[r.EntropyReGroup])
+	if e > r.Entropy {
+		return true, e
 	}
 
-	return &Offender{
-		Match:        match,
-		EntropyLevel: entropyLevel,
-	}
-}
-
-// RegexAllowed checks if the content is allowlisted
-func (r *Rule) RegexAllowed(content string) bool {
-	return anyRegexMatch(content, r.AllowList.Regexes)
-}
-
-// CommitAllowed checks if a commit is allowlisted
-func (r *Rule) CommitAllowed(commit string) bool {
-	return r.AllowList.CommitAllowed(commit)
+	return false, e
 }
 
-// CheckEntropy checks if there is an entropy leak
-func (r *Rule) CheckEntropy(groups []string) (bool, float64) {
-	var highestFound float64 = 0
-
-	for _, e := range r.Entropies {
-		if len(groups) > e.Group {
-			entropy := shannonEntropy(groups[e.Group])
-			if entropy >= e.Min && entropy <= e.Max {
-				return true, entropy
-			} else if entropy > highestFound {
-				highestFound = entropy
-			}
-		}
-	}
-
-	if len(r.Entropies) == 0 {
-		// entropies not checked
-		return false, -1
-	}
-
-	// entropies checked but not within the range
-	return false, highestFound
-}
-
-// HasFileOrPathLeakOnly first checks if there are no entropy/regex rules, then checks if
-// there are any file/path leaks
-func (r *Rule) HasFileOrPathLeakOnly(filePath string) bool {
-	if r.Regex.String() != "" {
-		return false
-	}
-	if len(r.Entropies) != 0 {
+func (r *Rule) EntropySet() bool {
+	if r.Entropy == 0.0 {
 		return false
 	}
-	if r.AllowList.FileAllowed(filepath.Base(filePath)) || r.AllowList.PathAllowed(filePath) {
-		return false
-	}
-	return r.HasFileLeak(filepath.Base(filePath)) || r.HasFilePathLeak(filePath)
-}
-
-// HasFileLeak checks if there is a file leak
-func (r *Rule) HasFileLeak(fileName string) bool {
-	return regexMatched(fileName, r.File)
-}
-
-// HasFilePathLeak checks if there is a path leak
-func (r *Rule) HasFilePathLeak(filePath string) bool {
-	return regexMatched(filePath, r.Path)
-}
-
-// shannonEntropy calculates the entropy of data using the formula defined here:
-// https://en.wiktionary.org/wiki/Shannon_entropy
-// Another way to think about what this is doing is calculating the number of bits
-// needed to on average encode the data. So, the higher the entropy, the more random the data, the
-// more bits needed to encode that data.
-func shannonEntropy(data string) (entropy float64) {
-	if data == "" {
-		return 0
-	}
-
-	charCounts := make(map[rune]int)
-	for _, char := range data {
-		charCounts[char]++
-	}
-
-	invLength := 1.0 / float64(len(data))
-	for _, count := range charCounts {
-		freq := float64(count) * invLength
-		entropy -= freq * math.Log2(freq)
-	}
-
-	return entropy
-}
-
-// regexMatched matched an interface to a regular expression. The interface f can
-// be a string type or go-git *object.File type.
-func regexMatched(f string, re *regexp.Regexp) bool {
-	if re == nil {
-		return false
-	}
-	if re.FindString(f) != "" {
-		return true
-	}
-	return false
-}
-
-// anyRegexMatch matched an interface to a regular expression. The interface f can
-// be a string type or go-git *object.File type.
-func anyRegexMatch(f string, res []*regexp.Regexp) bool {
-	for _, re := range res {
-		if regexMatched(f, re) {
-			return true
-		}
-	}
-	return false
+	return true
 }

+ 60 - 0
config/utils.go

@@ -0,0 +1,60 @@
+package config
+
+import (
+	"math"
+	"regexp"
+)
+
+func anyRegexMatch(f string, res []*regexp.Regexp) bool {
+	for _, re := range res {
+		if regexMatched(f, re) {
+			return true
+		}
+	}
+	return false
+}
+
+func regexMatched(f string, re *regexp.Regexp) bool {
+	if re == nil {
+		return false
+	}
+	if re.FindString(f) != "" {
+		return true
+	}
+	return false
+}
+
+func containsDigit(s string) bool {
+	for _, c := range s {
+		switch c {
+		case '1', '2', '3', '4', '5', '6', '7', '8', '9':
+			return true
+		}
+
+	}
+	return false
+}
+
+// shannonEntropy calculates the entropy of data using the formula defined here:
+// https://en.wiktionary.org/wiki/Shannon_entropy
+// Another way to think about what this is doing is calculating the number of bits
+// needed to on average encode the data. So, the higher the entropy, the more random the data, the
+// more bits needed to encode that data.
+func shannonEntropy(data string) (entropy float64) {
+	if data == "" {
+		return 0
+	}
+
+	charCounts := make(map[rune]int)
+	for _, char := range data {
+		charCounts[char]++
+	}
+
+	invLength := 1.0 / float64(len(data))
+	for _, count := range charCounts {
+		freq := float64(count) * invLength
+		entropy -= freq * math.Log2(freq)
+	}
+
+	return entropy
+}

+ 105 - 0
detect/detect.go

@@ -0,0 +1,105 @@
+package detect
+
+import (
+	"encoding/json"
+	"fmt"
+	"regexp"
+	"strings"
+
+	"github.com/zricethezav/gitleaks/v8/config"
+	"github.com/zricethezav/gitleaks/v8/report"
+)
+
+type Options struct {
+	Verbose bool
+	Redact  bool
+}
+
+func DetectFindings(cfg config.Config, b []byte, filePath string, commit string) []report.Finding {
+	var findings []report.Finding
+	linePairs := regexp.MustCompile("\n").FindAllIndex(b, -1)
+
+	// check if we should skip file based on the global allowlist
+	if cfg.Allowlist.PathAllowed(filePath) {
+		return findings
+	}
+
+	for _, r := range cfg.Rules {
+		pathSkip := false
+		if r.Allowlist.CommitAllowed(commit) {
+			continue
+		}
+		if r.Allowlist.PathAllowed(filePath) {
+			continue
+		}
+
+		// Check if path should be considered
+		if r.Path != nil {
+			if r.Path.Match([]byte(filePath)) {
+				if r.Regex == nil {
+					// This is a path only rule
+					f := report.Finding{
+						Description: r.Description,
+						File:        filePath,
+						RuleID:      r.RuleID,
+						Context:     fmt.Sprintf("file detected: %s", filePath),
+						Tags:        r.Tags,
+					}
+					findings = append(findings, f)
+					pathSkip = true
+				}
+			} else {
+				pathSkip = true
+			}
+		}
+		if pathSkip {
+			continue
+		}
+
+		matchIndices := r.Regex.FindAllIndex(b, -1)
+		for _, m := range matchIndices {
+			location := getLocation(linePairs, m[0], m[1])
+			f := report.Finding{
+				Description: r.Description,
+				File:        filePath,
+				RuleID:      r.RuleID,
+				StartLine:   location.startLine,
+				EndLine:     location.endLine,
+				StartColumn: location.startColumn,
+				EndColumn:   location.endColumn,
+				Secret:      strings.Trim(string(b[m[0]:m[1]]), "\n"),
+				Context:     limit(strings.Trim(string(b[location.startLineIndex:location.endLineIndex]), "\n")),
+				Tags:        r.Tags,
+			}
+
+			if r.Allowlist.RegexAllowed(f.Secret) {
+				continue
+			}
+
+			if r.EntropySet() {
+				include, entropy := r.IncludeEntropy(strings.Trim(string(b[m[0]:m[1]]), "\n"))
+				if include {
+					f.Entropy = float32(entropy)
+					findings = append(findings, f)
+				}
+			} else {
+				findings = append(findings, f)
+			}
+		}
+	}
+
+	return findings
+}
+
+func limit(s string) string {
+	if len(s) > 500 {
+		return s[:500] + "..."
+	}
+	return s
+}
+
+func printFinding(f report.Finding) {
+	var b []byte
+	b, _ = json.MarshalIndent(f, "", "	")
+	fmt.Println(string(b))
+}

+ 143 - 0
detect/detect_test.go

@@ -0,0 +1,143 @@
+package detect
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/spf13/viper"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/zricethezav/gitleaks/v8/config"
+	"github.com/zricethezav/gitleaks/v8/report"
+)
+
+func TestDetectFindings(t *testing.T) {
+	tests := []struct {
+		cfgName          string
+		opts             Options
+		filePath         string
+		bytes            []byte
+		commit           string
+		expectedFindings []report.Finding
+		wantError        error
+	}{
+		{
+			cfgName:  "simple",
+			bytes:    []byte(`awsToken := \"AKIALALEMEL33243OLIA\"`),
+			filePath: "tmp.go",
+			expectedFindings: []report.Finding{
+				{
+					Description: "AWS Access Key",
+					Secret:      "AKIALALEMEL33243OLIA",
+					File:        "tmp.go",
+					RuleID:      "aws-access-key",
+					Tags:        []string{"key", "AWS"},
+				},
+			},
+		},
+		{
+			cfgName:          "allow_aws_re",
+			bytes:            []byte(`awsToken := \"AKIALALEMEL33243OLIA\"`),
+			filePath:         "tmp.go",
+			expectedFindings: []report.Finding{},
+		},
+		{
+			cfgName:          "allow_path",
+			bytes:            []byte(`awsToken := \"AKIALALEMEL33243OLIA\"`),
+			filePath:         "tmp.go",
+			expectedFindings: []report.Finding{},
+		},
+		{
+			cfgName:          "allow_commit",
+			bytes:            []byte(`awsToken := \"AKIALALEMEL33243OLIA\"`),
+			filePath:         "tmp.go",
+			expectedFindings: []report.Finding{},
+			commit:           "allowthiscommit",
+		},
+		{
+			cfgName:  "entropy_group",
+			bytes:    []byte(`const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`),
+			filePath: "tmp.go",
+			expectedFindings: []report.Finding{
+				{
+					Description: "Discord API key",
+					Secret:      "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
+					File:        "tmp.go",
+					RuleID:      "discord-api-key",
+					Tags:        []string{},
+					Entropy:     3.7906237,
+				},
+			},
+		},
+		{
+			cfgName:          "generic_with_py_path",
+			bytes:            []byte(`const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`),
+			filePath:         "tmp.go",
+			expectedFindings: []report.Finding{},
+		},
+		{
+			cfgName:  "generic_with_py_path",
+			bytes:    []byte(`const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`),
+			filePath: "tmp.py",
+			expectedFindings: []report.Finding{
+				{
+					Description: "Generic API Key",
+					Secret:      "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
+					File:        "tmp.py",
+					RuleID:      "generic-api-key",
+					Tags:        []string{},
+					Entropy:     3.7906237,
+				},
+			},
+		},
+		{
+			cfgName:  "path_only",
+			bytes:    []byte(`const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`),
+			filePath: "tmp.py",
+			expectedFindings: []report.Finding{
+				{
+					Description: "Python Files",
+					Context:     "file detected: tmp.py",
+					File:        "tmp.py",
+					RuleID:      "python-files-only",
+					Tags:        []string{},
+				},
+			},
+		},
+		{
+			cfgName:          "bad_entropy_group",
+			bytes:            []byte(`const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`),
+			filePath:         "tmp.go",
+			expectedFindings: []report.Finding{},
+			wantError:        fmt.Errorf("Discord API key invalid regex entropy group 5, max regex entropy group 3"),
+		},
+	}
+
+	for _, tt := range tests {
+		viper.Reset()
+		viper.AddConfigPath(configPath)
+		viper.SetConfigName(tt.cfgName)
+		viper.SetConfigType("toml")
+		err := viper.ReadInConfig()
+		if err != nil {
+			t.Error(err)
+		}
+
+		var vc config.ViperConfig
+		viper.Unmarshal(&vc)
+		cfg, err := vc.Translate()
+		if tt.wantError != nil {
+			if err == nil {
+				t.Errorf("expected error")
+			}
+			assert.Equal(t, tt.wantError, err)
+		}
+
+		findings := DetectFindings(cfg, tt.bytes, tt.filePath, tt.commit)
+		for _, f := range findings {
+			f.Context = "" // remove lines cause copying and pasting them has some wack formatting
+			f.Date = ""
+		}
+		assert.ElementsMatch(t, tt.expectedFindings, findings)
+	}
+}

+ 74 - 0
detect/files.go

@@ -0,0 +1,74 @@
+package detect
+
+import (
+	"context"
+	"os"
+	"path/filepath"
+	"sync"
+
+	"golang.org/x/sync/errgroup"
+	godocutil "golang.org/x/tools/godoc/util"
+
+	"github.com/zricethezav/gitleaks/v8/config"
+	"github.com/zricethezav/gitleaks/v8/report"
+)
+
+// FromFiles opens the directory or file specified in source and checks each file against the rules
+// from the configuration. If any secrets are found, they are added to the list of findings.
+func FromFiles(source string, cfg config.Config, outputOptions Options) ([]*report.Finding, error) {
+	var (
+		findings []*report.Finding
+		mu       sync.Mutex
+	)
+	g, _ := errgroup.WithContext(context.Background())
+	paths := make(chan string)
+	g.Go(func() error {
+		defer close(paths)
+		return filepath.Walk(source,
+			func(path string, fInfo os.FileInfo, err error) error {
+				if err != nil {
+					return err
+				}
+				if fInfo.Name() == ".git" {
+					return filepath.SkipDir
+				}
+				if fInfo.Mode().IsRegular() {
+					paths <- path
+				}
+				return nil
+			})
+	})
+	for pa := range paths {
+		p := pa
+		g.Go(func() error {
+			b, err := os.ReadFile(p)
+			if err != nil {
+				return err
+			}
+
+			if !godocutil.IsText(b) {
+				return nil
+			}
+			fis := DetectFindings(cfg, b, p, "")
+			for _, fi := range fis {
+				fi.File = p
+				if outputOptions.Redact {
+					fi.Redact()
+				}
+				if outputOptions.Verbose {
+					printFinding(fi)
+				}
+				mu.Lock()
+				findings = append(findings, &fi)
+				mu.Unlock()
+			}
+			return nil
+		})
+	}
+
+	if err := g.Wait(); err != nil {
+		return findings, err
+	}
+
+	return findings, nil
+}

+ 80 - 0
detect/files_test.go

@@ -0,0 +1,80 @@
+package detect
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/spf13/viper"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/zricethezav/gitleaks/v8/config"
+	"github.com/zricethezav/gitleaks/v8/report"
+)
+
+// TestFromGit tests the FromGit function
+func TestFromFiles(t *testing.T) {
+	tests := []struct {
+		cfgName          string
+		opts             Options
+		source           string
+		expectedFindings []*report.Finding
+	}{
+		{
+			source:  filepath.Join(repoBasePath, "nogit"),
+			cfgName: "simple",
+			expectedFindings: []*report.Finding{
+				{
+					Description: "AWS Access Key",
+					StartLine:   19,
+					EndLine:     19,
+					StartColumn: 16,
+					EndColumn:   35,
+					Context:     "\tawsToken := \"AKIALALEMEL33243OLIA\"",
+					Secret:      "AKIALALEMEL33243OLIA",
+					File:        "../testdata/repos/nogit/main.go",
+					RuleID:      "aws-access-key",
+					Tags:        []string{"key", "AWS"},
+				},
+			},
+		},
+		{
+			source:  filepath.Join(repoBasePath, "nogit", "main.go"),
+			cfgName: "simple",
+			expectedFindings: []*report.Finding{
+				{
+					Description: "AWS Access Key",
+					StartLine:   19,
+					EndLine:     19,
+					StartColumn: 16,
+					EndColumn:   35,
+					Context:     "\tawsToken := \"AKIALALEMEL33243OLIA\"",
+					Secret:      "AKIALALEMEL33243OLIA",
+					File:        "../testdata/repos/nogit/main.go",
+					RuleID:      "aws-access-key",
+					Tags:        []string{"key", "AWS"},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		viper.AddConfigPath(configPath)
+		viper.SetConfigName("simple")
+		viper.SetConfigType("toml")
+		err := viper.ReadInConfig()
+		if err != nil {
+			t.Error(err)
+		}
+
+		var vc config.ViperConfig
+		viper.Unmarshal(&vc)
+		cfg, _ := vc.Translate()
+
+		findings, err := FromFiles(tt.source, cfg, tt.opts)
+		if err != nil {
+			t.Error(err)
+		}
+
+		assert.ElementsMatch(t, tt.expectedFindings, findings)
+	}
+}

+ 89 - 0
detect/git.go

@@ -0,0 +1,89 @@
+package detect
+
+import (
+	"strings"
+	"sync"
+
+	"github.com/gitleaks/go-gitdiff/gitdiff"
+	"github.com/zricethezav/gitleaks/v8/config"
+	"github.com/zricethezav/gitleaks/v8/report"
+	godocutil "golang.org/x/tools/godoc/util"
+)
+
+// FromGit accepts a gitdiff.File channel (structure output from `git log -p`) and a configuration
+// struct. Files from the gitdiff.File channel are then checked against each rule in the configuration to
+// check for secrets. If any secrets are found, they are added to the list of findings.
+func FromGit(files <-chan *gitdiff.File, cfg config.Config, outputOptions Options) []*report.Finding {
+	var findings []*report.Finding
+	mu := sync.Mutex{}
+	wg := sync.WaitGroup{}
+	for f := range files {
+
+		wg.Add(1)
+		go func(f *gitdiff.File) {
+			defer wg.Done()
+			if f.IsBinary {
+				return
+			}
+
+			if f.IsDelete {
+				return
+			}
+
+			commitSHA := ""
+
+			// Check if commit is allowed
+			if f.PatchHeader != nil {
+				commitSHA = f.PatchHeader.SHA
+
+				if cfg.Allowlist.CommitAllowed(f.PatchHeader.SHA) {
+					return
+				}
+			}
+
+			for _, tf := range f.TextFragments {
+				if f.TextFragments == nil {
+					// TODO fix this in gitleaks gitdiff fork
+					// https://github.com/gitleaks/gitleaks/issues/11
+					continue
+				}
+
+				if !godocutil.IsText([]byte(tf.Raw(gitdiff.OpAdd))) {
+					continue
+				}
+
+				for _, fi := range DetectFindings(cfg, []byte(tf.Raw(gitdiff.OpAdd)), f.NewName, commitSHA) {
+					// don't add to start/end lines if finding is from a file only rule
+					if !strings.HasPrefix(fi.Context, "file detected") {
+						fi.StartLine += int(tf.NewPosition)
+						fi.EndLine += int(tf.NewPosition)
+					}
+					if f.PatchHeader != nil {
+						fi.Commit = f.PatchHeader.SHA
+						fi.Message = f.PatchHeader.Message()
+						if f.PatchHeader.Author != nil {
+							fi.Author = f.PatchHeader.Author.Name
+							fi.Email = f.PatchHeader.Author.Email
+						}
+						fi.Date = f.PatchHeader.AuthorDate.String()
+					}
+
+					if outputOptions.Redact {
+						fi.Redact()
+					}
+
+					if outputOptions.Verbose {
+						printFinding(fi)
+					}
+					mu.Lock()
+					findings = append(findings, &fi)
+					mu.Unlock()
+
+				}
+			}
+		}(f)
+	}
+
+	wg.Wait()
+	return findings
+}

+ 158 - 0
detect/git_test.go

@@ -0,0 +1,158 @@
+package detect
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/spf13/viper"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/zricethezav/gitleaks/v8/config"
+	"github.com/zricethezav/gitleaks/v8/git"
+	"github.com/zricethezav/gitleaks/v8/report"
+)
+
+const repoBasePath = "../testdata/repos/"
+const expectPath = "../testdata/expected/"
+const configPath = "../testdata/config/"
+
+// TestFromGit tests the FromGit function
+func TestFromGit(t *testing.T) {
+	tests := []struct {
+		cfgName          string
+		opts             Options
+		source           string
+		logOpts          string
+		expected         string
+		expectedFindings []*report.Finding
+	}{
+		{
+			source:   filepath.Join(repoBasePath, "small"),
+			expected: filepath.Join(expectPath, "git", "small.txt"),
+			cfgName:  "simple",
+			expectedFindings: []*report.Finding{
+				{
+					Description: "AWS Access Key",
+					StartLine:   20,
+					EndLine:     20,
+					StartColumn: 19,
+					EndColumn:   38,
+					Secret:      "AKIALALEMEL33243OLIA",
+					File:        "main.go",
+					// Line:        "\tawsToken := \"AKIALALEMEL33243OLIA\"",
+					Commit:  "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587",
+					Author:  "Zachary Rice",
+					Email:   "zricer@protonmail.com",
+					Message: "Accidentally add a secret",
+					RuleID:  "aws-access-key",
+					Tags:    []string{"key", "AWS"},
+				},
+				{
+					Description: "AWS Access Key",
+					StartLine:   9,
+					EndLine:     9,
+					StartColumn: 17,
+					EndColumn:   36,
+					Secret:      "AKIALALEMEL33243OLIA",
+					File:        "foo/foo.go",
+					// Line:        "\taws_token := \"AKIALALEMEL33243OLIA\"",
+					Commit:  "491504d5a31946ce75e22554cc34203d8e5ff3ca",
+					Author:  "Zach Rice",
+					Email:   "zricer@protonmail.com",
+					Message: "adding foo package with secret",
+					RuleID:  "aws-access-key",
+					Tags:    []string{"key", "AWS"},
+				},
+			},
+		},
+		{
+			source:   filepath.Join(repoBasePath, "small"),
+			expected: filepath.Join(expectPath, "git", "small-branch-foo.txt"),
+			logOpts:  "--all foo...",
+			cfgName:  "simple",
+			expectedFindings: []*report.Finding{
+				{
+					Description: "AWS Access Key",
+					StartLine:   9,
+					EndLine:     9,
+					StartColumn: 17,
+					EndColumn:   36,
+					Secret:      "AKIALALEMEL33243OLIA",
+					// Line:        "\taws_token := \"AKIALALEMEL33243OLIA\"",
+					File:    "foo/foo.go",
+					Commit:  "491504d5a31946ce75e22554cc34203d8e5ff3ca",
+					Author:  "Zach Rice",
+					Email:   "zricer@protonmail.com",
+					Message: "adding foo package with secret",
+					RuleID:  "aws-access-key",
+					Tags:    []string{"key", "AWS"},
+				},
+			},
+		},
+	}
+
+	err := moveDotGit("dotGit", ".git")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer moveDotGit(".git", "dotGit")
+
+	for _, tt := range tests {
+		files, err := git.GitLog(tt.source, tt.logOpts)
+		if err != nil {
+			t.Error(err)
+		}
+
+		viper.AddConfigPath(configPath)
+		viper.SetConfigName("simple")
+		viper.SetConfigType("toml")
+		err = viper.ReadInConfig()
+		if err != nil {
+			t.Error(err)
+		}
+
+		var vc config.ViperConfig
+		viper.Unmarshal(&vc)
+		cfg, _ := vc.Translate()
+
+		findings := FromGit(files, cfg, tt.opts)
+		for _, f := range findings {
+			f.Context = "" // remove lines cause copying and pasting them has some wack formatting
+			f.Date = ""
+		}
+		assert.ElementsMatch(t, tt.expectedFindings, findings)
+	}
+}
+
+func moveDotGit(from, to string) error {
+	repoDirs, err := os.ReadDir("../testdata/repos")
+	if err != nil {
+		return err
+	}
+	for _, dir := range repoDirs {
+		if to == ".git" {
+			_, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), "dotGit"))
+			if os.IsNotExist(err) {
+				// dont want to delete the only copy of .git accidentally
+				continue
+			}
+			os.RemoveAll(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), ".git"))
+		}
+		if !dir.IsDir() {
+			continue
+		}
+		_, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from))
+		if os.IsNotExist(err) {
+			continue
+		}
+
+		err = os.Rename(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from),
+			fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), to))
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}

+ 38 - 0
detect/location.go

@@ -0,0 +1,38 @@
+package detect
+
+// Location represents a location in a file
+type Location struct {
+	startLine      int
+	endLine        int
+	startColumn    int
+	endColumn      int
+	startLineIndex int
+	endLineIndex   int
+}
+
+func getLocation(linePairs [][]int, start int, end int) Location {
+	var (
+		prevNewLine int
+		location    Location
+	)
+
+	for lineNum, pair := range linePairs {
+		newLineByteIndex := pair[0]
+		if prevNewLine <= start && start < newLineByteIndex {
+			location.startLine = lineNum
+			location.endLine = lineNum
+			location.startColumn = (start - prevNewLine) + 1 // +1 because counting starts at 1
+			location.startLineIndex = prevNewLine
+			location.endLineIndex = newLineByteIndex
+		}
+		if prevNewLine < end && end <= newLineByteIndex {
+			location.endLine = lineNum
+			location.endColumn = (end - prevNewLine)
+			location.endLineIndex = newLineByteIndex
+		}
+
+		prevNewLine = pair[0]
+	}
+
+	return location
+}

+ 60 - 0
detect/location_test.go

@@ -0,0 +1,60 @@
+package detect
+
+import (
+	"testing"
+)
+
+// TestGetLocation tests the getLocation function.
+func TestGetLocation(t *testing.T) {
+	tests := []struct {
+		linePairs    [][]int
+		start        int
+		end          int
+		wantLocation Location
+	}{
+		{
+			linePairs: [][]int{
+				{0, 39},
+				{40, 55},
+				{56, 57},
+			},
+			start: 35,
+			end:   38,
+			wantLocation: Location{
+				startLine:      1,
+				startColumn:    36,
+				endLine:        1,
+				endColumn:      38,
+				startLineIndex: 0,
+				endLineIndex:   40,
+			},
+		},
+		{
+			linePairs: [][]int{
+				{0, 39},
+				{40, 55},
+				{56, 57},
+			},
+			start: 40,
+			end:   44,
+			wantLocation: Location{
+				startLine:      2,
+				startColumn:    1,
+				endLine:        2,
+				endColumn:      4,
+				startLineIndex: 40,
+				endLineIndex:   56,
+			},
+		},
+	}
+
+	for _, test := range tests {
+		loc := getLocation(test.linePairs, test.start, test.end)
+		if loc != test.wantLocation {
+			t.Errorf("\nstartLine %d\nstartColumn: %d\nendLine: %d\nendColumn: %d\nstartLineIndex: %d\nendlineIndex %d",
+				loc.startLine, loc.startColumn, loc.endLine, loc.endColumn, loc.startLineIndex, loc.endLineIndex)
+
+			t.Error("got", loc, "want", test.wantLocation)
+		}
+	}
+}

+ 0 - 195
examples/leaky-repo.toml

@@ -1,195 +0,0 @@
-title = "gitleaks config"
-
-[[rules]]
-	description = "AWS Access Key"
-	regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-	tags = ["key", "AWS"]
-
-[[rules]]
-	description = "AWS cred file info"
-	regex = '''(?i)(aws_access_key_id|aws_secret_access_key)(.{0,20})?=.[0-9a-zA-Z\/+]{20,40}'''
-	tags = ["AWS"]
-
-[[rules]]
-	description = "AWS Secret Key"
-	regex = '''(?i)aws(.{0,20})?(?-i)['\"][0-9a-zA-Z\/+]{40}['\"]'''
-	tags = ["key", "AWS"]
-
-[[rules]]
-	description = "AWS MWS key"
-	regex = '''amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'''
-	tags = ["key", "AWS", "MWS"]
-
-[[rules]]
-	description = "Facebook Secret Key"
-	regex = '''(?i)(facebook|fb)(.{0,20})?(?-i)['\"][0-9a-f]{32}['\"]'''
-	tags = ["key", "Facebook"]
-
-[[rules]]
-	description = "Facebook Client ID"
-	regex = '''(?i)(facebook|fb)(.{0,20})?['\"][0-9]{13,17}['\"]'''
-	tags = ["key", "Facebook"]
-
-[[rules]]
-	description = "Twitter Secret Key"
-	regex = '''(?i)twitter(.{0,20})?['\"][0-9a-z]{35,44}['\"]'''
-	tags = ["key", "Twitter"]
-
-[[rules]]
-	description = "Twitter Client ID"
-	regex = '''(?i)twitter(.{0,20})?['\"][0-9a-z]{18,25}['\"]'''
-	tags = ["client", "Twitter"]
-
-[[rules]]
-	description = "Github"
-	regex = '''(?i)github(.{0,20})?(?-i)['\"][0-9a-zA-Z]{35,40}['\"]'''
-	tags = ["key", "Github"]
-
-[[rules]]
-	description = "LinkedIn Client ID"
-	regex = '''(?i)linkedin(.{0,20})?(?-i)['\"][0-9a-z]{12}['\"]'''
-	tags = ["client", "LinkedIn"]
-
-[[rules]]
-	description = "LinkedIn Secret Key"
-	regex = '''(?i)linkedin(.{0,20})?['\"][0-9a-z]{16}['\"]'''
-	tags = ["secret", "LinkedIn"]
-
-[[rules]]
-	description = "Slack"
-	regex = '''xox[baprs]-([0-9a-zA-Z]{10,48})?'''
-	tags = ["key", "Slack"]
-
-[[rules]]
-	description = "EC"
-	regex = '''-----BEGIN EC PRIVATE KEY-----'''
-	tags = ["key", "EC"]
-
-
-[[rules]]
-	description = "Google API key"
-	regex = '''AIza[0-9A-Za-z\\-_]{35}'''
-	tags = ["key", "Google"]
-
-
-[[rules]]
-	description = "Heroku API key"
-	regex = '''(?i)heroku(.{0,20})?['"][0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}['"]'''
-	tags = ["key", "Heroku"]
-
-[[rules]]
-	description = "MailChimp API key"
-	regex = '''(?i)(mailchimp|mc)(.{0,20})?['"][0-9a-f]{32}-us[0-9]{1,2}['"]'''
-	tags = ["key", "Mailchimp"]
-
-[[rules]]
-	description = "Mailgun API key"
-	regex = '''(?i)(mailgun|mg)(.{0,20})?['"][0-9a-z]{32}['"]'''
-	tags = ["key", "Mailgun"]
-
-[[rules]]
-	description = "PayPal Braintree access token"
-	regex = '''access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}'''
-	tags = ["key", "Paypal"]
-
-[[rules]]
-	description = "Picatic API key"
-	regex = '''sk_live_[0-9a-z]{32}'''
-	tags = ["key", "Picatic"]
-
-[[rules]]
-	description = "Slack Webhook"
-	regex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}'''
-	tags = ["key", "slack"]
-
-[[rules]]
-	description = "Stripe API key"
-	regex = '''(?i)stripe(.{0,20})?['\"][sk|rk]_live_[0-9a-zA-Z]{24}'''
-	tags = ["key", "Stripe"]
-
-[[rules]]
-	description = "Square access token"
-	regex = '''sq0atp-[0-9A-Za-z\-_]{22}'''
-	tags = ["key", "square"]
-
-[[rules]]
-	description = "Square OAuth secret"
-	regex = '''sq0csp-[0-9A-Za-z\\-_]{43}'''
-	tags = ["key", "square"]
-
-[[rules]]
-	description = "Twilio API key"
-	regex = '''(?i)twilio(.{0,20})?['\"][0-9a-f]{32}['\"]'''
-	tags = ["key", "twilio"]
-
-[[rules]]
-	description = "Env Var"
-	regex = '''(?i)(apikey|secret|key|api|password|pass|pw|host)=[0-9a-zA-Z-_.{}]{4,120}'''
-
-[[rules]]
-	description = "Port"
-	regex = '''(?i)port(.{0,4})?[0-9]{1,10}'''
-	[rules.allowlist]
-		regexes = ['''(?i)port ''']
-		description = "ignore export "
-
-
-
-[[rules]]
-	description = "Email"
-	regex = '''[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}'''
-	tags = ["email"]
-	[rules.allowlist]
-		files = ['''(?i)bashrc''']
-		description = "ignore bashrc emails"
-
-
-[[rules]]
-	description = "Generic Credential"
-	regex = '''(?i)(dbpasswd|dbuser|dbname|dbhost|api_key|apikey|secret|key|api|password|user|guid|hostname|pw|auth)(.{0,20})?['|"]([0-9a-zA-Z-_\/+!{}/=]{4,120})['|"]'''
-	tags = ["key", "API", "generic"]
-	# ignore leaks with specific identifiers like slack and aws
-	[rules.allowlist]
-		description = "ignore slack, mailchimp, aws"
-		regexes = [
-		    '''xox[baprs]-([0-9a-zA-Z]{10,48})''',
-		    '''(?i)(.{0,20})?['"][0-9a-f]{32}-us[0-9]{1,2}['"]''',
-		    '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-		]
-
-[[rules]]
-	description = "High Entropy"
-	regex = '''[0-9a-zA-Z-_!{}/=]{4,120}'''
-  	file = '''(?i)(dump.sql|high-entropy-misc.txt)$'''
-	tags = ["entropy"]
-    [[rules.Entropies]]
-        Min = "4.3"
-        Max = "7.0"
-    [rules.allowlist]
-        description = "ignore ssh key and pems"
-        files = ['''(pem|ppk|env)$''']
-        paths = ['''(.*)?ssh''']
-
-[[rules]]
-	description = "Potential bash var"
-	regex='''(?i)(=)([0-9a-zA-Z-_!{}=]{4,120})'''
-	tags = ["key", "bash", "API", "generic"]
-        [[rules.Entropies]]
-            Min = "3.5"
-            Max = "4.5"
-            Group = "1"
-
-[[rules]]
-	description = "WP-Config"
-	regex='''define(.{0,20})?(DB_CHARSET|NONCE_SALT|LOGGED_IN_SALT|AUTH_SALT|NONCE_KEY|DB_HOST|DB_PASSWORD|AUTH_KEY|SECURE_AUTH_KEY|LOGGED_IN_KEY|DB_NAME|DB_USER)(.{0,20})?['|"].{10,120}['|"]'''
-	tags = ["key", "API", "generic"]
-
-[[rules]]
-	description = "Files with keys and credentials"
-	file = '''(?i)(id_rsa|passwd|id_rsa.pub|pgpass|pem|key|shadow)'''
-
-# Global allowlist
-[allowlist]
-	description = "image allowlists"
-	files = ['''(.*?)(jpg|gif|doc|pdf|bin)$''']
-

+ 0 - 14
examples/pre-commit-config-example.yaml

@@ -1,14 +0,0 @@
-# pre-commit configuration example to add docker-based hook that executes gitleaks
-# This should be added to .pre-commit-config.yaml coniguration file: https://pre-commit.com/#2-add-a-pre-commit-configuration
-
-repos:
-- repo: local
-  hooks:
-  - id: gitleaks
-    name: Gitleaks
-    language: docker_image
-    entry: zricethezav/gitleaks:v7.4.0
-    args:
-    - --config-path
-    - .gitleaks.toml
-    - --verbose

+ 0 - 19
examples/pre-commit.example

@@ -1,19 +0,0 @@
-#!/bin/sh
-
-# This is an example of what adding gitleaks to a pre-commit hook would look like.
-
-gitleaksEnabled=$(git config --bool hooks.gitleaks)
-cmd="/Users/zrice/go/src/github.com/zricethezav/gitleaks/gitleaks --verbose --redact --pretty"
-if [ $gitleaksEnabled == "true" ]; then
-    $cmd
-    if [ $? -eq 1 ]; then
-cat <<\EOF
-Error: gitleaks has detected sensitive information in your changes.
-If you know what you are doing you can disable this check using:
-
-    git config hooks.gitleaks false
-
-EOF
-exit 1
-    fi
-fi

+ 0 - 17
examples/regex_and_entropy_config.toml

@@ -1,17 +0,0 @@
-# This config contains a single rule which defines a regex and a range of entropy values. If a rule has
-# both regex and entropy then that rule uses BOTH the regex and entropy in combination when performing an scan.
-# In other words, if a line of code has an entropy value that is within the range of the entropies defined and
-# a regex match is found then that line of code contains a leak.
-
-# So, for this example if a line of code has an entropy value of 4.6 AND matches the regex below then we got a leak.
-
-[[rules]]
-	description = "entropy and regex"
-	regex = '''(?i)key(.{0,20})?['|"][0-9a-zA-Z]{16,45}['|"]'''
-	tags = ["entropy"]
-		[[rules.Entropies]]
-			Min = "4.5"
-			Max = "5.7"
-		[[rules.Entropies]]
-			Min = "5.5"
-			Max = "6.3"

+ 0 - 13
examples/simple_regex_and_allowlist_config.toml

@@ -1,13 +0,0 @@
-# This config contains a single rule that checks for AWS keys. However, it also contains a allowlist table
-# where you can define one or more allowlists. What this means is that if you have an example AWS key as part of your
-# code (in a test for example), then you can allowlist that specific key so gitleaks will not label it as a leak.
-# If this line was present in a git history: `aws_access_key_id='AKIAIO5FODNN7EXAMPLE``, gitleaks would match this line
-# with the rule below, but since we have a allowlist against that specific key, it would be ignored.
-
-[[rules]]
-    description = "AWS Access Key"
-    regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-    tags = ["key", "AWS"]
-    [rules.allowlist]
-        regexes = ['''AKIAIO5FODNN7EXAMPLE.*''']
-        description = "ignore example aws key"

+ 85 - 0
git/git.go

@@ -0,0 +1,85 @@
+package git
+
+import (
+	"bufio"
+	"io"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+
+	"github.com/gitleaks/go-gitdiff/gitdiff"
+	"github.com/rs/zerolog/log"
+)
+
+// GitLog returns a channel of gitdiff.File objects from the git log -p command for the given source.
+func GitLog(source string, logOpts string) (<-chan *gitdiff.File, error) {
+	sourceClean := filepath.Clean(source)
+	var cmd *exec.Cmd
+	if logOpts != "" {
+		args := []string{"-C", sourceClean, "log", "-p", "-U0"}
+		args = append(args, strings.Split(logOpts, " ")...)
+		cmd = exec.Command("git", args...)
+	} else {
+		cmd = exec.Command("git", "-C", sourceClean, "log", "-p", "-U0", "--full-history", "--simplify-merges", "--show-pulls", "--all")
+	}
+
+	log.Debug().Msgf("executing: %s", cmd.String())
+
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return nil, err
+	}
+	stderr, err := cmd.StderrPipe()
+	if err != nil {
+		return nil, err
+	}
+	if err := cmd.Start(); err != nil {
+		return nil, err
+	}
+
+	go listenForStdErr(stderr)
+
+	return gitdiff.Parse(stdout)
+}
+
+// GitDiff returns a channel of gitdiff.File objects from the git diff command for the given source.
+func GitDiff(source string, staged bool) (<-chan *gitdiff.File, error) {
+	sourceClean := filepath.Clean(source)
+	var cmd *exec.Cmd
+	cmd = exec.Command("git", "-C", sourceClean, "diff", "-U0", ".")
+	if staged {
+		cmd = exec.Command("git", "-C", sourceClean, "diff", "-U0", "--staged", ".")
+	}
+	log.Debug().Msgf("executing: %s", cmd.String())
+
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return nil, err
+	}
+	stderr, err := cmd.StderrPipe()
+	if err != nil {
+		return nil, err
+	}
+	if err := cmd.Start(); err != nil {
+		return nil, err
+	}
+
+	go listenForStdErr(stderr)
+
+	return gitdiff.Parse(stdout)
+}
+
+// listenForStdErr listens for stderr output from git and prints it to stdout
+// then exits with exit code 1
+func listenForStdErr(stderr io.ReadCloser) {
+	scanner := bufio.NewScanner(stderr)
+	errEncountered := false
+	for scanner.Scan() {
+		log.Error().Msg(scanner.Text())
+		errEncountered = true
+	}
+	if errEncountered {
+		os.Exit(1)
+	}
+}

+ 157 - 0
git/git_test.go

@@ -0,0 +1,157 @@
+package git_test
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/gitleaks/go-gitdiff/gitdiff"
+	"github.com/zricethezav/gitleaks/v8/git"
+)
+
+const repoBasePath = "../testdata/repos/"
+const expectPath = "../testdata/expected/"
+
+func TestGitLog(t *testing.T) {
+	tests := []struct {
+		source   string
+		logOpts  string
+		expected string
+	}{
+		{
+			source:   filepath.Join(repoBasePath, "small"),
+			expected: filepath.Join(expectPath, "git", "small.txt"),
+		},
+		{
+			source:   filepath.Join(repoBasePath, "small"),
+			expected: filepath.Join(expectPath, "git", "small-branch-foo.txt"),
+			logOpts:  "--all foo...",
+		},
+	}
+
+	err := moveDotGit("dotGit", ".git")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer moveDotGit(".git", "dotGit")
+
+	for _, tt := range tests {
+		files, err := git.GitLog(tt.source, tt.logOpts)
+		if err != nil {
+			t.Error(err)
+		}
+
+		var diffSb strings.Builder
+		for f := range files {
+			for _, tf := range f.TextFragments {
+				diffSb.WriteString(tf.Raw(gitdiff.OpAdd))
+			}
+		}
+
+		expectedBytes, err := os.ReadFile(tt.expected)
+		if err != nil {
+			t.Error(err)
+		}
+		expected := string(expectedBytes)
+		if expected != diffSb.String() {
+			// write string builder to .got file using os.Create
+			err = os.WriteFile(strings.Replace(tt.expected, ".txt", ".got.txt", 1), []byte(diffSb.String()), 0644)
+			if err != nil {
+				t.Error(err)
+			}
+			t.Error("expected: ", expected, "got: ", diffSb.String())
+		}
+	}
+}
+
+func TestGitDiff(t *testing.T) {
+	tests := []struct {
+		source    string
+		expected  string
+		additions string
+		target    string
+	}{
+		{
+			source:    filepath.Join(repoBasePath, "small"),
+			expected:  "this line is added\nand another one",
+			additions: "this line is added\nand another one",
+			target:    filepath.Join(repoBasePath, "small", "main.go"),
+		},
+	}
+
+	err := moveDotGit("dotGit", ".git")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer moveDotGit(".git", "dotGit")
+
+	for _, tt := range tests {
+		noChanges, err := os.ReadFile(tt.target)
+		if err != nil {
+			t.Error(err)
+		}
+		err = os.WriteFile(tt.target, []byte(tt.additions), 0644)
+		if err != nil {
+			restore(tt.target, noChanges, t)
+			t.Error(err)
+		}
+
+		files, err := git.GitDiff(tt.source, false)
+		if err != nil {
+			restore(tt.target, noChanges, t)
+			t.Error(err)
+		}
+
+		for f := range files {
+			sb := strings.Builder{}
+			for _, tf := range f.TextFragments {
+				sb.WriteString(tf.Raw(gitdiff.OpAdd))
+			}
+			if sb.String() != tt.expected {
+				restore(tt.target, noChanges, t)
+				t.Error("expected: ", tt.expected, "got: ", sb.String())
+			}
+		}
+		restore(tt.target, noChanges, t)
+	}
+}
+
+func restore(path string, data []byte, t *testing.T) {
+	err := os.WriteFile(path, data, 0644)
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func moveDotGit(from, to string) error {
+	repoDirs, err := os.ReadDir("../testdata/repos")
+	if err != nil {
+		return err
+	}
+	for _, dir := range repoDirs {
+		if to == ".git" {
+			_, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), "dotGit"))
+			if os.IsNotExist(err) {
+				// dont want to delete the only copy of .git accidentally
+				continue
+			}
+			os.RemoveAll(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), ".git"))
+		}
+		if !dir.IsDir() {
+			continue
+		}
+		_, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from))
+		if os.IsNotExist(err) {
+			continue
+		}
+
+		err = os.Rename(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from),
+			fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), to))
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}

+ 10 - 10
go.mod

@@ -1,15 +1,15 @@
-module github.com/zricethezav/gitleaks/v7
+module github.com/zricethezav/gitleaks/v8
 
 go 1.16
 
-replace github.com/go-git/go-git/v5 => github.com/zricethezav/go-git/v5 v5.3.0
-
 require (
-	github.com/BurntSushi/toml v0.3.1
-	github.com/go-git/go-git/v5 v5.3.0
-	github.com/hako/durafmt v0.0.0-20191009132224-3f39dc1ed9f4
-	github.com/jessevdk/go-flags v1.5.0
-	github.com/sergi/go-diff v1.1.0
-	github.com/sirupsen/logrus v1.4.2
-	golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
+	github.com/gitleaks/go-gitdiff v0.7.2
+	github.com/rs/zerolog v1.25.0
+	github.com/spf13/cobra v1.2.1
+	github.com/spf13/viper v1.8.1
+	github.com/stretchr/testify v1.7.0
+	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
+	golang.org/x/sys v0.0.0-20211110154304-99a53858aa08 // indirect
+	golang.org/x/tools v0.1.5
+	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 )

+ 572 - 82
go.sum

@@ -1,112 +1,602 @@
-github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
+cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
+cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
-github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
-github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
-github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ=
-github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
-github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
-github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
-github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
-github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
-github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
-github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
-github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
-github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
-github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
-github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
-github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
-github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
-github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
-github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34=
-github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
-github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8=
-github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
-github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gitleaks/go-gitdiff v0.7.2 h1:V9a22yhh7BTrJZZCM77EaMbmCSDNEcHGNwqB+lC/6v0=
+github.com/gitleaks/go-gitdiff v0.7.2/go.mod h1:pKz0X4YzCKZs30BL+weqBIG7mx0jl4tF1uXV9ZyNvrA=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/hako/durafmt v0.0.0-20191009132224-3f39dc1ed9f4 h1:60gBOooTSmNtrqNaRvrDbi8VAne0REaek2agjnITKSw=
-github.com/hako/durafmt v0.0.0-20191009132224-3f39dc1ed9f4/go.mod h1:5Scbynm8dF1XAPwIwkGPqzkM/shndPm79Jd1003hTjE=
-github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
-github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
-github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
-github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
-github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
-github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
-github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
+github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
-github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
-github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
-github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
+github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
+github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
-github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
-github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II=
+github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
+github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
+github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
+github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
+github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
-github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
-github.com/zricethezav/go-git/v5 v5.3.0 h1:sx02mqKAT6Doe4rLcquIJr1XjrV5QcX8KXk3wSCVic4=
-github.com/zricethezav/go-git/v5 v5.3.0/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
-golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
-golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
+go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
+go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs=
-golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
+golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79 h1:RX8C8PRZc2hTIod4ds8ij+/4RQX3AqhYj3uOHmyaz4E=
-golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211110154304-99a53858aa08 h1:WecRHqgE09JBkh/584XIE6PMz5KKE/vER4izNUi30AQ=
+golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
+google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
+google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
-gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
+gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

+ 8 - 54
main.go

@@ -3,72 +3,26 @@ package main
 import (
 	"os"
 	"os/signal"
-	"time"
 
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-	"github.com/zricethezav/gitleaks/v7/scan"
-
-	"github.com/hako/durafmt"
-	log "github.com/sirupsen/logrus"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
+	"github.com/zricethezav/gitleaks/v8/cmd"
 )
 
 func main() {
+	// send all logs to stdout
+	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
+
 	// this block sets up a go routine to listen for an interrupt signal
 	// which will immediately exit gitleaks
 	stopChan := make(chan os.Signal, 1)
 	signal.Notify(stopChan, os.Interrupt)
 	go listenForInterrupt(stopChan)
 
-	// setup options
-	opts, err := options.ParseOptions()
-	if err != nil {
-		log.Error(err)
-		os.Exit(1)
-	}
-
-	err = opts.Guard()
-	if err != nil {
-		log.Error(err)
-		os.Exit(1)
-	}
-
-	// setup configs
-	cfg, err := config.NewConfig(opts)
-	if err != nil {
-		log.Error(err)
-		os.Exit(1)
-	}
-
-	// setup scanner
-	scanner, err := scan.NewScanner(opts, cfg)
-	if err != nil {
-		log.Error(err)
-		os.Exit(1)
-	}
-
-	// run and time the scan
-	start := time.Now()
-	scannerReport, err := scanner.Scan()
-	log.Info("scan time: ", durafmt.Parse(time.Now().Sub(start)))
-	if err != nil {
-		log.Error(err)
-		os.Exit(1)
-	}
-
-	// report scan
-	if err := scan.WriteReport(scannerReport, opts, cfg); err != nil {
-		log.Error(err)
-		os.Exit(1)
-	}
-
-	if len(scannerReport.Leaks) != 0 {
-		os.Exit(opts.CodeOnLeak)
-	}
+	cmd.Execute()
 }
 
 func listenForInterrupt(stopScan chan os.Signal) {
 	<-stopScan
-	log.Warn("halting gitleaks scan")
-	os.Exit(1)
+	log.Fatal().Msg("Interrupt signal received. Exiting...")
 }

+ 0 - 230
options/options.go

@@ -1,230 +0,0 @@
-package options
-
-import (
-	"fmt"
-	"io/ioutil"
-	"os"
-	"os/user"
-	"strings"
-
-	"github.com/zricethezav/gitleaks/v7/version"
-
-	"github.com/go-git/go-git/v5"
-	"github.com/go-git/go-git/v5/plumbing"
-	"github.com/go-git/go-git/v5/plumbing/transport"
-	"github.com/go-git/go-git/v5/plumbing/transport/http"
-	"github.com/go-git/go-git/v5/plumbing/transport/ssh"
-	"github.com/jessevdk/go-flags"
-	log "github.com/sirupsen/logrus"
-)
-
-// Options stores values of command line options
-type Options struct {
-	Verbose          bool   `short:"v" long:"verbose" description:"Show verbose output from scan"`
-	Quiet            bool   `short:"q" long:"quiet" description:"Sets log level to error and only output leaks, one json object per line"`
-	RepoURL          string `short:"r" long:"repo-url" description:"Repository URL"`
-	Path             string `short:"p" long:"path" description:"Path to directory (repo if contains .git) or file"`
-	ConfigPath       string `short:"c" long:"config-path" description:"Path to config"`
-	RepoConfigPath   string `long:"repo-config-path" description:"Path to gitleaks config relative to repo root"`
-	ClonePath        string `long:"clone-path" description:"Path to clone repo to disk"`
-	Version          bool   `long:"version" description:"Version number"`
-	Username         string `long:"username" description:"Username for git repo"`
-	Password         string `long:"password" description:"Password for git repo"`
-	AccessToken      string `long:"access-token" description:"Access token for git repo"`
-	Threads          int    `long:"threads" description:"Maximum number of threads gitleaks spawns"`
-	SSH              string `long:"ssh-key" description:"Path to ssh key used for auth"`
-	Unstaged         bool   `long:"unstaged" description:"Run gitleaks on unstaged code"`
-	Branch           string `long:"branch" description:"Branch to scan"`
-	Redact           bool   `long:"redact" description:"Redact secrets from log messages and leaks"`
-	Debug            bool   `long:"debug" description:"Log debug messages"`
-	NoGit            bool   `long:"no-git" description:"Treat git repos as plain directories and scan those files"`
-	CodeOnLeak       int    `long:"leaks-exit-code" default:"1" description:"Exit code when leaks have been encountered"`
-	AppendRepoConfig bool   `long:"append-repo-config" description:"Append the provided or default config with the repo config."`
-	AdditionalConfig string `long:"additional-config" description:"Path to an additional gitleaks config to append with an existing config. Can be used with --append-repo-config to append up to three configurations"`
-
-	// Report Options
-	Report       string `short:"o" long:"report" description:"Report output path"`
-	ReportFormat string `short:"f" long:"format" default:"json" description:"json, csv, sarif"`
-
-	// Commit Options
-	FilesAtCommit string `long:"files-at-commit" description:"Sha of commit to scan all files at commit"`
-	Commit        string `long:"commit" description:"Sha of commit to scan or \"latest\" to scan the last commit of the repository"`
-	Commits       string `long:"commits" description:"Comma separated list of a commits to scan"`
-	CommitsFile   string `long:"commits-file" description:"Path to file of line separated list of commits to scan"`
-	CommitFrom    string `long:"commit-from" description:"Commit to start scan from"`
-	CommitTo      string `long:"commit-to" description:"Commit to stop scan"`
-	CommitSince   string `long:"commit-since" description:"Scan commits more recent than a specific date. Ex: '2006-01-02' or '2006-01-02T15:04:05-0700' format."`
-	CommitUntil   string `long:"commit-until" description:"Scan commits older than a specific date. Ex: '2006-01-02' or '2006-01-02T15:04:05-0700' format."`
-	Depth         int    `long:"depth" description:"Number of commits to scan"`
-}
-
-// ParseOptions is responsible for parsing options passed in by cli. An Options struct
-// is returned if successful. This struct is passed around the program
-// and will determine how the program executes. If err, an err message or help message
-// will be displayed and the program will exit with code 0.
-func ParseOptions() (Options, error) {
-	var opts Options
-	parser := flags.NewParser(&opts, flags.Default)
-	_, err := parser.Parse()
-
-	if err != nil {
-		if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type != flags.ErrHelp {
-			parser.WriteHelp(os.Stdout)
-		}
-		os.Exit(1)
-	}
-
-	if opts.Version {
-		if version.Version == "" {
-			fmt.Println("Gitleaks uses LDFLAGS to pull most recent version. Build with 'make build' for version")
-		} else {
-			fmt.Printf("%s\n", version.Version)
-		}
-		os.Exit(0)
-	}
-
-	if opts.Debug {
-		log.SetLevel(log.DebugLevel)
-	}
-	if opts.Quiet {
-		log.SetLevel(log.ErrorLevel)
-	}
-
-	return opts, nil
-}
-
-// Guard checks to makes sure there are no invalid options set.
-// If invalid sets of options are present, a descriptive error will return
-// else nil is returned
-func (opts Options) Guard() error {
-	if !oneOrNoneSet(opts.RepoURL, opts.Path) {
-		return fmt.Errorf("only one target option must can be set. target options: repo, owner-path, repo-path, host")
-	}
-	if !oneOrNoneSet(opts.AccessToken, opts.Password) {
-		log.Warn("both access-token and password are set. Only password will be attempted")
-	}
-
-	return nil
-}
-
-func oneOrNoneSet(optStr ...string) bool {
-	c := 0
-	for _, s := range optStr {
-		if s != "" {
-			c++
-		}
-	}
-	if c <= 1 {
-		return true
-	}
-	return false
-}
-
-// CloneOptions returns a git.cloneOptions pointer. The authentication method
-// is determined by what is passed in via command-Line options. If No
-// Username/PW or AccessToken is available and the repo target is not using the
-// git protocol then the repo must be a available via no auth.
-func (opts Options) CloneOptions() (*git.CloneOptions, error) {
-	var err error
-	progress := ioutil.Discard
-	if opts.Verbose {
-		progress = os.Stdout
-	}
-
-	cloneOpts := &git.CloneOptions{
-		URL:      opts.RepoURL,
-		Progress: progress,
-	}
-	if opts.Depth != 0 {
-		cloneOpts.Depth = opts.Depth
-	}
-	if opts.Branch != "" {
-		cloneOpts.ReferenceName = plumbing.NewBranchReferenceName(opts.Branch)
-	}
-
-	var auth transport.AuthMethod
-
-	if strings.HasPrefix(opts.RepoURL, "ssh://") || (!strings.Contains(opts.RepoURL, "://") && strings.Contains(opts.RepoURL, ":")) {
-		// using ssh:// url or scp-like syntax
-		auth, err = SSHAuth(opts)
-		if err != nil {
-			return nil, err
-		}
-	} else if opts.Password != "" && opts.Username != "" {
-		// auth using username and password
-		auth = &http.BasicAuth{
-			Username: opts.Username,
-			Password: opts.Password,
-		}
-	} else if opts.AccessToken != "" {
-		auth = &http.BasicAuth{
-			Username: "gitleaks_user",
-			Password: opts.AccessToken,
-		}
-	} else if os.Getenv("GITLEAKS_ACCESS_TOKEN") != "" {
-		auth = &http.BasicAuth{
-			Username: "gitleaks_user",
-			Password: os.Getenv("GITLEAKS_ACCESS_TOKEN"),
-		}
-	}
-	if auth != nil {
-		cloneOpts.Auth = auth
-	}
-	return cloneOpts, nil
-}
-
-// SSHAuth tried to generate ssh public keys based on what was passed via cli. If no
-// path was passed via cli then this will attempt to retrieve keys from the default
-// location for ssh keys, $HOME/.ssh/id_rsa. This function is only called if the
-// repo url using the ssh:// protocol or scp-like syntax.
-func SSHAuth(opts Options) (*ssh.PublicKeys, error) {
-	params := strings.Split(opts.RepoURL, "@")
-
-	if len(params) != 2 {
-		return nil, fmt.Errorf("user must be specified in the URL")
-	}
-
-	// the part of the RepoURL before the "@" (params[0]) can be something like:
-	// - "ssh://user" if RepoURL is an ssh:// URL
-	// - "user" if RepoURL uses scp-like syntax
-	// we must strip the protocol if it is present so that we only have "user"
-	username := strings.Replace(params[0], "ssh://", "", 1)
-
-	if opts.SSH != "" {
-		return ssh.NewPublicKeysFromFile(username, opts.SSH, "")
-	}
-	c, err := user.Current()
-	if err != nil {
-		return nil, err
-	}
-	defaultPath := fmt.Sprintf("%s/.ssh/id_rsa", c.HomeDir)
-	return ssh.NewPublicKeysFromFile(username, defaultPath, "")
-}
-
-// OpenLocal checks what options are set, if no remote targets are set
-// then return true
-func (opts Options) OpenLocal() bool {
-	if opts.Unstaged || opts.Path != "" || opts.RepoURL == "" {
-		return true
-	}
-	return false
-}
-
-// CheckUncommitted returns a boolean that indicates whether or not gitleaks should check unstaged pre-commit changes
-// or if gitleaks should check the entire git history
-func (opts Options) CheckUncommitted() bool {
-	// check to make sure no remote shit is set
-	if opts.Unstaged {
-		return true
-	}
-	if opts == (Options{}) {
-		return true
-	}
-	if opts.RepoURL != "" {
-		return false
-	}
-	if opts.Path != "" {
-		return false
-	}
-	return true
-}

+ 0 - 1
options/options_test.go

@@ -1 +0,0 @@
-package options

+ 5 - 0
report/constants.go

@@ -0,0 +1,5 @@
+package report
+
+const version = "v8.0.0"
+const driver = "gitleaks"
+const driverURL = "https://github.com/zricethezav/gitleaks"

+ 55 - 0
report/csv.go

@@ -0,0 +1,55 @@
+package report
+
+import (
+	"encoding/csv"
+	"io"
+	"strconv"
+)
+
+// writeCsv writes the list of findings to a writeCloser.
+func writeCsv(f []*Finding, w io.WriteCloser) error {
+	if len(f) == 0 {
+		return nil
+	}
+	defer w.Close()
+	cw := csv.NewWriter(w)
+	err := cw.Write([]string{"RuleID",
+		"Commit",
+		"File",
+		"Secret",
+		"Context",
+		"StartLine",
+		"EndLine",
+		"StartColumn",
+		"EndColumn",
+		"Author",
+		"Message",
+		"Date",
+		"Email",
+	})
+	if err != nil {
+		return err
+	}
+	for _, f := range f {
+		err = cw.Write([]string{f.RuleID,
+			f.Commit,
+			f.File,
+			f.Secret,
+			f.Context,
+			strconv.Itoa(f.StartLine),
+			strconv.Itoa(f.EndLine),
+			strconv.Itoa(f.StartColumn),
+			strconv.Itoa(f.EndColumn),
+			f.Author,
+			f.Message,
+			f.Date,
+			f.Email,
+		})
+		if err != nil {
+			return err
+		}
+	}
+
+	cw.Flush()
+	return cw.Error()
+}

+ 84 - 0
report/csv_test.go

@@ -0,0 +1,84 @@
+package report
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func TestWriteCSV(t *testing.T) {
+	tests := []struct {
+		findings       []*Finding
+		testReportName string
+		expected       string
+		wantEmpty      bool
+	}{
+		{
+			testReportName: "simple",
+			expected:       filepath.Join(expectPath, "report", "csv_simple.csv"),
+			findings: []*Finding{
+				{
+					RuleID:      "test-rule",
+					Context:     "line containing secret",
+					Secret:      "a secret",
+					StartLine:   1,
+					EndLine:     2,
+					StartColumn: 1,
+					EndColumn:   2,
+					Message:     "opps",
+					File:        "auth.py",
+					Commit:      "0000000000000000",
+					Author:      "John Doe",
+					Email:       "johndoe@gmail.com",
+					Date:        "10-19-2003",
+				},
+			}},
+		{
+
+			wantEmpty:      true,
+			testReportName: "empty",
+			expected:       filepath.Join(expectPath, "report", "this_should_not_exist.csv"),
+			findings:       []*Finding{}},
+	}
+
+	for _, test := range tests {
+		tmpfile, err := os.Create(filepath.Join(tmpPath, test.testReportName+".csv"))
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		err = writeCsv(test.findings, tmpfile)
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		got, err := os.ReadFile(tmpfile.Name())
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		if test.wantEmpty {
+			if len(got) > 0 {
+				t.Errorf("Expected empty file, got %s", got)
+			}
+			os.Remove(tmpfile.Name())
+			continue
+		}
+		want, err := os.ReadFile(test.expected)
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+
+		if string(got) != string(want) {
+			err = os.WriteFile(strings.Replace(test.expected, ".csv", ".got.csv", 1), got, 0644)
+			if err != nil {
+				t.Error(err)
+			}
+			t.Errorf("got %s, want %s", string(got), string(want))
+		}
+
+		os.Remove(tmpfile.Name())
+	}
+}

+ 42 - 0
report/finding.go

@@ -0,0 +1,42 @@
+package report
+
+import "strings"
+
+// Finding contains information about strings that
+// have been captured by a tree-sitter query.
+type Finding struct {
+	Description string
+	StartLine   int
+	EndLine     int
+	StartColumn int
+	EndColumn   int
+
+	Context string
+
+	// Secret contains the full content of what is matched in
+	// the tree-sitter query.
+	Secret string
+
+	// File is the name of the file containing the finding
+	File string
+
+	Commit string
+
+	// Entropy is the shannon entropy of Value
+	Entropy float32
+
+	Author  string
+	Email   string
+	Date    string
+	Message string
+	Tags    []string
+
+	// Rule is the name of the rule that was matched
+	RuleID string
+}
+
+// Redact removes sensitive information from a finding.
+func (f *Finding) Redact() {
+	f.Context = strings.Replace(f.Context, f.Secret, "REDACTED", -1)
+	f.Secret = "REDACT"
+}

+ 27 - 0
report/finding_test.go

@@ -0,0 +1,27 @@
+package report
+
+import "testing"
+
+func TestRedact(t *testing.T) {
+	tests := []struct {
+		findings []Finding
+		redact   bool
+	}{
+		{
+			redact: true,
+			findings: []Finding{
+				{
+					Secret:  "line containing secret",
+					Context: "secret",
+				},
+			}},
+	}
+	for _, test := range tests {
+		for _, f := range test.findings {
+			f.Redact()
+			if f.Secret != "REDACT" {
+				t.Error("redact not redacting: ", f.Secret)
+			}
+		}
+	}
+}

+ 15 - 0
report/json.go

@@ -0,0 +1,15 @@
+package report
+
+import (
+	"encoding/json"
+	"io"
+)
+
+func writeJson(findings []*Finding, w io.WriteCloser) error {
+	if len(findings) == 0 {
+		return nil
+	}
+	encoder := json.NewEncoder(w)
+	encoder.SetIndent("", " ")
+	return encoder.Encode(findings)
+}

+ 89 - 0
report/json_test.go

@@ -0,0 +1,89 @@
+package report
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func TestWriteJSON(t *testing.T) {
+	tests := []struct {
+		findings       []*Finding
+		testReportName string
+		expected       string
+		wantEmpty      bool
+	}{
+		{
+			testReportName: "simple",
+			expected:       filepath.Join(expectPath, "report", "json_simple.json"),
+			findings: []*Finding{
+				{
+
+					Description: "",
+					RuleID:      "test-rule",
+					Context:     "line containing secret",
+					Secret:      "a secret",
+					StartLine:   1,
+					EndLine:     2,
+					StartColumn: 1,
+					EndColumn:   2,
+					Message:     "opps",
+					File:        "auth.py",
+					Commit:      "0000000000000000",
+					Author:      "John Doe",
+					Email:       "johndoe@gmail.com",
+					Date:        "10-19-2003",
+					Tags:        []string{},
+				},
+			}},
+		{
+
+			wantEmpty:      true,
+			testReportName: "empty",
+			expected:       filepath.Join(expectPath, "report", "this_should_not_exist.json"),
+			findings:       []*Finding{}},
+	}
+
+	for _, test := range tests {
+		// create tmp file using os.TempDir()
+		tmpfile, err := os.Create(filepath.Join(tmpPath, test.testReportName+".json"))
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		err = writeJson(test.findings, tmpfile)
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		got, err := os.ReadFile(tmpfile.Name())
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		if test.wantEmpty {
+			if len(got) > 0 {
+				os.Remove(tmpfile.Name())
+				t.Errorf("Expected empty file, got %s", got)
+			}
+			os.Remove(tmpfile.Name())
+			continue
+		}
+		want, err := os.ReadFile(test.expected)
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+
+		if string(got) != string(want) {
+			err = os.WriteFile(strings.Replace(test.expected, ".json", ".got.json", 1), got, 0644)
+			if err != nil {
+				t.Error(err)
+			}
+			t.Errorf("got %s, want %s", string(got), string(want))
+		}
+
+		os.Remove(tmpfile.Name())
+	}
+}

+ 36 - 0
report/report.go

@@ -0,0 +1,36 @@
+package report
+
+import (
+	"os"
+	"strings"
+
+	"github.com/zricethezav/gitleaks/v8/config"
+)
+
+const (
+	// https://cwe.mitre.org/data/definitions/798.html
+	CWE             = "CWE-798"
+	CWE_DESCRIPTION = "Use of Hard-coded Credentials"
+)
+
+func Write(findings []*Finding, cfg config.Config, ext string, reportPath string) error {
+	if len(findings) == 0 {
+		return nil
+	}
+	file, err := os.Create(reportPath)
+	if err != nil {
+		return err
+	}
+	ext = strings.ToLower(ext)
+	switch ext {
+	case ".json", "json":
+		writeJson(findings, file)
+	case ".csv", "csv":
+		writeCsv(findings, file)
+	case ".sarif", "sarif":
+		writeSarif(cfg, findings, file)
+
+	}
+
+	return nil
+}

+ 111 - 0
report/report_test.go

@@ -0,0 +1,111 @@
+package report
+
+import (
+	"os"
+	"path/filepath"
+	"strconv"
+	"testing"
+
+	"github.com/zricethezav/gitleaks/v8/config"
+)
+
+const (
+	expectPath = "../testdata/expected/"
+	tmpPath    = "../testdata/tmp"
+)
+
+func TestReport(t *testing.T) {
+	tests := []struct {
+		findings  []*Finding
+		ext       string
+		wantEmpty bool
+	}{
+		{
+			ext: "json",
+			findings: []*Finding{
+				{
+					RuleID: "test-rule",
+				},
+			},
+		},
+		{
+			ext: ".json",
+			findings: []*Finding{
+				{
+					RuleID: "test-rule",
+				},
+			},
+		},
+		{
+			ext: ".jsonj",
+			findings: []*Finding{
+				{
+					RuleID: "test-rule",
+				},
+			},
+			wantEmpty: true,
+		},
+		{
+			ext: ".csv",
+			findings: []*Finding{
+				{
+					RuleID: "test-rule",
+				},
+			},
+		},
+		{
+			ext: "csv",
+			findings: []*Finding{
+				{
+					RuleID: "test-rule",
+				},
+			},
+		},
+		{
+			ext: "CSV",
+			findings: []*Finding{
+				{
+					RuleID: "test-rule",
+				},
+			},
+		},
+		// {
+		// 	ext: "SARIF",
+		// 	findings: []Finding{
+		// 		{
+		// 			RuleID: "test-rule",
+		// 		},
+		// 	},
+		// },
+	}
+
+	for i, test := range tests {
+		tmpfile, err := os.Create(filepath.Join(tmpPath, strconv.Itoa(i)+test.ext))
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		err = Write(test.findings, config.Config{}, test.ext, tmpfile.Name())
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		got, err := os.ReadFile(tmpfile.Name())
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		os.Remove(tmpfile.Name())
+
+		if len(got) == 0 && !test.wantEmpty {
+			t.Errorf("got empty file with extension " + test.ext)
+		}
+
+		if test.wantEmpty {
+			if len(got) > 0 {
+				t.Errorf("Expected empty file, got %s", got)
+			}
+			continue
+		}
+	}
+}

+ 199 - 0
report/sarif.go

@@ -0,0 +1,199 @@
+package report
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+
+	"github.com/zricethezav/gitleaks/v8/config"
+)
+
+func writeSarif(cfg config.Config, findings []*Finding, w io.WriteCloser) error {
+	sarif := Sarif{
+		Schema:  "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json",
+		Version: version,
+		Runs:    getRuns(cfg, findings),
+	}
+
+	encoder := json.NewEncoder(w)
+	encoder.SetIndent("", " ")
+	return encoder.Encode(sarif)
+}
+
+func getRuns(cfg config.Config, findings []*Finding) []Runs {
+	return []Runs{
+		{
+			Tool:    getTool(cfg),
+			Results: getResults(findings),
+		},
+	}
+}
+
+func getTool(cfg config.Config) Tool {
+	return Tool{
+		Driver: Driver{
+			Name:            driver,
+			SemanticVersion: version,
+			Rules:           getRules(cfg),
+		},
+	}
+}
+
+func getRules(cfg config.Config) []Rules {
+	// TODO	for _, rule := range cfg.Rules {
+	var rules []Rules
+	for _, rule := range cfg.Rules {
+		rules = append(rules, Rules{
+			ID:   rule.RuleID,
+			Name: rule.Description,
+			Description: ShortDescription{
+				Text: rule.Regex.String(),
+			},
+		})
+	}
+	return rules
+}
+
+func messageText(f *Finding) string {
+	if f.Commit == "" {
+		return fmt.Sprintf("%s has detected secret for file %s.", f.RuleID, f.File)
+	}
+
+	return fmt.Sprintf("%s has detected secret for file %s at commit %s.", f.RuleID, f.File, f.Commit)
+
+}
+
+func getResults(findings []*Finding) []Results {
+	var results []Results
+	for _, f := range findings {
+		r := Results{
+			Message: Message{
+				Text: messageText(f),
+			},
+			RuleId:    f.RuleID,
+			Locations: getLocation(f),
+			// This information goes in partial fingerprings until revision
+			// data can be added somewhere else
+			PartialFingerPrints: PartialFingerPrints{
+				CommitSha:     f.Commit,
+				Email:         f.Email,
+				CommitMessage: f.Message,
+				Date:          f.Date,
+				Author:        f.Author,
+			},
+		}
+		results = append(results, r)
+	}
+	return results
+}
+
+func getLocation(f *Finding) []Locations {
+	return []Locations{
+		{
+			PhysicalLocation: PhysicalLocation{
+				ArtifactLocation: ArtifactLocation{
+					URI: f.File,
+				},
+				Region: Region{
+					StartLine:   f.StartLine,
+					EndLine:     f.EndLine,
+					StartColumn: f.StartColumn,
+					EndColumn:   f.EndColumn,
+					Snippet: Snippet{
+						Text: f.Secret,
+					},
+				},
+			},
+		},
+	}
+}
+
+type PartialFingerPrints struct {
+	CommitSha     string `json:"commitSha"`
+	Email         string `json:"email"`
+	Author        string `json:"author"`
+	Date          string `json:"date"`
+	CommitMessage string `json:"commitMessage"`
+}
+
+type Sarif struct {
+	Schema  string `json:"$schema"`
+	Version string `json:"version"`
+	Runs    []Runs `json:"runs"`
+}
+
+type ShortDescription struct {
+	Text string `json:"text"`
+}
+
+type FullDescription struct {
+	Text string `json:"text"`
+}
+
+type Rules struct {
+	ID          string           `json:"id"`
+	Name        string           `json:"name"`
+	Description ShortDescription `json:"shortDescription"`
+}
+
+type Driver struct {
+	Name            string  `json:"name"`
+	SemanticVersion string  `json:"semanticVersion"`
+	Rules           []Rules `json:"rules"`
+}
+
+type Tool struct {
+	Driver Driver `json:"driver"`
+}
+
+type Message struct {
+	Text string `json:"text"`
+}
+
+type ArtifactLocation struct {
+	URI string `json:"uri"`
+}
+
+type Region struct {
+	StartLine   int     `json:"startLine"`
+	StartColumn int     `json:"startColumn"`
+	EndLine     int     `json:"endLine"`
+	EndColumn   int     `json:"endColumn"`
+	Snippet     Snippet `json:"snippet"`
+}
+
+type Snippet struct {
+	Text string `json:"text"`
+}
+
+type PhysicalLocation struct {
+	ArtifactLocation ArtifactLocation `json:"artifactLocation"`
+	Region           Region           `json:"region"`
+}
+
+type Locations struct {
+	PhysicalLocation PhysicalLocation `json:"physicalLocation"`
+}
+
+type Results struct {
+	Message             Message     `json:"message"`
+	RuleId              string      `json:"ruleId"`
+	Locations           []Locations `json:"locations"`
+	PartialFingerPrints `json:"partialFingerprints"`
+}
+
+type Runs struct {
+	Tool    Tool      `json:"tool"`
+	Results []Results `json:"results"`
+}
+
+func configToRules(cfg config.Config) []Rules {
+	var rules []Rules
+	for _, rule := range cfg.Rules {
+		rules = append(rules, Rules{
+			ID:   rule.RuleID,
+			Name: rule.Description,
+		})
+	}
+	return rules
+}

+ 105 - 0
report/sarif_test.go

@@ -0,0 +1,105 @@
+package report
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/spf13/viper"
+	"github.com/zricethezav/gitleaks/v8/config"
+)
+
+const configPath = "../testdata/config/"
+
+func TestWriteSarif(t *testing.T) {
+	tests := []struct {
+		findings       []*Finding
+		testReportName string
+		expected       string
+		wantEmpty      bool
+		cfgName        string
+	}{
+		{
+			cfgName:        "simple",
+			testReportName: "simple",
+			expected:       filepath.Join(expectPath, "report", "sarif_simple.sarif"),
+			findings: []*Finding{
+				{
+
+					Description: "",
+					RuleID:      "test-rule",
+					Context:     "line containing secret",
+					Secret:      "a secret",
+					StartLine:   1,
+					EndLine:     2,
+					StartColumn: 1,
+					EndColumn:   2,
+					Message:     "opps",
+					File:        "auth.py",
+					Commit:      "0000000000000000",
+					Author:      "John Doe",
+					Email:       "johndoe@gmail.com",
+					Date:        "10-19-2003",
+					Tags:        []string{},
+				},
+			}},
+	}
+
+	for _, test := range tests {
+		// create tmp file using os.TempDir()
+		tmpfile, err := os.Create(filepath.Join(tmpPath, test.testReportName+".json"))
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		viper.Reset()
+		viper.AddConfigPath(configPath)
+		viper.SetConfigName(test.cfgName)
+		viper.SetConfigType("toml")
+		err = viper.ReadInConfig()
+		if err != nil {
+			t.Error(err)
+		}
+
+		var vc config.ViperConfig
+		viper.Unmarshal(&vc)
+		cfg, err := vc.Translate()
+		if err != nil {
+			t.Error(err)
+		}
+		err = writeSarif(cfg, test.findings, tmpfile)
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		got, err := os.ReadFile(tmpfile.Name())
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		if test.wantEmpty {
+			if len(got) > 0 {
+				os.Remove(tmpfile.Name())
+				t.Errorf("Expected empty file, got %s", got)
+			}
+			os.Remove(tmpfile.Name())
+			continue
+		}
+		want, err := os.ReadFile(test.expected)
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+
+		if string(got) != string(want) {
+			err = os.WriteFile(strings.Replace(test.expected, ".sarif", ".got.sarif", 1), got, 0644)
+			if err != nil {
+				t.Error(err)
+			}
+			t.Errorf("got %s, want %s", string(got), string(want))
+		}
+
+		os.Remove(tmpfile.Name())
+	}
+}

+ 0 - 158
scan/commit.go

@@ -1,158 +0,0 @@
-package scan
-
-import (
-	"fmt"
-	"path/filepath"
-	"strings"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-
-	"github.com/go-git/go-git/v5"
-	fdiff "github.com/go-git/go-git/v5/plumbing/format/diff"
-	"github.com/go-git/go-git/v5/plumbing/object"
-)
-
-// CommitScanner is a commit scanner
-type CommitScanner struct {
-	cfg      config.Config
-	opts     options.Options
-	repo     *git.Repository
-	repoName string
-	commit   *object.Commit
-}
-
-// NewCommitScanner creates and returns a commit scanner
-func NewCommitScanner(opts options.Options, cfg config.Config, repo *git.Repository, commit *object.Commit) *CommitScanner {
-	cs := &CommitScanner{
-		cfg:      cfg,
-		opts:     opts,
-		repo:     repo,
-		commit:   commit,
-		repoName: getRepoName(opts),
-	}
-	return cs
-}
-
-// SetRepoName sets the repo name of the scanner.
-func (cs *CommitScanner) SetRepoName(repoName string) {
-	cs.repoName = repoName
-}
-
-// Scan kicks off a CommitScanner Scan
-func (cs *CommitScanner) Scan() (Report, error) {
-	var scannerReport Report
-
-	defer func() {
-		if err := recover(); err != nil {
-			// sometimes the Patch generation will fail due to a known bug in
-			// sergi's go-diff: https://github.com/sergi/go-diff/issues/89.
-			return
-		}
-	}()
-
-	if cs.cfg.Allowlist.CommitAllowed(cs.commit.Hash.String()) {
-		return scannerReport, nil
-	}
-
-	if len(cs.commit.ParentHashes) == 0 {
-		facScanner := NewFilesAtCommitScanner(cs.opts, cs.cfg, cs.repo, cs.commit)
-		return facScanner.Scan()
-	}
-
-	parent, err := cs.commit.Parent(0)
-	if err != nil {
-		return scannerReport, err
-	}
-
-	if parent == nil {
-		return scannerReport, nil
-	}
-
-	patch, err := parent.Patch(cs.commit)
-	if err != nil || patch == nil {
-		return scannerReport, fmt.Errorf("could not generate Patch")
-	}
-
-	patchContent := patch.String()
-
-	for _, f := range patch.FilePatches() {
-		if f.IsBinary() {
-			continue
-		}
-		for _, chunk := range f.Chunks() {
-			if chunk.Type() == fdiff.Add {
-				_, to := f.Files()
-				if cs.cfg.Allowlist.FileAllowed(filepath.Base(to.Path())) ||
-					cs.cfg.Allowlist.PathAllowed(to.Path()) {
-					continue
-				}
-
-				// Check individual file path ONLY rules
-				for _, rule := range cs.cfg.Rules {
-					if rule.CommitAllowed(cs.commit.Hash.String()) {
-						continue
-					}
-
-					if rule.HasFileOrPathLeakOnly(to.Path()) {
-						leak := NewLeak("", "Filename or path offender: "+to.Path(), defaultLineNumber).WithCommit(cs.commit)
-						leak.Repo = cs.repoName
-						leak.File = to.Path()
-						leak.RepoURL = cs.opts.RepoURL
-						leak.LeakURL = leak.URL()
-						leak.Rule = rule.Description
-						leak.Tags = strings.Join(rule.Tags, ", ")
-
-						leak.Log(cs.opts)
-
-						scannerReport.Leaks = append(scannerReport.Leaks, leak)
-						continue
-					}
-				}
-
-				lineLookup := make(map[string]bool)
-
-				// Check the actual content
-				for _, line := range strings.Split(chunk.Content(), "\n") {
-					for _, rule := range cs.cfg.Rules {
-						if rule.AllowList.FileAllowed(filepath.Base(to.Path())) ||
-							rule.AllowList.PathAllowed(to.Path()) ||
-							rule.AllowList.CommitAllowed(cs.commit.Hash.String()) {
-							continue
-						}
-						offender := rule.Inspect(line)
-						if offender.IsEmpty() {
-							continue
-						}
-
-						if cs.cfg.Allowlist.RegexAllowed(line) {
-							continue
-						}
-
-						if rule.File.String() != "" && !rule.HasFileLeak(filepath.Base(to.Path())) {
-							continue
-						}
-						if rule.Path.String() != "" && !rule.HasFilePathLeak(to.Path()) {
-							continue
-						}
-
-						leak := NewLeak(line, offender.ToString(), defaultLineNumber).WithCommit(cs.commit).WithEntropy(offender.EntropyLevel)
-						leak.File = to.Path()
-						leak.LineNumber = extractLine(patchContent, leak, lineLookup)
-						leak.RepoURL = cs.opts.RepoURL
-						leak.Repo = cs.repoName
-						leak.LeakURL = leak.URL()
-						leak.Rule = rule.Description
-						leak.Tags = strings.Join(rule.Tags, ", ")
-
-						leak.Log(cs.opts)
-
-						scannerReport.Leaks = append(scannerReport.Leaks, leak)
-					}
-				}
-			}
-		}
-	}
-	scannerReport.Commits = 1
-	return scannerReport, nil
-}

+ 0 - 90
scan/commit_test.go

@@ -1,90 +0,0 @@
-package scan_test
-
-import (
-	"path/filepath"
-	"testing"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-	"github.com/zricethezav/gitleaks/v7/scan"
-)
-
-func TestCommitScan(t *testing.T) {
-	err := moveDotGit("dotGit", ".git")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer moveDotGit(".git", "dotGit")
-	tests := []struct {
-		description string
-		opts        options.Options
-		wantPath    string
-		empty       bool
-	}{
-		{
-			description: "empty repo",
-			opts: options.Options{
-				Path:   filepath.Join(repoBasePath, "empty"),
-				Report: filepath.Join(expectPath, "empty", "empty_report.json.got"),
-			},
-			empty: true,
-		},
-		{
-			description: "basic repo with default config at specific commit",
-			opts: options.Options{
-				Path:         filepath.Join(repoBasePath, "basic"),
-				Report:       filepath.Join(expectPath, "basic", "results_208ae46.json.got"),
-				ReportFormat: "json",
-				Commit:       "208ae4669ade2563fcaf9f12922fa2c0a5b37c63",
-			},
-			wantPath: filepath.Join(expectPath, "basic", "results_208ae46.json"),
-		},
-		{
-			description: "basic repo with custom config at specific commit",
-			opts: options.Options{
-				Path:           filepath.Join(repoBasePath, "with_config"),
-				Report:         filepath.Join(expectPath, "with_config", "results_e7c0aff3.json.got"),
-				ReportFormat:   "json",
-				RepoConfigPath: "gitleaks.toml",
-				Commit:         "e7c0aff3e8a60b50a85432fdf933f8beff013743",
-			},
-			wantPath: filepath.Join(expectPath, "with_config", "results_e7c0aff3.json"),
-		},
-	}
-
-	for _, test := range tests {
-		cfg, err := config.NewConfig(test.opts)
-		if err != nil {
-			t.Error(err)
-		}
-
-		scanner, err := scan.NewScanner(test.opts, cfg)
-		if err != nil {
-			t.Error(test.description, err)
-		}
-
-		scannerReport, err := scanner.Scan()
-		if err != nil {
-			t.Fatal(test.description, err)
-		}
-
-		err = scan.WriteReport(scannerReport, test.opts, cfg)
-		if err != nil {
-			t.Error(test.description, err)
-		}
-
-		if test.empty {
-			if len(scannerReport.Leaks) != 0 {
-				t.Errorf("%s wanted no leaks but got some instead: %+v", test.description, scannerReport.Leaks)
-			}
-			continue
-		}
-
-		if test.wantPath != "" {
-			err := fileCheck(test.wantPath, test.opts.Report)
-			if err != nil {
-				t.Error(test.description, err)
-			}
-		}
-	}
-}

+ 0 - 50
scan/commits.go

@@ -1,50 +0,0 @@
-package scan
-
-import (
-	"github.com/go-git/go-git/v5"
-	log "github.com/sirupsen/logrus"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-)
-
-// CommitsScanner is a commit scanner
-type CommitsScanner struct {
-	opts options.Options
-	cfg  config.Config
-
-	repo     *git.Repository
-	repoName string
-	commits  []string
-}
-
-// NewCommitsScanner creates and returns a commits scanner, notice the 's' in commits
-func NewCommitsScanner(opts options.Options, cfg config.Config, repo *git.Repository, commits []string) *CommitsScanner {
-	return &CommitsScanner{
-		opts:     opts,
-		cfg:      cfg,
-		repo:     repo,
-		commits:  commits,
-		repoName: getRepoName(opts),
-	}
-}
-
-// Scan kicks off a CommitsScanner Scan
-func (css *CommitsScanner) Scan() (Report, error) {
-	var scannerReport Report
-	for _, commitHash := range css.commits {
-		c, err := obtainCommit(css.repo, commitHash)
-		if err != nil {
-			log.Errorf("skipping %s, err: %v", commitHash, err)
-			continue
-		}
-		cs := NewCommitScanner(css.opts, css.cfg, css.repo, c)
-		commitReport, err := cs.Scan()
-		if err != nil {
-			return scannerReport, err
-		}
-		scannerReport.Leaks = append(scannerReport.Leaks, commitReport.Leaks...)
-		scannerReport.Commits++
-	}
-	return scannerReport, nil
-}

+ 0 - 102
scan/commits_test.go

@@ -1,102 +0,0 @@
-package scan_test
-
-import (
-	"path/filepath"
-	"testing"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-	"github.com/zricethezav/gitleaks/v7/scan"
-)
-
-func TestCommitsScan(t *testing.T) {
-	err := moveDotGit("dotGit", ".git")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer moveDotGit(".git", "dotGit")
-	tests := []struct {
-		description string
-		opts        options.Options
-		wantPath    string
-		empty       bool
-	}{
-		{
-			description: "basic repo with default config ranging first and third commit",
-			opts: options.Options{
-				Path:         filepath.Join(repoBasePath, "basic"),
-				Report:       filepath.Join(expectPath, "basic", "results_ae8db4a2_208ae46.json.got"),
-				ReportFormat: "json",
-				Commits:      "ae8db4a2306798fcb3a5b9cbe8c486027fc1931f,208ae4669ade2563fcaf9f12922fa2c0a5b37c63",
-			},
-			wantPath: filepath.Join(expectPath, "basic", "results_ae8db4a2_208ae46.json"),
-		},
-		{
-			description: "repo with config first two commits",
-			opts: options.Options{
-				Path:           filepath.Join(repoBasePath, "with_config"),
-				Report:         filepath.Join(expectPath, "with_config", "results_ae8db4a_e7c0aff.json.got"),
-				ReportFormat:   "json",
-				RepoConfigPath: "gitleaks.toml",
-				Commits:        "ae8db4a2306798fcb3a5b9cbe8c486027fc1931f,e7c0aff3e8a60b50a85432fdf933f8beff013743",
-			},
-			wantPath: filepath.Join(expectPath, "with_config", "results_ae8db4a_e7c0aff.json"),
-		},
-		{
-			description: "basic repo with depth=1",
-			opts: options.Options{
-				Path:         filepath.Join(repoBasePath, "basic"),
-				Report:       filepath.Join(expectPath, "basic", "results_depth_1.json.got"),
-				ReportFormat: "json",
-				Depth:        1,
-			},
-			wantPath: filepath.Join(expectPath, "basic", "results_depth_1.json"),
-		},
-		{
-			description: "basic repo with default config ranging first and third commit with a non-existent commit in middle",
-			opts: options.Options{
-				Path:         filepath.Join(repoBasePath, "basic"),
-				Report:       filepath.Join(expectPath, "basic", "results_ae8db4a2_208ae46.json.got"),
-				ReportFormat: "json",
-				Commits:      "ae8db4a2306798fcb3a5b9cbe8c486027fc1931f,nocommithere,208ae4669ade2563fcaf9f12922fa2c0a5b37c63",
-			},
-			wantPath: filepath.Join(expectPath, "basic", "results_ae8db4a2_208ae46.json"),
-		},
-	}
-
-	for _, test := range tests {
-		cfg, err := config.NewConfig(test.opts)
-		if err != nil {
-			t.Error(err)
-		}
-
-		scanner, err := scan.NewScanner(test.opts, cfg)
-		if err != nil {
-			t.Error(test.description, err)
-		}
-
-		scannerReport, err := scanner.Scan()
-		if err != nil {
-			t.Fatal(test.description, err)
-		}
-
-		err = scan.WriteReport(scannerReport, test.opts, cfg)
-		if err != nil {
-			t.Error(test.description, err)
-		}
-
-		if test.empty {
-			if len(scannerReport.Leaks) != 0 {
-				t.Errorf("%s wanted no leaks but got some instead: %+v", test.description, scannerReport.Leaks)
-			}
-			continue
-		}
-
-		if test.wantPath != "" {
-			err := fileCheck(test.wantPath, test.opts.Report)
-			if err != nil {
-				t.Error(test.description, err)
-			}
-		}
-	}
-}

+ 0 - 135
scan/filesatcommit.go

@@ -1,135 +0,0 @@
-package scan
-
-import (
-	"path/filepath"
-	"strings"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-
-	"github.com/go-git/go-git/v5"
-	"github.com/go-git/go-git/v5/plumbing/object"
-)
-
-// FilesAtCommitScanner is a files at commit scanner. This differs from CommitScanner
-// as CommitScanner generates patches that are scanned. FilesAtCommitScanner instead looks at
-// files available at a commit's worktree and scans the entire content of said files.
-// Apologies for the awful struct name...
-type FilesAtCommitScanner struct {
-	opts     options.Options
-	cfg      config.Config
-	repo     *git.Repository
-	commit   *object.Commit
-	repoName string
-}
-
-// NewFilesAtCommitScanner creates and returns a files at commit scanner
-func NewFilesAtCommitScanner(opts options.Options, cfg config.Config, repo *git.Repository, commit *object.Commit) *FilesAtCommitScanner {
-	fs := &FilesAtCommitScanner{
-		opts:     opts,
-		cfg:      cfg,
-		repo:     repo,
-		commit:   commit,
-		repoName: getRepoName(opts),
-	}
-	return fs
-}
-
-// Scan kicks off a FilesAtCommitScanner Scan
-func (fs *FilesAtCommitScanner) Scan() (Report, error) {
-	var scannerReport Report
-
-	if fs.cfg.Allowlist.CommitAllowed(fs.commit.Hash.String()) {
-		return scannerReport, nil
-	}
-
-	fIter, err := fs.commit.Files()
-	if err != nil {
-		return scannerReport, err
-	}
-
-	err = fIter.ForEach(func(f *object.File) error {
-		bin, err := f.IsBinary()
-		if bin {
-			return nil
-		} else if err != nil {
-			return err
-		}
-
-		if fs.cfg.Allowlist.FileAllowed(filepath.Base(f.Name)) ||
-			fs.cfg.Allowlist.PathAllowed(f.Name) {
-			return nil
-		}
-
-		content, err := f.Contents()
-		if err != nil {
-			return err
-		}
-
-		// Check individual file path ONLY rules
-		for _, rule := range fs.cfg.Rules {
-			if rule.CommitAllowed(fs.commit.Hash.String()) {
-				continue
-			}
-
-			if rule.HasFileOrPathLeakOnly(f.Name) {
-				leak := NewLeak("", "Filename or path offender: "+f.Name, defaultLineNumber).WithCommit(fs.commit)
-				leak.Repo = fs.repoName
-				leak.File = f.Name
-				leak.RepoURL = fs.opts.RepoURL
-				leak.LeakURL = leak.URL()
-				leak.Rule = rule.Description
-				leak.Tags = strings.Join(rule.Tags, ", ")
-
-				leak.Log(fs.opts)
-
-				scannerReport.Leaks = append(scannerReport.Leaks, leak)
-				continue
-			}
-		}
-
-		for i, line := range strings.Split(content, "\n") {
-			for _, rule := range fs.cfg.Rules {
-				if rule.AllowList.FileAllowed(filepath.Base(f.Name)) ||
-					rule.AllowList.PathAllowed(f.Name) ||
-					rule.AllowList.CommitAllowed(fs.commit.Hash.String()) {
-					continue
-				}
-
-				offender := rule.Inspect(line)
-				if offender.IsEmpty() {
-					continue
-				}
-
-				if fs.cfg.Allowlist.RegexAllowed(line) {
-					continue
-				}
-
-				if rule.File.String() != "" && !rule.HasFileLeak(filepath.Base(f.Name)) {
-					continue
-				}
-				if rule.Path.String() != "" && !rule.HasFilePathLeak(f.Name) {
-					continue
-				}
-
-				leak := NewLeak(line, offender.ToString(), defaultLineNumber).WithCommit(fs.commit).WithEntropy(offender.EntropyLevel)
-				leak.File = f.Name
-				leak.LineNumber = i + 1
-				leak.RepoURL = fs.opts.RepoURL
-				leak.Repo = fs.repoName
-				leak.LeakURL = leak.URL()
-				leak.Rule = rule.Description
-				leak.Tags = strings.Join(rule.Tags, ", ")
-
-				leak.Log(fs.opts)
-
-				scannerReport.Leaks = append(scannerReport.Leaks, leak)
-			}
-		}
-
-		return nil
-	})
-
-	scannerReport.Commits = 1
-	return scannerReport, err
-}

+ 0 - 81
scan/filesatcommit_test.go

@@ -1,81 +0,0 @@
-package scan_test
-
-import (
-	"path/filepath"
-	"testing"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-	"github.com/zricethezav/gitleaks/v7/scan"
-)
-
-func TestFilesAtCommitScan(t *testing.T) {
-	err := moveDotGit("dotGit", ".git")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer moveDotGit(".git", "dotGit")
-	tests := []struct {
-		description string
-		opts        options.Options
-		wantPath    string
-		empty       bool
-	}{
-		{
-			description: "basic repo with no secrets present in files at first commit",
-			opts: options.Options{
-				Path:          filepath.Join(repoBasePath, "basic"),
-				Report:        filepath.Join(expectPath, "basic", "results_files_at_ae8db4a2.json.got"),
-				ReportFormat:  "json",
-				FilesAtCommit: "ae8db4a2306798fcb3a5b9cbe8c486027fc1931f",
-			},
-			empty: true,
-		},
-		{
-			description: "basic repo with secrets present in files at third commit",
-			opts: options.Options{
-				Path:          filepath.Join(repoBasePath, "basic"),
-				Report:        filepath.Join(expectPath, "basic", "results_files_at_208ae46.json.got"),
-				ReportFormat:  "json",
-				FilesAtCommit: "208ae4669ade2563fcaf9f12922fa2c0a5b37c63",
-			},
-			wantPath: filepath.Join(expectPath, "basic", "results_files_at_208ae46.json"),
-		},
-	}
-
-	for _, test := range tests {
-		cfg, err := config.NewConfig(test.opts)
-		if err != nil {
-			t.Error(err)
-		}
-
-		scanner, err := scan.NewScanner(test.opts, cfg)
-		if err != nil {
-			t.Error(test.description, err)
-		}
-
-		scannerReport, err := scanner.Scan()
-		if err != nil {
-			t.Fatal(test.description, err)
-		}
-
-		err = scan.WriteReport(scannerReport, test.opts, cfg)
-		if err != nil {
-			t.Error(test.description, err)
-		}
-
-		if test.empty {
-			if len(scannerReport.Leaks) != 0 {
-				t.Errorf("%s wanted no leaks but got some instead: %+v", test.description, scannerReport.Leaks)
-			}
-			continue
-		}
-
-		if test.wantPath != "" {
-			err := fileCheck(test.wantPath, test.opts.Report)
-			if err != nil {
-				t.Error(test.description, err)
-			}
-		}
-	}
-}

+ 0 - 94
scan/leak.go

@@ -1,94 +0,0 @@
-package scan
-
-import (
-	"encoding/json"
-	"fmt"
-	"math"
-	"strings"
-	"time"
-
-	"github.com/zricethezav/gitleaks/v7/options"
-
-	"github.com/go-git/go-git/v5/plumbing/object"
-)
-
-// Leak is a struct that contains information about some line of code that contains
-// sensitive information as determined by the rules set in a gitleaks config
-type Leak struct {
-	Line            string    `json:"line"`
-	LineNumber      int       `json:"lineNumber"`
-	Offender        string    `json:"offender"`
-	OffenderEntropy float64   `json:"offenderEntropy"`
-	Commit          string    `json:"commit"`
-	Repo            string    `json:"repo"`
-	RepoURL         string    `json:"repoURL"`
-	LeakURL         string    `json:"leakURL"`
-	Rule            string    `json:"rule"`
-	Message         string    `json:"commitMessage"`
-	Author          string    `json:"author"`
-	Email           string    `json:"email"`
-	File            string    `json:"file"`
-	Date            time.Time `json:"date"`
-	Tags            string    `json:"tags"`
-}
-
-// RedactLeak will replace the offending string with "REDACTED" in both
-// the offender and line field of the leak which.
-func RedactLeak(leak Leak) Leak {
-	leak.Line = strings.Replace(leak.Line, leak.Offender, "REDACTED", -1)
-	leak.Offender = "REDACTED"
-	return leak
-}
-
-// NewLeak creates a new leak from common data all leaks must have, line, offender, linenumber
-func NewLeak(line string, offender string, lineNumber int) Leak {
-	return Leak{
-		Line:            line,
-		Offender:        offender,
-		LineNumber:      lineNumber,
-		OffenderEntropy: -1, // -1 means not checked
-	}
-}
-
-// WithCommit adds commit data to the leak
-func (leak Leak) WithCommit(commit *object.Commit) Leak {
-	leak.Commit = commit.Hash.String()
-	leak.Author = commit.Author.Name
-	leak.Email = commit.Author.Email
-	leak.Message = commit.Message
-	leak.Date = commit.Author.When
-	return leak
-}
-
-// WithEntropy adds OffenderEntropy data to the leak
-func (leak Leak) WithEntropy(entropyLevel float64) Leak {
-	leak.OffenderEntropy = math.Round(entropyLevel*1000) / 1000
-	return leak
-}
-
-// Log logs a leak and redacts if necessary
-func (leak Leak) Log(opts options.Options) {
-	if !opts.Quiet && !opts.Verbose {
-		return
-	}
-	if opts.Redact {
-		leak = RedactLeak(leak)
-	}
-	if opts.Quiet {
-		var b []byte
-		b, _ = json.Marshal(leak)
-		fmt.Println(string(b))
-	} else {
-		var b []byte
-		b, _ = json.MarshalIndent(leak, "", "	")
-		fmt.Println(string(b))
-	}
-}
-
-// URL generates a url to the leak if leak.RepoURL is set
-func (leak Leak) URL() string {
-	if leak.RepoURL != "" {
-		return fmt.Sprintf("%s/blob/%s/%s#L%d", leak.RepoURL, leak.Commit, leak.File, leak.LineNumber)
-	}
-	return ""
-}

+ 0 - 152
scan/nogit.go

@@ -1,152 +0,0 @@
-package scan
-
-import (
-	"bufio"
-	"context"
-	"os"
-	"path/filepath"
-	"strings"
-	"sync"
-
-	log "github.com/sirupsen/logrus"
-	"golang.org/x/sync/errgroup"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-)
-
-// NoGitScanner is a scanner that absolutely despises git
-type NoGitScanner struct {
-	opts     options.Options
-	cfg      config.Config
-	throttle *Throttle
-	mtx      *sync.Mutex
-}
-
-// NewNoGitScanner creates and returns a nogit scanner. This is used for scanning files and directories
-func NewNoGitScanner(opts options.Options, cfg config.Config) *NoGitScanner {
-	ngs := &NoGitScanner{
-		opts:     opts,
-		cfg:      cfg,
-		throttle: NewThrottle(opts),
-		mtx:      &sync.Mutex{},
-	}
-
-	// no-git scans should ignore .git folders by default
-	// issue: https://github.com/zricethezav/gitleaks/issues/474
-	// ngs.cfg.Allowlist
-	err := ngs.cfg.Allowlist.IgnoreDotGit()
-	if err != nil {
-		log.Error(err)
-		return nil
-	}
-	return ngs
-}
-
-// Scan kicks off a NoGitScanner Scan
-func (ngs *NoGitScanner) Scan() (Report, error) {
-	var scannerReport Report
-	g, _ := errgroup.WithContext(context.Background())
-	paths := make(chan string)
-	g.Go(func() error {
-		defer close(paths)
-		return filepath.Walk(ngs.opts.Path,
-			func(path string, fInfo os.FileInfo, err error) error {
-				if err != nil {
-					return err
-				}
-				if fInfo.Mode().IsRegular() {
-					paths <- path
-				}
-				return nil
-			})
-	})
-
-	for path := range paths {
-		p := path
-		ngs.throttle.Limit()
-		g.Go(func() error {
-			defer ngs.throttle.Release()
-			if ngs.cfg.Allowlist.FileAllowed(filepath.Base(p)) ||
-				ngs.cfg.Allowlist.PathAllowed(p) {
-				return nil
-			}
-
-			for _, rule := range ngs.cfg.Rules {
-				if rule.HasFileOrPathLeakOnly(p) {
-					leak := NewLeak("", "Filename or path offender: "+p, defaultLineNumber)
-					relPath, err := filepath.Rel(ngs.opts.Path, p)
-					if err != nil {
-						leak.File = p
-					} else {
-						leak.File = relPath
-					}
-					leak.Rule = rule.Description
-					leak.Tags = strings.Join(rule.Tags, ", ")
-
-					leak.Log(ngs.opts)
-
-					ngs.mtx.Lock()
-					scannerReport.Leaks = append(scannerReport.Leaks, leak)
-					ngs.mtx.Unlock()
-				}
-			}
-
-			f, err := os.Open(p) // #nosec
-			if err != nil {
-				return err
-			}
-			scanner := bufio.NewScanner(f)
-			lineNumber := 0
-			for scanner.Scan() {
-				lineNumber++
-				for _, rule := range ngs.cfg.Rules {
-					line := scanner.Text()
-
-					if rule.AllowList.FileAllowed(filepath.Base(p)) ||
-						rule.AllowList.PathAllowed(p) {
-						continue
-					}
-
-					offender := rule.Inspect(line)
-					if offender.IsEmpty() {
-						continue
-					}
-					if ngs.cfg.Allowlist.RegexAllowed(line) {
-						continue
-					}
-
-					if rule.File.String() != "" && !rule.HasFileLeak(filepath.Base(p)) {
-						continue
-					}
-					if rule.Path.String() != "" && !rule.HasFilePathLeak(p) {
-						continue
-					}
-
-					leak := NewLeak(line, offender.ToString(), defaultLineNumber).WithEntropy(offender.EntropyLevel)
-					relPath, err := filepath.Rel(ngs.opts.Path, p)
-					if err != nil {
-						leak.File = p
-					} else {
-						leak.File = relPath
-					}
-					leak.LineNumber = lineNumber
-					leak.Rule = rule.Description
-					leak.Tags = strings.Join(rule.Tags, ", ")
-					leak.Log(ngs.opts)
-
-					ngs.mtx.Lock()
-					scannerReport.Leaks = append(scannerReport.Leaks, leak)
-					ngs.mtx.Unlock()
-				}
-			}
-			return f.Close()
-		})
-	}
-
-	if err := g.Wait(); err != nil {
-		log.Error(err)
-	}
-
-	return scannerReport, nil
-}

+ 0 - 81
scan/nogit_test.go

@@ -1,81 +0,0 @@
-package scan_test
-
-import (
-	"path/filepath"
-	"testing"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-	"github.com/zricethezav/gitleaks/v7/scan"
-)
-
-func TestNoGit(t *testing.T) {
-	err := moveDotGit("dotGit", ".git")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer moveDotGit(".git", "dotGit")
-	tests := []struct {
-		description string
-		opts        options.Options
-		wantPath    string
-		empty       bool
-	}{
-		{
-			description: "[nogit] basic repo",
-			opts: options.Options{
-				Path:         filepath.Join(repoBasePath, "basic"),
-				Report:       filepath.Join(expectPath, "basic", "results_no_git.json.got"),
-				ReportFormat: "json",
-				NoGit:        true,
-			},
-			wantPath: filepath.Join(expectPath, "basic", "results_no_git.json"),
-		},
-		{
-			description: "[nogit] empty",
-			opts: options.Options{
-				Path:         filepath.Join(repoBasePath, "empty"),
-				Report:       filepath.Join(expectPath, "empty", "results_no_git_empty.json.got"),
-				ReportFormat: "json",
-				NoGit:        true,
-			},
-			empty: true,
-		},
-	}
-
-	for _, test := range tests {
-		cfg, err := config.NewConfig(test.opts)
-		if err != nil {
-			t.Error(err)
-		}
-
-		scanner, err := scan.NewScanner(test.opts, cfg)
-		if err != nil {
-			t.Error(test.description, err)
-		}
-
-		scannerReport, err := scanner.Scan()
-		if err != nil {
-			t.Fatal(test.description, err)
-		}
-
-		err = scan.WriteReport(scannerReport, test.opts, cfg)
-		if err != nil {
-			t.Error(test.description, err)
-		}
-
-		if test.empty {
-			if len(scannerReport.Leaks) != 0 {
-				t.Errorf("%s wanted no leaks but got some instead: %+v", test.description, scannerReport.Leaks)
-			}
-			continue
-		}
-
-		if test.wantPath != "" {
-			err := fileCheck(test.wantPath, test.opts.Report)
-			if err != nil {
-				t.Error(test.description, err)
-			}
-		}
-	}
-}

+ 0 - 75
scan/parent.go

@@ -1,75 +0,0 @@
-package scan
-
-import (
-	"io/ioutil"
-	"path/filepath"
-
-	"github.com/zricethezav/gitleaks/v7/options"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-
-	"github.com/go-git/go-git/v5"
-	log "github.com/sirupsen/logrus"
-)
-
-// ParentScanner is a parent directory scanner
-type ParentScanner struct {
-	cfg  config.Config
-	opts options.Options
-}
-
-// NewParentScanner creates and returns a directory scanner
-func NewParentScanner(opts options.Options, cfg config.Config) *ParentScanner {
-	ds := &ParentScanner{
-		opts: opts,
-		cfg:  cfg,
-	}
-	return ds
-}
-
-// Scan kicks off a ParentScanner scan. This uses the directory from --path to discovery repos
-func (ds *ParentScanner) Scan() (Report, error) {
-	var scannerReport Report
-	log.Debugf("scanning repos in %s\n", ds.opts.Path)
-
-	files, err := ioutil.ReadDir(ds.opts.Path)
-	if err != nil {
-		return scannerReport, err
-	}
-	for _, f := range files {
-		if !f.IsDir() {
-			continue
-		}
-
-		repo, err := git.PlainOpen(filepath.Join(ds.opts.Path, f.Name()))
-		if err != nil {
-			if err.Error() == "repository does not exist" {
-				log.Debugf("%s is not a git repository", f.Name())
-				continue
-			}
-			return scannerReport, err
-		}
-		if ds.cfg.Allowlist.RepoAllowed(f.Name()) {
-			continue
-		}
-
-		if ds.opts.RepoConfigPath != "" {
-			cfg, err := config.LoadRepoConfig(repo, ds.opts.RepoConfigPath)
-			if err != nil {
-				log.Warn(err)
-			} else {
-				ds.cfg = cfg
-			}
-		}
-
-		rs := NewRepoScanner(ds.opts, ds.cfg, repo)
-		rs.repoName = f.Name()
-		repoReport, err := rs.Scan()
-		if err != nil {
-			return scannerReport, err
-		}
-		scannerReport.Leaks = append(scannerReport.Leaks, repoReport.Leaks...)
-		scannerReport.Commits += repoReport.Commits
-	}
-	return scannerReport, nil
-}

+ 0 - 115
scan/repo.go

@@ -1,115 +0,0 @@
-package scan
-
-import (
-	"context"
-	"sync"
-
-	"golang.org/x/sync/errgroup"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-
-	"github.com/go-git/go-git/v5"
-	"github.com/go-git/go-git/v5/plumbing/object"
-	"github.com/go-git/go-git/v5/plumbing/storer"
-	log "github.com/sirupsen/logrus"
-)
-
-// RepoScanner is a repo scanner
-type RepoScanner struct {
-	opts     options.Options
-	cfg      config.Config
-	repo     *git.Repository
-	throttle *Throttle
-	repoName string
-	mtx      *sync.Mutex
-}
-
-// NewRepoScanner returns a new repo scanner (go figure). This function also
-// sets up the leak listener for multi-threaded awesomeness.
-func NewRepoScanner(opts options.Options, cfg config.Config, repo *git.Repository) *RepoScanner {
-	rs := &RepoScanner{
-		opts:     opts,
-		cfg:      cfg,
-		repo:     repo,
-		throttle: NewThrottle(opts),
-		repoName: getRepoName(opts),
-		mtx:      &sync.Mutex{},
-	}
-
-	return rs
-}
-
-// Scan kicks of a repo scan
-func (rs *RepoScanner) Scan() (Report, error) {
-	var (
-		scannerReport Report
-		commits       chan *object.Commit
-	)
-	logOpts, err := logOptions(rs.repo, rs.opts)
-	if err != nil {
-		return scannerReport, err
-	}
-	cIter, err := rs.repo.Log(logOpts)
-	if err != nil {
-		return scannerReport, err
-	}
-
-	g, _ := errgroup.WithContext(context.Background())
-	commits = make(chan *object.Commit)
-
-	commitNum := 0
-	g.Go(func() error {
-		defer close(commits)
-		err = cIter.ForEach(func(c *object.Commit) error {
-			if c == nil || depthReached(commitNum, rs.opts) {
-				return storer.ErrStop
-			}
-
-			if rs.cfg.Allowlist.CommitAllowed(c.Hash.String()) {
-				return nil
-			}
-			commitNum++
-			commits <- c
-			if c.Hash.String() == rs.opts.CommitTo {
-				return storer.ErrStop
-			}
-
-			return err
-		})
-		cIter.Close()
-		return nil
-	})
-
-	for commit := range commits {
-		c := commit
-		rs.throttle.Limit()
-		g.Go(func() error {
-			commitScanner := NewCommitScanner(rs.opts, rs.cfg, rs.repo, c)
-			commitScanner.SetRepoName(rs.repoName)
-			report, err := commitScanner.Scan()
-			rs.throttle.Release()
-			if err != nil {
-				log.Error(err)
-			}
-			for _, leak := range report.Leaks {
-				rs.mtx.Lock()
-				scannerReport.Leaks = append(scannerReport.Leaks, leak)
-				rs.mtx.Unlock()
-			}
-			return nil
-		})
-	}
-
-	if err := g.Wait(); err != nil {
-		log.Error(err)
-	}
-
-	scannerReport.Commits = commitNum
-	return scannerReport, nil
-}
-
-// SetRepoName sets the repo name
-func (rs *RepoScanner) SetRepoName(repoName string) {
-	rs.repoName = repoName
-}

+ 0 - 88
scan/repo_test.go

@@ -1,88 +0,0 @@
-package scan_test
-
-import (
-	"path/filepath"
-	"testing"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-	"github.com/zricethezav/gitleaks/v7/scan"
-)
-
-func TestRepoScan(t *testing.T) {
-	err := moveDotGit("dotGit", ".git")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer moveDotGit(".git", "dotGit")
-	tests := []struct {
-		description string
-		opts        options.Options
-		wantPath    string
-		empty       bool
-	}{
-		{
-			description: "empty repo",
-			opts: options.Options{
-				Path:   filepath.Join(repoBasePath, "empty"),
-				Report: filepath.Join(expectPath, "empty", "empty_report.json.got"),
-			},
-			empty: true,
-		},
-		{
-			description: "basic repo with default config",
-			opts: options.Options{
-				Path:         filepath.Join(repoBasePath, "basic"),
-				Report:       filepath.Join(expectPath, "basic", "results.json.got"),
-				ReportFormat: "json",
-			},
-			wantPath: filepath.Join(expectPath, "basic", "results.json"),
-		},
-		{
-			description: "with_config repo",
-			opts: options.Options{
-				Path:           filepath.Join(repoBasePath, "with_config"),
-				Report:         filepath.Join(expectPath, "with_config", "results.json.got"),
-				RepoConfigPath: "gitleaks.toml",
-				ReportFormat:   "json",
-			},
-			wantPath: filepath.Join(expectPath, "with_config", "results.json"),
-		},
-	}
-
-	for _, test := range tests {
-		cfg, err := config.NewConfig(test.opts)
-		if err != nil {
-			t.Error(err)
-		}
-
-		scanner, err := scan.NewScanner(test.opts, cfg)
-		if err != nil {
-			t.Error(test.description, err)
-		}
-
-		scannerReport, err := scanner.Scan()
-		if err != nil {
-			t.Fatal(test.description, err)
-		}
-
-		err = scan.WriteReport(scannerReport, test.opts, cfg)
-		if err != nil {
-			t.Error(test.description, err)
-		}
-
-		if test.empty {
-			if len(scannerReport.Leaks) != 0 {
-				t.Errorf("%s wanted no leaks but got some instead: %+v", test.description, scannerReport.Leaks)
-			}
-			continue
-		}
-
-		if test.wantPath != "" {
-			err := fileCheck(test.wantPath, test.opts.Report)
-			if err != nil {
-				t.Error(test.description, err)
-			}
-		}
-	}
-}

+ 0 - 102
scan/report.go

@@ -1,102 +0,0 @@
-package scan
-
-import (
-	"encoding/csv"
-	"encoding/json"
-	"os"
-	"regexp"
-	"strings"
-	"time"
-
-	"github.com/sirupsen/logrus"
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-	"github.com/zricethezav/gitleaks/v7/version"
-)
-
-// Report is a container for leaks and number of commits scanned
-type Report struct {
-	Leaks   []Leak
-	Commits int
-}
-
-// WriteReport accepts a report and options and will write a report if --report has been set
-func WriteReport(report Report, opts options.Options, cfg config.Config) error {
-	if !(opts.NoGit || opts.CheckUncommitted()) {
-		logrus.Info("commits scanned: ", report.Commits)
-	}
-	if len(report.Leaks) != 0 {
-		logrus.Warn("leaks found: ", len(report.Leaks))
-	} else {
-		logrus.Info("No leaks found")
-		return nil
-	}
-
-	if opts.Report == "" {
-		return nil
-	} else {
-		if opts.Redact {
-			var redactedLeaks []Leak
-			for _, leak := range report.Leaks {
-				redactedLeaks = append(redactedLeaks, RedactLeak(leak))
-			}
-			report.Leaks = redactedLeaks
-		}
-
-		file, err := os.Create(opts.Report)
-		if err != nil {
-			return err
-		}
-		defer rable(file.Close)
-
-		switch strings.ToLower(opts.ReportFormat) {
-		case "json":
-			encoder := json.NewEncoder(file)
-			encoder.SetIndent("", " ")
-			err = encoder.Encode(report.Leaks)
-			if err != nil {
-				return err
-			}
-		case "csv":
-			newLineRegex := regexp.MustCompile("[\r]*\n")
-			w := csv.NewWriter(file)
-			err = w.Write([]string{"repo", "line", "commit", "offender", "leakURL", "rule", "tags", "commitMsg", "author", "email", "file", "date"})
-			if err != nil {
-				return err
-			}
-			for _, leak := range report.Leaks {
-				commitFirstLine := newLineRegex.ReplaceAllString(leak.Message, " ")
-				err := w.Write([]string{leak.Repo, leak.Line, leak.Commit, leak.Offender, leak.LeakURL, leak.Rule, leak.Tags, commitFirstLine, leak.Author, leak.Email, leak.File, leak.Date.Format(time.RFC3339)})
-				if err != nil {
-					return err
-				}
-			}
-			w.Flush()
-		case "sarif":
-			s := Sarif{
-				Schema:  "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json",
-				Version: "2.1.0",
-				Runs: []Runs{
-					{
-						Tool: Tool{
-							Driver: Driver{
-								Name:            "Gitleaks",
-								SemanticVersion: version.Version,
-								Rules:           configToRules(cfg),
-							},
-						},
-						Results: leaksToResults(report.Leaks),
-					},
-				},
-			}
-			encoder := json.NewEncoder(file)
-			encoder.SetIndent("", " ")
-			err = encoder.Encode(s)
-			if err != nil {
-				return err
-			}
-		}
-	}
-
-	return nil
-}

+ 0 - 158
scan/sarif.go

@@ -1,158 +0,0 @@
-package scan
-
-import (
-	"fmt"
-	"time"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-)
-
-//Sarif ...
-type Sarif struct {
-	Schema  string `json:"$schema"`
-	Version string `json:"version"`
-	Runs    []Runs `json:"runs"`
-}
-
-//ShortDescription ...
-type ShortDescription struct {
-	Text string `json:"text"`
-}
-
-//FullDescription ...
-type FullDescription struct {
-	Text string `json:"text"`
-}
-
-//Rules ...
-type Rules struct {
-	ID   string `json:"id"`
-	Name string `json:"name"`
-}
-
-//Driver ...
-type Driver struct {
-	Name            string  `json:"name"`
-	SemanticVersion string  `json:"semanticVersion"`
-	Rules           []Rules `json:"rules"`
-}
-
-//Tool ...
-type Tool struct {
-	Driver Driver `json:"driver"`
-}
-
-//Message ...
-type Message struct {
-	Text string `json:"text"`
-}
-
-//ArtifactLocation ...
-type ArtifactLocation struct {
-	URI string `json:"uri"`
-}
-
-//Region ...
-type Region struct {
-	StartLine int     `json:"startLine"`
-	Snippet   Snippet `json:"snippet"`
-}
-
-//Snippet ...
-type Snippet struct {
-	Text string `json:"text"`
-}
-
-//PhysicalLocation ...
-type PhysicalLocation struct {
-	ArtifactLocation ArtifactLocation `json:"artifactLocation"`
-	Region           Region           `json:"region"`
-}
-
-//Locations ...
-type Locations struct {
-	PhysicalLocation PhysicalLocation `json:"physicalLocation"`
-}
-
-//Results ...
-type Results struct {
-	Message    Message          `json:"message"`
-	RuleId     string           `json:"ruleId"`
-	Properties ResultProperties `json:"properties"`
-	Locations  []Locations      `json:"locations"`
-}
-
-//ResultProperties ...
-type ResultProperties struct {
-	Commit        string    `json:"commit"`
-	Offender      string    `json:"offender"`
-	Date          time.Time `json:"date"`
-	Author        string    `json:"author"`
-	Email         string    `json:"email"`
-	CommitMessage string    `json:"commitMessage"`
-	Repo          string    `json:"repo"`
-}
-
-//Runs ...
-type Runs struct {
-	Tool    Tool      `json:"tool"`
-	Results []Results `json:"results"`
-}
-
-func configToRules(cfg config.Config) []Rules {
-	var rules []Rules
-	for _, rule := range cfg.Rules {
-		rules = append(rules, Rules{
-			ID:   rule.Description,
-			Name: rule.Description,
-		})
-	}
-	return rules
-}
-
-func leaksToResults(leaks []Leak) []Results {
-	results := make([]Results, 0)
-
-	for _, leak := range leaks {
-		results = append(results, Results{
-			Message: Message{
-				Text: fmt.Sprintf("%s secret detected", leak.Rule),
-			},
-			RuleId: leak.Rule,
-			Properties: ResultProperties{
-				Commit:        leak.Commit,
-				Offender:      leak.Offender,
-				Date:          leak.Date,
-				Author:        leak.Author,
-				Email:         leak.Email,
-				CommitMessage: leak.Message,
-				Repo:          leak.Repo,
-			},
-			Locations: leakToLocation(leak),
-		})
-	}
-
-	return results
-}
-
-func leakToLocation(leak Leak) []Locations {
-	uri := leak.File
-	if leak.LeakURL != "" {
-		uri = leak.LeakURL
-	}
-	return []Locations{
-		{
-			PhysicalLocation: PhysicalLocation{
-				ArtifactLocation: ArtifactLocation{
-					URI: uri,
-				},
-				Region: Region{
-					StartLine: leak.LineNumber,
-					Snippet: Snippet{
-						Text: leak.Line,
-					},
-				},
-			},
-		},
-	}
-}

+ 0 - 164
scan/scan.go

@@ -1,164 +0,0 @@
-package scan
-
-import (
-	"os"
-	"path/filepath"
-
-	"github.com/go-git/go-git/v5"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-)
-
-// Scanner abstracts unique scanner internals while exposing the Scan function which
-// returns a report.
-type Scanner interface {
-	Scan() (Report, error)
-}
-
-// ScannerType is the scanner type which is determined based on program arguments
-type ScannerType int
-
-const (
-	typeRepoScanner ScannerType = iota + 1
-	typeDirScanner
-	typeCommitScanner
-	typeCommitsScanner
-	typeUnstagedScanner
-	typeFilesAtCommitScanner
-	typeNoGitScanner
-	typeEmpty
-)
-
-// NewScanner accepts options and a config which will be used to determine and create a
-// new scanner which is then returned.
-func NewScanner(opts options.Options, cfg config.Config) (Scanner, error) {
-	var (
-		repo *git.Repository
-		err  error
-	)
-
-	// We want to return a dir scanner immediately since if the scan type is a directory scan
-	// we don't want to clone/open a repo until inside ParentScanner.Scan
-	st, err := scanType(opts)
-	if err != nil {
-		return nil, err
-	}
-	if st == typeDirScanner {
-		if opts.AdditionalConfig != "" {
-			additionalCfg, err := config.LoadAdditionalConfig(opts.AdditionalConfig)
-			if err != nil {
-				return nil, err
-			}
-			cfg = cfg.AppendConfig(additionalCfg)
-		}
-		return NewParentScanner(opts, cfg), nil
-	}
-
-	// Clone or open a repo if we need it
-	if needsRepo(st) {
-		repo, err = getRepo(opts)
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	// load up alternative config if possible, if not use default/specified config. Will append if AppendRepoConfig is true
-	if opts.RepoConfigPath != "" && !opts.NoGit {
-		repoCfg, err := config.LoadRepoConfig(repo, opts.RepoConfigPath)
-		if err != nil {
-			return nil, err
-		}
-		if opts.AppendRepoConfig {
-			cfg = cfg.AppendConfig(repoCfg)
-		} else {
-			cfg = repoCfg
-		}
-	}
-
-	// append additional config with the rest of the config
-	if opts.AdditionalConfig != "" {
-		additionalCfg, err := config.LoadAdditionalConfig(opts.AdditionalConfig)
-		if err != nil {
-			return nil, err
-		}
-		cfg = cfg.AppendConfig(additionalCfg)
-	}
-
-	switch st {
-	case typeCommitScanner:
-		c, err := obtainCommit(repo, opts.Commit)
-		if err != nil {
-			return nil, err
-		}
-		return NewCommitScanner(opts, cfg, repo, c), nil
-	case typeCommitsScanner:
-		commits, err := optsToCommits(opts)
-		if err != nil {
-			return nil, err
-		}
-		return NewCommitsScanner(opts, cfg, repo, commits), nil
-	case typeFilesAtCommitScanner:
-		c, err := obtainCommit(repo, opts.FilesAtCommit)
-		if err != nil {
-			return nil, err
-		}
-		return NewFilesAtCommitScanner(opts, cfg, repo, c), nil
-	case typeUnstagedScanner:
-		return NewUnstagedScanner(opts, cfg, repo), nil
-	case typeDirScanner:
-		return NewParentScanner(opts, cfg), nil
-	case typeNoGitScanner:
-		return NewNoGitScanner(opts, cfg), nil
-	default:
-		return NewRepoScanner(opts, cfg, repo), nil
-	}
-}
-
-func scanType(opts options.Options) (ScannerType, error) {
-	if opts.Commit != "" {
-		return typeCommitScanner, nil
-	}
-	if opts.Commits != "" || opts.CommitsFile != "" {
-		return typeCommitsScanner, nil
-	}
-	if opts.FilesAtCommit != "" {
-		return typeFilesAtCommitScanner, nil
-	}
-	if opts.Path != "" && !opts.NoGit {
-		if opts.CheckUncommitted() {
-			return typeUnstagedScanner, nil
-		}
-		_, err := os.Stat(filepath.Join(opts.Path))
-		if err != nil {
-			return typeEmpty, err
-		}
-		// check if path/.git exists, if it does, this is a repo scan
-		// if not this is a multi-repo scan
-		_, err = os.Stat(filepath.Join(opts.Path, ".git"))
-		if os.IsNotExist(err) {
-			return typeDirScanner, nil
-		}
-		return typeRepoScanner, nil
-	}
-	if opts.Path != "" && opts.NoGit {
-		_, err := os.Stat(filepath.Join(opts.Path))
-		if err != nil {
-			return typeEmpty, err
-		}
-		return typeNoGitScanner, nil
-	}
-	if opts.CheckUncommitted() {
-		return typeUnstagedScanner, nil
-	}
-
-	// default to the most commonly used scanner, RepoScanner
-	return typeRepoScanner, nil
-}
-
-func needsRepo(st ScannerType) bool {
-	if !(st == typeDirScanner || st == typeNoGitScanner) {
-		return true
-	}
-	return false
-}

+ 0 - 192
scan/scan_test.go

@@ -1,192 +0,0 @@
-package scan_test
-
-import (
-	"encoding/json"
-	"fmt"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"testing"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-	"github.com/zricethezav/gitleaks/v7/scan"
-)
-
-const repoBasePath = "../testdata/repos/"
-const expectPath = "../testdata/expect/"
-
-func TestScan(t *testing.T) {
-	err := moveDotGit("dotGit", ".git")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer moveDotGit(".git", "dotGit")
-	tests := []struct {
-		description string
-		opts        options.Options
-		wantPath    string
-	}{
-		{
-			description: "test google api key leak AND square oauth leak",
-			opts: options.Options{
-				Path:         filepath.Join(repoBasePath, "with_square_and_google"),
-				Report:       filepath.Join(expectPath, "results_square_and_google.json.got"),
-				ReportFormat: "json",
-				NoGit:        true,
-			},
-			wantPath: filepath.Join(expectPath, "results_square_and_google.json"),
-		},
-	}
-
-	for _, test := range tests {
-		cfg, err := config.NewConfig(test.opts)
-		if err != nil {
-			t.Error(err)
-		}
-
-		scanner, err := scan.NewScanner(test.opts, cfg)
-		if err != nil {
-			t.Error(test.description, err)
-		}
-
-		scannerReport, err := scanner.Scan()
-		if err != nil {
-			t.Fatal(test.description, err)
-		}
-
-		err = scan.WriteReport(scannerReport, test.opts, cfg)
-		if err != nil {
-			t.Error(test.description, err)
-		}
-
-		if test.wantPath != "" {
-			err := fileCheck(test.wantPath, test.opts.Report)
-			if err != nil {
-				t.Error(test.description, err)
-			}
-		}
-	}
-}
-
-func moveDotGit(from, to string) error {
-	repoDirs, err := ioutil.ReadDir("../testdata/repos")
-	if err != nil {
-		return err
-	}
-	for _, dir := range repoDirs {
-		if to == ".git" {
-			_, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), "dotGit"))
-			if os.IsNotExist(err) {
-				// dont want to delete the only copy of .git accidentally
-				continue
-			}
-			os.RemoveAll(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), ".git"))
-		}
-		if !dir.IsDir() {
-			continue
-		}
-		_, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from))
-		if os.IsNotExist(err) {
-			continue
-		}
-
-		err = os.Rename(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from),
-			fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), to))
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func fileCheck(wantPath, gotPath string) error {
-	var (
-		gotLeaks  []scan.Leak
-		wantLeaks []scan.Leak
-	)
-	want, err := ioutil.ReadFile(wantPath)
-	if err != nil {
-		return err
-	}
-
-	got, err := ioutil.ReadFile(gotPath)
-	if err != nil {
-		return err
-	}
-
-	err = json.Unmarshal(got, &gotLeaks)
-	if err != nil {
-		return err
-	}
-
-	err = json.Unmarshal(want, &wantLeaks)
-	if err != nil {
-		return err
-	}
-
-	if len(wantLeaks) != len(gotLeaks) {
-		return fmt.Errorf("got %d leaks, want %d leaks", len(gotLeaks), len(wantLeaks))
-	}
-
-	for _, wantLeak := range wantLeaks {
-		found := false
-		for _, gotLeak := range gotLeaks {
-			if same(gotLeak, wantLeak) {
-				found = true
-			}
-		}
-		if !found {
-			return fmt.Errorf("unable to find %+v in %s", wantLeak, gotPath)
-		}
-	}
-
-	if err := os.Remove(gotPath); err != nil {
-		return err
-	}
-	return nil
-}
-
-func same(l1, l2 scan.Leak) bool {
-	if l1.Commit != l2.Commit {
-		return false
-	}
-
-	if l1.Offender != l2.Offender {
-		return false
-	}
-
-	if l1.OffenderEntropy != l2.OffenderEntropy {
-		return false
-	}
-
-	if l1.Line != l2.Line {
-		return false
-	}
-
-	if l1.Tags != l2.Tags {
-		return false
-	}
-
-	if l1.LineNumber != l2.LineNumber {
-		return false
-	}
-
-	if l1.Author != l2.Author {
-		return false
-	}
-
-	if l1.LeakURL != l2.LeakURL {
-		return false
-	}
-
-	if l1.RepoURL != l2.RepoURL {
-		return false
-	}
-
-	if l1.Repo != l2.Repo {
-		return false
-	}
-	return true
-
-}

+ 0 - 43
scan/throttle.go

@@ -1,43 +0,0 @@
-package scan
-
-import (
-	"runtime"
-
-	"github.com/zricethezav/gitleaks/v7/options"
-)
-
-const (
-	singleThreadCommitBuffer          = 1
-	multiThreadCommitBufferMultiplier = 10
-)
-
-// Throttle is a struct that limits the number of concurrent goroutines and sets the
-// number of threads available for gitleaks to use via GOMAXPROCS.
-type Throttle struct {
-	throttle chan bool
-}
-
-// NewThrottle accepts some options and returns a throttle for scanners to use
-func NewThrottle(opts options.Options) *Throttle {
-	t := Throttle{}
-	if opts.Threads <= 1 {
-		runtime.GOMAXPROCS(1)
-		t.throttle = make(chan bool, singleThreadCommitBuffer)
-		return &t
-	}
-
-	runtime.GOMAXPROCS(opts.Threads)
-	t.throttle = make(chan bool, multiThreadCommitBufferMultiplier*opts.Threads)
-	return &t
-
-}
-
-// Limit blocks new goroutines from spinning up if throttle is at capacity
-func (t *Throttle) Limit() {
-	t.throttle <- true
-}
-
-// Release releases the hold on the throttle, allowing more goroutines to be spun up
-func (t *Throttle) Release() {
-	<-t.throttle
-}

+ 0 - 298
scan/unstaged.go

@@ -1,298 +0,0 @@
-package scan
-
-import (
-	"bytes"
-	"fmt"
-	"io"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"strings"
-	"time"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-
-	"github.com/go-git/go-git/v5"
-	"github.com/go-git/go-git/v5/plumbing"
-	"github.com/sergi/go-diff/diffmatchpatch"
-)
-
-// UnstagedScanner is an unstaged scanner. This is the scanner used when you don't provide program arguments
-// which will then scan your PWD. This scans unstaged changes in your repo.
-type UnstagedScanner struct {
-	opts     options.Options
-	cfg      config.Config
-	repo     *git.Repository
-	repoName string
-}
-
-// NewUnstagedScanner returns an unstaged scanner
-func NewUnstagedScanner(opts options.Options, cfg config.Config, repo *git.Repository) *UnstagedScanner {
-	us := &UnstagedScanner{
-		opts:     opts,
-		cfg:      cfg,
-		repo:     repo,
-		repoName: getRepoName(opts),
-	}
-	return us
-}
-
-// Scan kicks off an unstaged scan. This will attempt to determine unstaged changes which are then scanned.
-func (us *UnstagedScanner) Scan() (Report, error) {
-	var scannerReport Report
-	r, err := us.repo.Head()
-	if err == plumbing.ErrReferenceNotFound {
-		wt, err := us.repo.Worktree()
-		if err != nil {
-			return scannerReport, err
-		}
-
-		status, err := wt.Status()
-		if err != nil {
-			return scannerReport, err
-		}
-		for fn := range status {
-			workTreeBuf := bytes.NewBuffer(nil)
-			workTreeFile, err := wt.Filesystem.Open(fn)
-			if err != nil {
-				continue
-			}
-
-			// Check if file is allow listed
-			if us.cfg.Allowlist.FileAllowed(filepath.Base(fn)) ||
-				us.cfg.Allowlist.PathAllowed(fn) {
-				continue
-			}
-			// Check individual file path ONLY rules
-			for _, rule := range us.cfg.Rules {
-				if rule.HasFileOrPathLeakOnly(fn) {
-					leak := NewLeak("", "Filename or path offender: "+fn, defaultLineNumber)
-					leak.Repo = us.repoName
-					leak.File = fn
-					leak.RepoURL = us.opts.RepoURL
-					leak.LeakURL = leak.URL()
-					leak.Rule = rule.Description
-					leak.Tags = strings.Join(rule.Tags, ", ")
-					leak.Log(us.opts)
-					scannerReport.Leaks = append(scannerReport.Leaks, leak)
-					continue
-				}
-			}
-
-			if fc, err := os.Readlink(fn); err == nil {
-				workTreeBuf = bytes.NewBufferString(fc)
-			} else if _, err := io.Copy(workTreeBuf, workTreeFile); err != nil {
-				return scannerReport, err
-			}
-			lineNumber := 0
-			for _, line := range strings.Split(workTreeBuf.String(), "\n") {
-				lineNumber++
-				for _, rule := range us.cfg.Rules {
-					offender := rule.Inspect(line)
-					if offender.IsEmpty() {
-						continue
-					}
-					if us.cfg.Allowlist.RegexAllowed(line) ||
-						rule.AllowList.FileAllowed(filepath.Base(workTreeFile.Name())) ||
-						rule.AllowList.PathAllowed(workTreeFile.Name()) {
-						continue
-					}
-					if rule.File.String() != "" && !rule.HasFileLeak(filepath.Base(workTreeFile.Name())) {
-						continue
-					}
-					if rule.Path.String() != "" && !rule.HasFilePathLeak(filepath.Base(workTreeFile.Name())) {
-						continue
-					}
-					leak := NewLeak(line, offender.ToString(), defaultLineNumber).WithCommit(emptyCommit()).WithEntropy(offender.EntropyLevel)
-					leak.File = workTreeFile.Name()
-					leak.LineNumber = lineNumber
-					leak.Repo = us.repoName
-					leak.Rule = rule.Description
-					leak.Tags = strings.Join(rule.Tags, ", ")
-					if us.opts.Verbose {
-						leak.Log(us.opts)
-					}
-					scannerReport.Leaks = append(scannerReport.Leaks, leak)
-				}
-			}
-		}
-		return scannerReport, nil
-	} else if err != nil {
-		return scannerReport, err
-	}
-
-	c, err := us.repo.CommitObject(r.Hash())
-	if err != nil {
-		return scannerReport, err
-	}
-
-	// Staged change so the Commit details do not yet exist. Insert empty defaults.
-	c.Hash = plumbing.Hash{}
-	c.Message = ""
-	c.Author.Name = ""
-	c.Author.Email = ""
-	c.Author.When = time.Unix(0, 0).UTC()
-
-	prevTree, err := c.Tree()
-	if err != nil {
-		return scannerReport, err
-	}
-	wt, err := us.repo.Worktree()
-	if err != nil {
-		return scannerReport, err
-	}
-
-	status, err := gitStatus(wt)
-	if err != nil {
-		return scannerReport, err
-	}
-	for fn, state := range status {
-		var (
-			prevFileContents string
-			currFileContents string
-			filename         string
-		)
-
-		if state.Staging != git.Untracked {
-			if state.Staging == git.Deleted {
-				// file in staging has been deleted, aka it is not on the filesystem
-				// so the contents of the file are ""
-				currFileContents = ""
-				//check if file is symlink
-			} else if fc, err := os.Readlink(fn); err == nil {
-				currFileContents = fc
-			} else {
-				workTreeBuf := bytes.NewBuffer(nil)
-				workTreeFile, err := wt.Filesystem.Open(fn)
-				if err != nil {
-					continue
-				}
-				if _, err := io.Copy(workTreeBuf, workTreeFile); err != nil {
-					return scannerReport, err
-				}
-				currFileContents = workTreeBuf.String()
-				filename = workTreeFile.Name()
-			}
-
-			// get files at HEAD state
-			prevFile, err := prevTree.File(fn)
-			if err != nil {
-				prevFileContents = ""
-
-			} else {
-				prevFileContents, err = prevFile.Contents()
-				if err != nil {
-					return scannerReport, err
-				}
-				if filename == "" {
-					filename = prevFile.Name
-				}
-			}
-
-			// Check if file is allow listed
-			if us.cfg.Allowlist.FileAllowed(filepath.Base(filename)) ||
-				us.cfg.Allowlist.PathAllowed(filename) {
-				continue
-			}
-
-			dmp := diffmatchpatch.New()
-			diffs := dmp.DiffMain(prevFileContents, currFileContents, false)
-			prettyDiff := diffPrettyText(diffs)
-
-			var diffContents string
-			for _, d := range diffs {
-				if d.Type == diffmatchpatch.DiffInsert {
-					diffContents += fmt.Sprintf("%s\n", d.Text)
-				}
-			}
-
-			lineLookup := make(map[string]bool)
-
-			for _, line := range strings.Split(diffContents, "\n") {
-				for _, rule := range us.cfg.Rules {
-					offender := rule.Inspect(line)
-					if offender.IsEmpty() {
-						continue
-					}
-					if us.cfg.Allowlist.RegexAllowed(line) ||
-						rule.AllowList.FileAllowed(filepath.Base(filename)) ||
-						rule.AllowList.PathAllowed(filename) {
-						continue
-					}
-					if rule.File.String() != "" && !rule.HasFileLeak(filepath.Base(filename)) {
-						continue
-					}
-					if rule.Path.String() != "" && !rule.HasFilePathLeak(filepath.Base(filename)) {
-						continue
-					}
-					leak := NewLeak(line, offender.ToString(), defaultLineNumber).WithCommit(emptyCommit()).WithEntropy(offender.EntropyLevel)
-					leak.File = filename
-					leak.LineNumber = extractLine(prettyDiff, leak, lineLookup) + 1
-					leak.Repo = us.repoName
-					leak.Rule = rule.Description
-					leak.Tags = strings.Join(rule.Tags, ", ")
-
-					leak.Log(us.opts)
-
-					scannerReport.Leaks = append(scannerReport.Leaks, leak)
-				}
-			}
-		}
-	}
-
-	return scannerReport, err
-}
-
-// DiffPrettyText converts a []Diff into a colored text report.
-// TODO open PR for this
-func diffPrettyText(diffs []diffmatchpatch.Diff) string {
-	var buff bytes.Buffer
-	for _, diff := range diffs {
-		text := diff.Text
-
-		switch diff.Type {
-		case diffmatchpatch.DiffInsert:
-			_, _ = buff.WriteString("+")
-			_, _ = buff.WriteString(text)
-		case diffmatchpatch.DiffDelete:
-			_, _ = buff.WriteString("-")
-			_, _ = buff.WriteString(text)
-		case diffmatchpatch.DiffEqual:
-			_, _ = buff.WriteString(" ")
-			_, _ = buff.WriteString(text)
-		}
-	}
-	return buff.String()
-}
-
-// gitStatus returns the status of modified files in the worktree. It will attempt to execute 'git status'
-// and will fall back to git.Worktree.Status() if that fails.
-func gitStatus(wt *git.Worktree) (git.Status, error) {
-	c := exec.Command("git", "status", "--porcelain", "-z")
-	c.Dir = wt.Filesystem.Root()
-	output, err := c.Output()
-	if err != nil {
-		stat, err := wt.Status()
-		return stat, err
-	}
-
-	lines := strings.Split(string(output), "\000")
-	stat := make(map[string]*git.FileStatus, len(lines))
-	for _, line := range lines {
-		if len(line) == 0 {
-			continue
-		}
-
-		// For copy/rename the output looks like
-		//   R  destination\000source
-		// Which means we can split on space and ignore anything with only one result
-		parts := strings.SplitN(strings.TrimLeft(line, " "), " ", 2)
-		if len(parts) == 2 {
-			stat[strings.Trim(parts[1], " ")] = &git.FileStatus{
-				Staging: git.StatusCode([]byte(parts[0])[0]),
-			}
-		}
-	}
-	return stat, err
-}

+ 0 - 111
scan/unstaged_test.go

@@ -1,111 +0,0 @@
-package scan_test
-
-import (
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"testing"
-
-	"github.com/zricethezav/gitleaks/v7/config"
-	"github.com/zricethezav/gitleaks/v7/options"
-	"github.com/zricethezav/gitleaks/v7/scan"
-)
-
-func TestUnstaged(t *testing.T) {
-	err := moveDotGit("dotGit", ".git")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer moveDotGit(".git", "dotGit")
-	tests := []struct {
-		description  string
-		opts         options.Options
-		wantPath     string
-		fileToChange string
-		change       string
-		empty        bool
-	}{
-		{
-			description: "basic repo with unstagged change containing a secret",
-			opts: options.Options{
-				Path:         filepath.Join(repoBasePath, "basic"),
-				Report:       filepath.Join(expectPath, "basic", "results_unstaged.json.got"),
-				ReportFormat: "json",
-				Unstaged:     true,
-			},
-			wantPath:     filepath.Join(expectPath, "basic", "results_unstaged.json"),
-			fileToChange: filepath.Join(repoBasePath, "basic", "secrets.py"),
-			change:       "\nadded_aws_access_key_id='AKIAIO5FODNN7DXAMPLE'\n",
-		},
-		{
-			description: "basic repo with unstagged change not containing a secret",
-			opts: options.Options{
-				Path:         filepath.Join(repoBasePath, "basic"),
-				Report:       filepath.Join(expectPath, "basic", "results_unstaged.json.got"),
-				ReportFormat: "json",
-				Unstaged:     true,
-			},
-			empty:        true,
-			fileToChange: filepath.Join(repoBasePath, "basic", "secrets.py"),
-			change:       "\nnice_variable='is_nice''\n",
-		},
-	}
-
-	for _, test := range tests {
-		var old []byte
-		if test.fileToChange != "" {
-			old, err = ioutil.ReadFile(test.fileToChange)
-			if err != nil {
-				t.Error(err)
-			}
-			altered, err := os.OpenFile(test.fileToChange,
-				os.O_WRONLY|os.O_APPEND, 0644)
-			if err != nil {
-				t.Error(err)
-			}
-
-			_, err = altered.WriteString(test.change)
-			if err != nil {
-				t.Error(err)
-			}
-		}
-
-		cfg, err := config.NewConfig(test.opts)
-		if err != nil {
-			t.Error(err)
-		}
-
-		scanner, err := scan.NewScanner(test.opts, cfg)
-		if err != nil {
-			t.Fatal(test.description, err)
-		}
-
-		scannerReport, err := scanner.Scan()
-		if err != nil {
-			t.Fatal(test.description, err)
-		}
-
-		err = scan.WriteReport(scannerReport, test.opts, cfg)
-		if err != nil {
-			t.Error(test.description, err)
-		}
-
-		if test.empty {
-			if len(scannerReport.Leaks) != 0 {
-				t.Errorf("%s wanted no leaks but got some instead: %+v", test.description, scannerReport.Leaks)
-			}
-		}
-
-		if test.wantPath != "" {
-			err := fileCheck(test.wantPath, test.opts.Report)
-			if err != nil {
-				t.Error(test.description, err)
-			}
-		}
-		err = ioutil.WriteFile(test.fileToChange, old, 0)
-		if err != nil {
-			t.Error(err)
-		}
-
-	}
-}

+ 0 - 229
scan/utils.go

@@ -1,229 +0,0 @@
-package scan
-
-import (
-	"bufio"
-	"fmt"
-	"os"
-	"path/filepath"
-	"runtime"
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/zricethezav/gitleaks/v7/options"
-
-	"github.com/go-git/go-git/v5"
-	"github.com/go-git/go-git/v5/plumbing"
-	"github.com/go-git/go-git/v5/plumbing/object"
-	"github.com/go-git/go-git/v5/storage/memory"
-	log "github.com/sirupsen/logrus"
-)
-
-const (
-	diffAddPrefix     = "+"
-	diffDelPrefix     = "-"
-	diffLineSignature = " @@"
-	defaultLineNumber = 1
-)
-
-func obtainCommit(repo *git.Repository, commitSha string) (*object.Commit, error) {
-	if commitSha == "latest" {
-		ref, err := repo.Head()
-		if err != nil {
-			return nil, err
-		}
-		commitSha = ref.Hash().String()
-	}
-	return repo.CommitObject(plumbing.NewHash(commitSha))
-}
-
-func getRepoName(opts options.Options) string {
-	if opts.RepoURL != "" {
-		return filepath.Base(opts.RepoURL)
-	}
-	if opts.Path != "" {
-		return filepath.Base(opts.Path)
-	}
-	if opts.CheckUncommitted() {
-		dir, _ := os.Getwd()
-		return filepath.Base(dir)
-	}
-	return ""
-}
-
-func getRepo(opts options.Options) (*git.Repository, error) {
-	if opts.OpenLocal() {
-		if opts.Path != "" {
-			log.Infof("opening %s\n", opts.Path)
-		} else {
-			log.Info("opening .")
-		}
-		return git.PlainOpen(opts.Path)
-	}
-	if opts.CheckUncommitted() {
-		// open git repo from PWD
-		dir, err := os.Getwd()
-		if err != nil {
-			return nil, err
-		}
-		log.Debugf("opening %s as a repo\n", dir)
-		return git.PlainOpen(dir)
-	}
-	return cloneRepo(opts)
-}
-
-func cloneRepo(opts options.Options) (*git.Repository, error) {
-	cloneOpts, err := opts.CloneOptions()
-	if err != nil {
-		return nil, err
-	}
-	if opts.ClonePath != "" {
-		log.Infof("cloning... %s to %s", cloneOpts.URL, opts.ClonePath)
-		return git.PlainClone(opts.ClonePath, false, cloneOpts)
-	}
-	log.Infof("cloning... %s", cloneOpts.URL)
-	return git.Clone(memory.NewStorage(), nil, cloneOpts)
-}
-
-// depthReached checks if i meets the depth (--depth=) if set
-func depthReached(i int, opts options.Options) bool {
-	if opts.Depth != 0 && opts.Depth == i {
-		log.Warnf("Exceeded depth limit (%d)", i)
-		return true
-	}
-	return false
-}
-
-// emptyCommit generates an empty commit used for scanning uncommitted changes
-func emptyCommit() *object.Commit {
-	return &object.Commit{
-		Hash:    plumbing.Hash{},
-		Message: "",
-		Author: object.Signature{
-			Name:  "",
-			Email: "",
-			When:  time.Unix(0, 0).UTC(),
-		},
-	}
-}
-
-// howManyThreads will return a number 1-GOMAXPROCS which is the number
-// of goroutines that will spawn during gitleaks execution
-func howManyThreads(threads int) int {
-	maxThreads := runtime.GOMAXPROCS(0)
-	if threads == 0 {
-		return 1
-	} else if threads > maxThreads {
-		log.Warnf("%d threads set too high, setting to system max, %d", threads, maxThreads)
-		return maxThreads
-	}
-	return threads
-}
-
-// getLogOptions determines what log options are used when iterating through commits.
-// It is similar to `git log {branch}`. Default behavior is to log ALL branches so
-// gitleaks gets the full git history.
-func logOptions(repo *git.Repository, opts options.Options) (*git.LogOptions, error) {
-	var logOpts git.LogOptions
-	const dateformat string = "2006-01-02"
-	const timeformat string = "2006-01-02T15:04:05-0700"
-	if opts.CommitFrom != "" {
-		logOpts.From = plumbing.NewHash(opts.CommitFrom)
-	}
-	if opts.CommitSince != "" {
-		if t, err := time.Parse(timeformat, opts.CommitSince); err == nil {
-			logOpts.Since = &t
-		} else if t, err := time.Parse(dateformat, opts.CommitSince); err == nil {
-			logOpts.Since = &t
-		} else {
-			return nil, err
-		}
-		logOpts.All = true
-	}
-	if opts.CommitUntil != "" {
-		if t, err := time.Parse(timeformat, opts.CommitUntil); err == nil {
-			logOpts.Until = &t
-		} else if t, err := time.Parse(dateformat, opts.CommitUntil); err == nil {
-			logOpts.Until = &t
-		} else {
-			return nil, err
-		}
-		logOpts.All = true
-	}
-	if opts.Branch != "" {
-		ref, err := repo.Storer.Reference(plumbing.NewBranchReferenceName(opts.Branch))
-		if err != nil {
-			return nil, fmt.Errorf("could not find branch %s", opts.Branch)
-		}
-		logOpts = git.LogOptions{
-			From: ref.Hash(),
-		}
-
-		if logOpts.From.IsZero() {
-			return nil, fmt.Errorf("could not find branch %s", opts.Branch)
-		}
-		return &logOpts, nil
-	}
-	if !logOpts.From.IsZero() || logOpts.Since != nil || logOpts.Until != nil {
-		return &logOpts, nil
-	}
-	return &git.LogOptions{All: true}, nil
-}
-
-func optsToCommits(opts options.Options) ([]string, error) {
-	if opts.Commits != "" {
-		return strings.Split(opts.Commits, ","), nil
-	}
-	file, err := os.Open(opts.CommitsFile)
-	if err != nil {
-		return []string{}, err
-	}
-	defer rable(file.Close)
-
-	scanner := bufio.NewScanner(file)
-	var commits []string
-	for scanner.Scan() {
-		commits = append(commits, scanner.Text())
-	}
-	return commits, nil
-}
-
-func extractLine(patchContent string, leak Leak, lineLookup map[string]bool) int {
-	i := strings.Index(patchContent, fmt.Sprintf("\n+++ b/%s", leak.File))
-	filePatchContent := patchContent[i+1:]
-	i = strings.Index(filePatchContent, "diff --git")
-	if i != -1 {
-		filePatchContent = filePatchContent[:i]
-	}
-	chunkStartLine := 0
-	currLine := 0
-	for _, patchLine := range strings.Split(filePatchContent, "\n") {
-		if strings.HasPrefix(patchLine, "@@") {
-			i := strings.Index(patchLine, diffAddPrefix)
-			pairs := strings.Split(strings.Split(patchLine[i+1:], diffLineSignature)[0], ",")
-			chunkStartLine, _ = strconv.Atoi(pairs[0])
-			currLine = -1
-		}
-		if strings.HasPrefix(patchLine, diffDelPrefix) {
-			currLine--
-		}
-		if strings.HasPrefix(patchLine, diffAddPrefix) && strings.Contains(patchLine, leak.Line) {
-			lineNumber := chunkStartLine + currLine
-			if _, ok := lineLookup[fmt.Sprintf("%s%s%d%s", leak.Offender, leak.Line, lineNumber, leak.File)]; !ok {
-				lineLookup[fmt.Sprintf("%s%s%d%s", leak.Offender, leak.Line, lineNumber, leak.File)] = true
-				return lineNumber
-			} else if ok {
-				return lineNumber
-			}
-		}
-		currLine++
-	}
-	return defaultLineNumber
-}
-
-// rable is the second half of deferrable... mainly used for defer file.Close()
-func rable(f func() error) {
-	if err := f(); err != nil {
-		log.Error(err)
-	}
-}

+ 22 - 0
scripts/pre-commit.py

@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+import os,sys
+import subprocess
+
+def gitleaksEnabled():
+    out = subprocess.getoutput("git config --bool hooks.gitleaks")
+    if out == "false":
+        return False
+    return True
+
+if gitleaksEnabled():
+    exitCode = os.WEXITSTATUS(os.system('gitleaks protect -v --staged'))
+    if exitCode == 1:
+        print('''Warning: gitleaks has detected sensitive information in your changes.
+To disable the gitleaks precommit hook run the following command:
+
+    git config hooks.gitleaks false
+''')
+        sys.exit(1)
+else:
+    print('gitleaks precommit disabled (enable with `git config hooks.gitleaks true`)')
+

+ 5 - 5
testdata/configs/allowlist_global_files.toml → testdata/config/allow_aws_re.toml

@@ -1,9 +1,9 @@
+title = "simple config with allowlist for aws"
+
 [[rules]]
     description = "AWS Access Key"
+    id = "aws-access-key"
     regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
     tags = ["key", "AWS"]
-
-[allowlist]
-	description = "Allowlisted files"
-	paths = ['''.py''']
-	files = ['''.md$''']
+    [rules.allowlist]
+        regexes = ['''AKIALALEMEL33243OLIA''']

+ 4 - 4
testdata/configs/aws_key.toml → testdata/config/allow_commit.toml

@@ -1,9 +1,9 @@
-[[rules]]
-	description = "AWS Secret Key"
-	regex = '''(?i)aws(.{0,20})?(?-i)['\"][0-9a-zA-Z\/+]{40}['\"]'''
-	tags = ["key", "AWS"]
+title = "simple config with allowlist for a specific commit"
 
 [[rules]]
     description = "AWS Access Key"
+    id = "aws-access-key"
     regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
     tags = ["key", "AWS"]
+    [rules.allowlist]
+        commits = ['''allowthiscommit''']

+ 4 - 1
examples/simple_regex_config.toml → testdata/config/allow_path.toml

@@ -1,6 +1,9 @@
-# This is a simple gitleaks config that contains one rule which checks for AWS keys.
+title = "simple config with allowlist for .go files"
 
 [[rules]]
     description = "AWS Access Key"
+    id = "aws-access-key"
     regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
     tags = ["key", "AWS"]
+    [rules.allowlist]
+        paths = ['''.go''']

+ 8 - 0
testdata/config/bad_entropy_group.toml

@@ -0,0 +1,8 @@
+title = "gitleaks config"
+
+[[rules]]
+id = "discord-api-key"
+description = "Discord API key"
+regex = '''(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{64})['\"]'''
+entropyGroup = 5
+entropy = 3.5

+ 8 - 0
testdata/config/entropy_group.toml

@@ -0,0 +1,8 @@
+title = "gitleaks config"
+
+[[rules]]
+id = "discord-api-key"
+description = "Discord API key"
+regex = '''(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{64})['\"]'''
+entropyGroup = 3
+entropy = 3.5

+ 8 - 0
testdata/config/generic.toml

@@ -0,0 +1,8 @@
+title = "gitleaks config"
+
+[[rules]]
+description = "Generic API Key"
+id = "generic-api-key"
+regex = '''(?i)((key|api|token|secret|password)[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9a-zA-Z\-_=]{8,64})['\"]'''
+entropy = 3.7
+entropyGroup = 4

+ 9 - 0
testdata/config/generic_with_py_path.toml

@@ -0,0 +1,9 @@
+title = "gitleaks config"
+
+[[rules]]
+description = "Generic API Key"
+id = "generic-api-key"
+regex = '''(?i)((key|api|token|secret|password)[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9a-zA-Z\-_=]{8,64})['\"]'''
+path = '''.py'''
+entropy = 3.7
+entropyGroup = 4

+ 6 - 0
testdata/config/path_only.toml

@@ -0,0 +1,6 @@
+title = "gitleaks config"
+
+[[rules]]
+description = "Python Files"
+id = "python-files-only"
+path = '''.py'''

+ 171 - 0
testdata/config/simple.toml

@@ -0,0 +1,171 @@
+title = "gitleaks config"
+# https://learnxinyminutes.com/docs/toml/ for toml reference
+
+[[rules]]
+    description = "AWS Access Key"
+    id = "aws-access-key"
+    regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
+    tags = ["key", "AWS"]
+
+[[rules]]
+    description = "AWS Secret Key"
+    regex = '''(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}'''
+    tags = ["key", "AWS"]
+
+[[rules]]
+    description = "AWS MWS key"
+    regex = '''amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'''
+    tags = ["key", "AWS", "MWS"]
+
+[[rules]]
+    description = "Facebook Secret Key"
+    regex = '''(?i)(facebook|fb)(.{0,20})?(?-i)['\"][0-9a-f]{32}['\"]'''
+    tags = ["key", "Facebook"]
+
+[[rules]]
+    description = "Facebook Client ID"
+    regex = '''(?i)(facebook|fb)(.{0,20})?['\"][0-9]{13,17}['\"]'''
+    tags = ["key", "Facebook"]
+
+[[rules]]
+    description = "Twitter Secret Key"
+    regex = '''(?i)twitter(.{0,20})?['\"][0-9a-z]{35,44}['\"]'''
+    tags = ["key", "Twitter"]
+
+[[rules]]
+    description = "Twitter Client ID"
+    regex = '''(?i)twitter(.{0,20})?['\"][0-9a-z]{18,25}['\"]'''
+    tags = ["client", "Twitter"]
+
+[[rules]]
+    description = "Github Personal Access Token"
+    regex = '''ghp_[0-9a-zA-Z]{36}'''
+    tags = ["key", "Github"]
+[[rules]]
+    description = "Github OAuth Access Token"
+    regex = '''gho_[0-9a-zA-Z]{36}'''
+    tags = ["key", "Github"]
+[[rules]]
+    description = "Github App Token"
+    regex = '''(ghu|ghs)_[0-9a-zA-Z]{36}'''
+    tags = ["key", "Github"]
+[[rules]]
+    description = "Github Refresh Token"
+    regex = '''ghr_[0-9a-zA-Z]{76}'''
+    tags = ["key", "Github"]
+
+[[rules]]
+    description = "LinkedIn Client ID"
+    regex = '''(?i)linkedin(.{0,20})?(?-i)[0-9a-z]{12}'''
+    tags = ["client", "LinkedIn"]
+
+[[rules]]
+    description = "LinkedIn Secret Key"
+    regex = '''(?i)linkedin(.{0,20})?[0-9a-z]{16}'''
+    tags = ["secret", "LinkedIn"]
+
+[[rules]]
+    description = "Slack"
+    regex = '''xox[baprs]-([0-9a-zA-Z]{10,48})?'''
+    tags = ["key", "Slack"]
+
+[[rules]]
+    description = "Asymmetric Private Key"
+    regex = '''-----BEGIN ((EC|PGP|DSA|RSA|OPENSSH) )?PRIVATE KEY( BLOCK)?-----'''
+    tags = ["key", "AsymmetricPrivateKey"]
+
+[[rules]]
+    description = "Google API key"
+    regex = '''AIza[0-9A-Za-z\-_]{35}'''
+    tags = ["key", "Google"]
+
+[[rules]]
+    description = "Google (GCP) Service Account"
+    regex = '''"type": "service_account"'''
+    tags = ["key", "Google"]
+
+[[rules]]
+    description = "Heroku API key"
+    regex = '''(?i)heroku(.{0,20})?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'''
+    tags = ["key", "Heroku"]
+
+[[rules]]
+    description = "MailChimp API key"
+    regex = '''(?i)(mailchimp|mc)(.{0,20})?[0-9a-f]{32}-us[0-9]{1,2}'''
+    tags = ["key", "Mailchimp"]
+
+[[rules]]
+    description = "Mailgun API key"
+    regex = '''((?i)(mailgun|mg)(.{0,20})?)?key-[0-9a-z]{32}'''
+    tags = ["key", "Mailgun"]
+
+[[rules]]
+    description = "PayPal Braintree access token"
+    regex = '''access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}'''
+    tags = ["key", "Paypal"]
+
+[[rules]]
+    description = "Picatic API key"
+    regex = '''sk_live_[0-9a-z]{32}'''
+    tags = ["key", "Picatic"]
+
+[[rules]]
+    description = "SendGrid API Key"
+    regex = '''SG\.[\w_]{16,32}\.[\w_]{16,64}'''
+    tags = ["key", "SendGrid"]
+
+[[rules]]
+    description = "Slack Webhook"
+    regex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8,12}/[a-zA-Z0-9_]{24}'''
+    tags = ["key", "slack"]
+
+[[rules]]
+    description = "Stripe API key"
+    regex = '''(?i)stripe(.{0,20})?[sr]k_live_[0-9a-zA-Z]{24}'''
+    tags = ["key", "Stripe"]
+
+[[rules]]
+    description = "Square access token"
+    regex = '''sq0atp-[0-9A-Za-z\-_]{22}'''
+    tags = ["key", "square"]
+
+[[rules]]
+    description = "Square OAuth secret"
+    regex = '''sq0csp-[0-9A-Za-z\-_]{43}'''
+    tags = ["key", "square"]
+
+[[rules]]
+    description = "Twilio API key"
+    regex = '''(?i)twilio(.{0,20})?SK[0-9a-f]{32}'''
+    tags = ["key", "twilio"]
+
+[[rules]]
+    description = "Dynatrace ttoken"
+    regex = '''dt0[a-zA-Z]{1}[0-9]{2}\.[A-Z0-9]{24}\.[A-Z0-9]{64}'''
+    tags = ["key", "Dynatrace"]
+
+[[rules]]
+    description = "Shopify shared secret"
+    regex = '''shpss_[a-fA-F0-9]{32}'''
+    tags = ["key", "Shopify"]
+
+[[rules]]
+    description = "Shopify access token"
+    regex = '''shpat_[a-fA-F0-9]{32}'''
+    tags = ["key", "Shopify"]
+
+[[rules]]
+    description = "Shopify custom app access token"
+    regex = '''shpca_[a-fA-F0-9]{32}'''
+    tags = ["key", "Shopify"]
+
+[[rules]]
+    description = "Shopify private app access token"
+    regex = '''shppa_[a-fA-F0-9]{32}'''
+    tags = ["key", "Shopify"]
+
+[[rules]]
+    description = "PyPI upload token"
+    regex = '''pypi-AgEIcHlwaS5vcmc[A-Za-z0-9-_]{50,1000}'''
+    tags = ["key", "pypi"]
+

+ 0 - 3
testdata/configs/allowlist_allow_all_repo_1.toml

@@ -1,3 +0,0 @@
-[allowlist]
-	description = "Allowlisted files"
-	files = [ '''server.test.py''','''server.test2.py''']

+ 0 - 3
testdata/configs/allowlist_bad_docx_10.toml

@@ -1,3 +0,0 @@
-[allowlist]
-	description = "Allowlisted files"
-	files = [ '''bad.docx''']

+ 0 - 15
testdata/configs/allowlist_commit.toml

@@ -1,15 +0,0 @@
-[[rules]]
-    description = "AWS Access Key"
-    regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-    tags = ["key", "AWS"]
-    [rules.allowlist]
-		description = "if we encounter this commit for this rule, skip it"
-        commits = ["b10b3e2cb320a8c211fda94c4567299d37de7776"]
-
-
-[allowlist]
-  commits = [
-    "17471a5fda722a9e423f1a0d3f0d267ea009d41c",
-    "996865bb912f3bc45898a370a13aadb315014b55"
-  ]
-

+ 0 - 11
testdata/configs/allowlist_docx.toml

@@ -1,11 +0,0 @@
-[[rules]]
-	description = "Block dangerous filetypes"
-	file = '''(.*?)(creds.git|gitfile.txt|gitignore|pdf|doc|docx|zip|xls|tfplan|tfstate|tfvars|vault_pass|vagrant|pyc|key|cache)$'''
-	tags = ["key", "extensions"]
-	[rules.allowlist]
-		paths = ['''.docx''']
-		description = "ignore known locations and files"
-
-#[allowlist]
-#	description = "Allowlisted files"
-#	paths = ['''.zip''']

+ 0 - 11
testdata/configs/allowlist_files.toml

@@ -1,11 +0,0 @@
-[[rules]]
-	description = "Block dangerous filetypes"
-	file = '''(.*?)(pdf|doc|docx|zip|xls|tfplan|tfstate|tfvars|vault_pass|vagrant|pyc|key|cache)$'''
-	tags = ["key", "extensions"]
-	[rules.allowlist]
-		paths = ['''.zip''']
-		description = "ignore known locations and files"
-#
-#[allowlist]
-#	description = "Allowlisted files"
-#	paths = ['''.zip''']

+ 0 - 8
testdata/configs/aws_key_allowlist_files.toml

@@ -1,8 +0,0 @@
-[[rules]]
-    description = "AWS Access Key"
-    regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-    tags = ["key", "AWS"]
-        [rules.allowlist]
-            description = "ignore sample regex md files"
-            files = ['''(.*)?md$''']
-            regexes = ['''ignore$''']

+ 0 - 7
testdata/configs/aws_key_allowlist_python_files.toml

@@ -1,7 +0,0 @@
-[[rules]]
-    description = "AWS Access Key"
-    regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-    tags = ["key", "AWS"]
-        [rules.allowlist]
-            description = "ignore python files"
-            files = ['''(.*)?py$''']

+ 0 - 7
testdata/configs/aws_key_aws_allowlisted.toml

@@ -1,7 +0,0 @@
-[[rules]]
-    description = "AWS Access Key"
-    regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-    tags = ["key", "AWS"]
-        [rules.allowlist]
-            description = "ignore aws key"
-            regexes = ['''AKIAIO5FODNN7EXAMPLE.*''']

+ 0 - 14
testdata/configs/aws_key_file_regex.toml

@@ -1,14 +0,0 @@
-[[rules]]
-	description = "AWS Secret Key"
-	regex = '''(?i)aws(.{0,20})?(?-i)['\"][0-9a-zA-Z\/+]{40}['\"]'''
-	tags = ["key", "AWS"]
-
-[[rules]]
-    description = "AWS Access Key"
-    regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-    tags = ["key", "AWS"]
-
-## this is an example of a rule that won't work
-[[rules]]
-	description = "Python files"
-	fileRegex = "(?i)*.py"

+ 0 - 10
testdata/configs/aws_key_global_allowlist_file.toml

@@ -1,10 +0,0 @@
-[[rules]]
-    description = "AWS Access Key"
-    regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-    tags = ["key", "AWS"]
-
-[allowlist]
-    description = "ignore md files"
-    files = [
-        '''(.*)?md$'''
-    ]

+ 0 - 10
testdata/configs/aws_key_global_allowlist_path.toml

@@ -1,10 +0,0 @@
-[[rules]]
-    description = "AWS Access Key"
-    regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-    tags = ["key", "AWS"]
-
-[allowlist]
-    description = "ignore config folders"
-    paths = [
-        '''config(uration)?'''
-    ]

+ 0 - 9
testdata/configs/aws_key_local_owner_allowlist_repo.toml

@@ -1,9 +0,0 @@
-[[rules]]
-    description = "AWS Access Key"
-    regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-    tags = ["key", "AWS"]
-    [allowlist]
-        description = "allowlist repo"
-        repos = [
-            '''test_repo_1'''
-        ]

+ 0 - 10
testdata/configs/aws_key_with_report_groups.toml

@@ -1,10 +0,0 @@
-[[rules]]
-	description = "AWS Secret Key"
-	regex = '''(?i)aws(.{0,20})?(?-i)['\"][0-9a-zA-Z\/+]{40}['\"]'''
-	tags = ["key", "AWS"]
-
-[[rules]]
-    description = "AWS Access Key"
-    regex = '''AWS secret: ("?)((A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16})("?)'''
-    tags = ["key", "AWS"]
-    reportGroup = 2

+ 0 - 9
testdata/configs/bad_aws_key.toml

@@ -1,9 +0,0 @@
-[[rules]]
-	description = "AWS Secret Key"
-	regex = '''(?i)aws(.{0,20})?(?-i)['\"][0-9a-zA-Z\/+]{40}['\"]'''
-	tags = ["key", "AWS"]
-
-[[rules]]
-    description = AWS Access Key"
-    regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-    tags = ["key", "AWS"]

+ 0 - 13
testdata/configs/bad_aws_key_file_regex.toml

@@ -1,13 +0,0 @@
-[[rules]]
-	description = "AWS Secret Key"
-	regex = '''(?i)aws(.{0,20})?(?-i)['\"][0-9a-zA-Z\/+]{40}['\"]'''
-	tags = ["key", "AWS"]
-
-[[rules]]
-    description = "AWS Access Key"
-    regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-    tags = ["key", "AWS"]
-
-[Global]
-    file = '''?????????????'''
-

+ 0 - 8
testdata/configs/bad_aws_key_global_allowlist_file.toml

@@ -1,8 +0,0 @@
-[[rules]]
-    description = "AWS Access Key"
-    regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-    tags = ["key", "AWS"]
-
-[allowlist]
-    description = "ignore md files"
-    files = ['''???????''']

+ 0 - 8
testdata/configs/bad_entropy_1.toml

@@ -1,8 +0,0 @@
-[[rules]]
-	description = "entropy"
-	regex = '''['|"]([0-9a-zA-Z-._{}$\/\+=]{20,120})['|"]'''
-	tags = ["entropy"]
-		[[rules.Entropies]]
-			Min = "4.3"
-			Max = "4.1"
-

+ 0 - 8
testdata/configs/bad_entropy_2.toml

@@ -1,8 +0,0 @@
-[[rules]]
-	description = "entropy"
-	regex = '''['|"]([0-9a-zA-Z-._{}$\/\+=]{20,120})['|"]'''
-	tags = ["entropy"]
-		[[rules.Entropies]]
-			Min = "4.3"
-			Max = "x"
-

+ 0 - 8
testdata/configs/bad_entropy_3.toml

@@ -1,8 +0,0 @@
-[[rules]]
-	description = "entropy"
-	regex = '''['|"]([0-9a-zA-Z-._{}$\/\+=]{20,120})['|"]'''
-	tags = ["entropy"]
-		[[rules.Entropies]]
-			Min = "x"
-			Max = "4.1"
-

+ 0 - 9
testdata/configs/bad_entropy_4.toml

@@ -1,9 +0,0 @@
-[[rules]]
-	description = "entropy"
-	regex = '''['|"]([0-9a-zA-Z-._{}$\/\+=]{20,120})['|"]'''
-	tags = ["entropy"]
-		[[rules.Entropies]]
-			Min = "1.0"
-			Max = "8"
-			Group = "x"
-

+ 0 - 9
testdata/configs/bad_entropy_5.toml

@@ -1,9 +0,0 @@
-[[rules]]
-	description = "entropy"
-	regex = '''['|"]([0-9a-zA-Z-._{}$\/\+=]{20,120})['|"]'''
-	tags = ["entropy"]
-		[[rules.Entropies]]
-			Min = "1.0"
-			Max = "8.5"
-			Group = "-2"
-

+ 0 - 9
testdata/configs/bad_entropy_6.toml

@@ -1,9 +0,0 @@
-[[rules]]
-	description = "entropy"
-	regex = '''['|"]([0-9a-zA-Z-._{}$\/\+=]{20,120})['|"]'''
-	tags = ["entropy"]
-		[[rules.Entropies]]
-			Min = "1.0"
-			Max = "8.5"
-			Group = "2"
-

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio