Kaynağa Gözat

Introducing v8.0.0 changes (#701)

* Introducing v8.0.0 changes
Zachary Rice 4 yıl önce
ebeveyn
işleme
93f292c3df
100 değiştirilmiş dosya ile 3923 ekleme ve 4502 silme
  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
 *.got
 gitleaks
 gitleaks
 build
 build
+.gitleaks.toml
 
 
 # Test binary
 # Test binary
 *.out
 *.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
 FROM golang:1.17 AS build
 WORKDIR /go/src/github.com/zricethezav/gitleaks
 WORKDIR /go/src/github.com/zricethezav/gitleaks
-ARG ldflags
 COPY . .
 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
 FROM alpine:3.14.2
 RUN adduser -D gitleaks && \
 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
 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
 COVER=--cover --coverprofile=cover.out
 
 
 test-cover:
 test-cover:
@@ -14,34 +13,14 @@ format:
 	go fmt ./...
 	go fmt ./...
 
 
 test: format
 test: format
-	go get golang.org/x/lint/golint
 	go vet ./...
 	go vet ./...
-	golint ./...
 	go test ./... --race $(PKG) -v
 	go test ./... --race $(PKG) -v
 
 
 build: format
 build: format
-	golint ./...
 	go vet ./...
 	go vet ./...
 	go mod tidy
 	go mod tidy
 	go build $(LDFLAGS)
 	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>
 </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.
 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
 ```bash
 brew install gitleaks
 brew install gitleaks
 ```
 ```
 
 
-##### Docker
-
-Building the image after cloning the repo:
-```bash
-make dockerbuild
-```
-
+### Docker
 Using the image from DockerHub:
 Using the image from DockerHub:
 ```bash
 ```bash
-# To just pull the image
 docker pull zricethezav/gitleaks:latest
 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
 ```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
 ```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:
 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
 ```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]]
 [[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
 0 - no leaks present
 1 - leaks or error encountered
 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
 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
 	Description string
 	Regexes     []*regexp.Regexp
 	Regexes     []*regexp.Regexp
-	Commits     []string
-	Files       []*regexp.Regexp
 	Paths       []*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 true
 		}
 		}
 	}
 	}
 	return false
 	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 (
 import (
 	_ "embed"
 	_ "embed"
 	"fmt"
 	"fmt"
-	"io"
-	"os"
-	"path"
-	"path/filepath"
 	"regexp"
 	"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
 //go:embed gitleaks.toml
 var DefaultConfig string
 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
 	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{
 	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 (
 import (
 	"fmt"
 	"fmt"
-	"io/ioutil"
-	"os"
-	"path/filepath"
 	"regexp"
 	"regexp"
 	"testing"
 	"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 {
 	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 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"
 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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]]
 [[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]
 [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
 package config
 
 
 import (
 import (
-	"math"
-	"path/filepath"
 	"regexp"
 	"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 {
 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
 		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
 go 1.16
 
 
-replace github.com/go-git/go-git/v5 => github.com/zricethezav/go-git/v5 v5.3.0
-
 require (
 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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 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/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.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/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.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.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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 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.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.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.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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 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-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/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-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-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-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-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/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.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-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 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-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.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-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 (
 import (
 	"os"
 	"os"
 	"os/signal"
 	"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() {
 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
 	// this block sets up a go routine to listen for an interrupt signal
 	// which will immediately exit gitleaks
 	// which will immediately exit gitleaks
 	stopChan := make(chan os.Signal, 1)
 	stopChan := make(chan os.Signal, 1)
 	signal.Notify(stopChan, os.Interrupt)
 	signal.Notify(stopChan, os.Interrupt)
 	go listenForInterrupt(stopChan)
 	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) {
 func listenForInterrupt(stopScan chan os.Signal) {
 	<-stopScan
 	<-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]]
 [[rules]]
     description = "AWS Access Key"
     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}'''
     regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
     tags = ["key", "AWS"]
     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]]
 [[rules]]
     description = "AWS Access Key"
     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}'''
     regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
     tags = ["key", "AWS"]
     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]]
 [[rules]]
     description = "AWS Access Key"
     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}'''
     regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
     tags = ["key", "AWS"]
     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"
-

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor