commit 48b3ccf279fbe2cb84a620eb2966002a66d3234e Author: wehub-resource-sync Date: Mon Jul 13 12:12:29 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..57c60df --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,8 @@ +# Default — every PR requires at least one org member review. +* @bryanbeverly @dustin-decker @dxa4481 @zricethezav + +# Release-critical paths +/.github/ @bryanbeverly @dustin-decker @dxa4481 @zricethezav +/.goreleaser.yml @bryanbeverly @dustin-decker @dxa4481 @zricethezav +/go.mod @bryanbeverly @dustin-decker @dxa4481 @zricethezav +/go.sum @bryanbeverly @dustin-decker @dxa4481 @zricethezav diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..c9e8d91 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [zricethezav] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..6e45ca8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Basic Info (please complete the following information):** + - OS: + - Gitleaks Version: + +**Additional context** +Add any other context about the problem here. + +cc @zricethezav + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..023f401 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. + +cc @zricethezav diff --git a/.github/ISSUE_TEMPLATE/maintenance.md b/.github/ISSUE_TEMPLATE/maintenance.md new file mode 100644 index 0000000..7f42bb9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/maintenance.md @@ -0,0 +1,16 @@ +--- +name: Maintenance request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context or screenshots about the feature request here. + +cc @zricethezav diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..11500c5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +### Description: +Explain the purpose of the PR. + +### Checklist: + +* [ ] Does your PR pass tests? +* [ ] Have you written new tests for your changes? +* [ ] Have you lint your code locally prior to submission? diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..71bdfa7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + groups: + actions: + patterns: + - "*" diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml new file mode 100644 index 0000000..18d9c48 --- /dev/null +++ b/.github/workflows/gitleaks.yml @@ -0,0 +1,17 @@ +name: gitleaks +on: + push: + workflow_dispatch: +jobs: + scan: + name: gitleaks + runs-on: ubuntu-latest + if: ${{ github.repository == 'gitleaks/gitleaks' }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + - uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE}} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1ad5c31 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up QEMU + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + + - name: Log in to Docker Hub + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + username: ${{ github.actor }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Log in to the Container registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + with: + images: | + zricethezav/gitleaks + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + with: + platforms: linux/amd64,linux/arm64 + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..73c33ca --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,37 @@ +name: Test + +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" + +jobs: + test: + strategy: + matrix: + platform: [ ubuntu-latest, windows-latest ] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + with: + go-version: 1.25 + + - name: Build + run: go build ./... + + - name: Set up gotestsum + run: | + go install gotest.tools/gotestsum@latest + + - name: Test + run: | + gotestsum --raw-command -- go test -json ./... --race + + - name: Validate Config + run: go generate ./... && git diff --exit-code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b4ef2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.DS_STORE +*.idea +*.got +gitleaks +build +profile + +# configs +.gitleaks.toml +cmd/generate/config/gitleaks.toml + +# test results +testdata/expected/report/*.got.* + +# Test binary +*.out + +dist/ diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 0000000..f38a99f --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,944 @@ +418edf165dbb63d6f46993ae8f8818ffd87ea582:cmd/generate/config/rules/jwt.go:jwt:17 +418edf165dbb63d6f46993ae8f8818ffd87ea582:cmd/generate/config/rules/jwt.go:jwt:19 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:46 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:48 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:50 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:52 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:54 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:55 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:56 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:57 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:cmd/generate/config/rules/sidekiq.go:sidekiq-secret:22 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:cmd/generate/config/rules/sidekiq.go:sidekiq-secret:23 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:cmd/generate/config/rules/sidekiq.go:sidekiq-secret:24 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:cmd/generate/config/rules/sidekiq.go:sidekiq-secret:28 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:cmd/generate/config/rules/sidekiq.go:sidekiq-secret:29 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:detect/detect_test.go:sidekiq-sensitive-url:164 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:detect/detect_test.go:sidekiq-sensitive-url:170 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:detect/detect_test.go:sidekiq-secret:120 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:detect/detect_test.go:sidekiq-secret:126 +525d9792b1e3670b4630b8fcc385ca22e8544f9b:detect/detect_test.go:sidekiq-secret:142 +31650f01e76858ce7a0490943426e84a0824bbc8:config/config_test.go:aws-access-token:31 +5ed010c944ccc715cb9245abafd4e97d98d75e9f:config/config_test.go:aws-access-token:31 +ad7509e3b47331ce9586743ace635422843b695b:cmd/generate/config/rules/privatekey.go:private-key:22 +717cf1b10be1625875199eca8cdf48883348985f:README.md:aws-access-token:23 +3474c58c9e25fe2b1cee855a35bd1bf8a8c0fae8:cmd/generate/config/rules/generic.go:clojars-api-token:38 +a42b32bdf11b6f4ea5c32ec76a1731b4b0c5e52a:cmd/generate/config/rules/generic.go:generic-api-key:40 +a42b32bdf11b6f4ea5c32ec76a1731b4b0c5e52a:cmd/generate/config/rules/generic.go:generic-api-key:41 +3e5e63956ea770be734dcb7642cf515910154fb5:detect/detect_test.go:aws-access-token:50 +3e5e63956ea770be734dcb7642cf515910154fb5:detect/detect_test.go:aws-access-token:60 +3e5e63956ea770be734dcb7642cf515910154fb5:detect/detect_test.go:aws-access-token:61 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:pypi-upload-token:29 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:51 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:73 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:81 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:89 +33082a996774ba4c8ad4ba26fd219d77497eb960:README.md:sidekiq-secret:43 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:discord-api-token:98 +57d2d345b6b6ea220be9d99792b21a75359555c0:detect/detect_test.go:aws-access-token:235 +57d2d345b6b6ea220be9d99792b21a75359555c0:detect/detect_test.go:aws-access-token:236 +57d2d345b6b6ea220be9d99792b21a75359555c0:detect/detect_test.go:aws-access-token:253 +57d2d345b6b6ea220be9d99792b21a75359555c0:detect/detect_test.go:aws-access-token:254 +57d2d345b6b6ea220be9d99792b21a75359555c0:detect/detect_test.go:aws-access-token:278 +57d2d345b6b6ea220be9d99792b21a75359555c0:detect/detect_test.go:aws-access-token:279 +57d2d345b6b6ea220be9d99792b21a75359555c0:detect/detect_test.go:aws-access-token:343 +57d2d345b6b6ea220be9d99792b21a75359555c0:detect/detect_test.go:aws-access-token:344 +57d2d345b6b6ea220be9d99792b21a75359555c0:detect/detect_test.go:aws-access-token:362 +57d2d345b6b6ea220be9d99792b21a75359555c0:detect/detect_test.go:aws-access-token:363 +c9bc6b46087e700a42eb3492cf2053b7da4a6d9e:detect/detect_test.go:pypi-upload-token:27 +c9bc6b46087e700a42eb3492cf2053b7da4a6d9e:detect/detect_test.go:aws-access-token:49 +c9bc6b46087e700a42eb3492cf2053b7da4a6d9e:detect/detect_test.go:aws-access-token:71 +c9bc6b46087e700a42eb3492cf2053b7da4a6d9e:detect/detect_test.go:aws-access-token:79 +c9bc6b46087e700a42eb3492cf2053b7da4a6d9e:detect/detect_test.go:aws-access-token:87 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:discord-api-token:120 +c9bc6b46087e700a42eb3492cf2053b7da4a6d9e:detect/detect_test.go:discord-api-token:96 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:discord-api-token:128 +c9bc6b46087e700a42eb3492cf2053b7da4a6d9e:detect/detect_test.go:discord-api-token:118 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:discord-api-token:150 +c9bc6b46087e700a42eb3492cf2053b7da4a6d9e:detect/detect_test.go:discord-api-token:126 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:discord-api-token:166 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:175 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:183 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:238 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:239 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:256 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:257 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:281 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:282 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:356 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:357 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:375 +6e72472b6019d29eaa4b76b39700cd2418741f0c:detect/detect_test.go:aws-access-token:376 +c9bc6b46087e700a42eb3492cf2053b7da4a6d9e:detect/detect_test.go:discord-api-token:148 +17b5540fb16bd9816d2a3a83f65cedf5918eaf70:detect/detect_test.go:pypi-upload-token:27 +17b5540fb16bd9816d2a3a83f65cedf5918eaf70:detect/detect_test.go:pypi-upload-token:32 +17b5540fb16bd9816d2a3a83f65cedf5918eaf70:detect/detect_test.go:pypi-upload-token:33 +c9bc6b46087e700a42eb3492cf2053b7da4a6d9e:detect/detect_test.go:discord-api-token:164 +c9bc6b46087e700a42eb3492cf2053b7da4a6d9e:detect/detect_test.go:aws-access-token:173 +c9bc6b46087e700a42eb3492cf2053b7da4a6d9e:detect/detect_test.go:aws-access-token:181 +ce42947cae32cda8d5d8813c1a8ce82eb06f018e:detect/git_test.go:aws-access-token:43 +ce42947cae32cda8d5d8813c1a8ce82eb06f018e:detect/git_test.go:aws-access-token:60 +ce42947cae32cda8d5d8813c1a8ce82eb06f018e:detect/git_test.go:aws-access-token:85 +b8141713e89f5acec500c7a62415f0f1cb7234dc:README.md:discord-client-secret:273 +98d5648f6e54ea84ca915c73fff88ad3bc5809e0:README.md:aws-access-token:130 +b8141713e89f5acec500c7a62415f0f1cb7234dc:README.md:discord-client-secret:289 +98d5648f6e54ea84ca915c73fff88ad3bc5809e0:README.md:discord-client-secret:251 +98d5648f6e54ea84ca915c73fff88ad3bc5809e0:README.md:discord-client-secret:267 +98d5648f6e54ea84ca915c73fff88ad3bc5809e0:detect/detect_test.go:aws-access-token:33 +98d5648f6e54ea84ca915c73fff88ad3bc5809e0:detect/files_test.go:aws-access-token:32 +98d5648f6e54ea84ca915c73fff88ad3bc5809e0:detect/files_test.go:aws-access-token:50 +98d5648f6e54ea84ca915c73fff88ad3bc5809e0:config/config_test.go:generic-api-key:133 +80d29764a0bc0f269607d3eb7b2774a0f31e5da3:detect/detect_test.go:aws-access-token:123 +80d29764a0bc0f269607d3eb7b2774a0f31e5da3:testdata/config/allow_global_aws_re.toml:aws-access-token:8 +4acd7a3c8d20b7116a32f9bbade6a3cf15f741e4:detect/detect_test.go:aws-access-token:117 +f0b8d26c9988af725132c100dda5051586a3026e:README.md:discord-client-secret:225 +98d5648f6e54ea84ca915c73fff88ad3bc5809e0:config/config_test.go:generic-api-key:144 +93f292c3dfa2649ef91f8925b623e79546fa992e:README.md:aws-access-token:121 +93f292c3dfa2649ef91f8925b623e79546fa992e:README.md:aws-access-token:122 +93f292c3dfa2649ef91f8925b623e79546fa992e:README.md:aws-access-token:157 +93f292c3dfa2649ef91f8925b623e79546fa992e:config/config_test.go:aws-access-token:31 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/files_test.go:aws-access-token:32 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/files_test.go:aws-access-token:33 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/files_test.go:aws-access-token:50 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/files_test.go:aws-access-token:51 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/git_test.go:aws-access-token:42 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/git_test.go:aws-access-token:44 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/git_test.go:aws-access-token:58 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/git_test.go:aws-access-token:60 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/git_test.go:aws-access-token:82 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/git_test.go:aws-access-token:83 +93f292c3dfa2649ef91f8925b623e79546fa992e:config/config_test.go:generic-api-key:133 +93f292c3dfa2649ef91f8925b623e79546fa992e:testdata/config/allow_aws_re.toml:aws-access-token:9 +93f292c3dfa2649ef91f8925b623e79546fa992e:config/config_test.go:generic-api-key:144 +93f292c3dfa2649ef91f8925b623e79546fa992e:testdata/expected/git/small-branch-foo.txt:aws-access-token:15 +93f292c3dfa2649ef91f8925b623e79546fa992e:testdata/repos/nogit/main.go:aws-access-token:20 +93f292c3dfa2649ef91f8925b623e79546fa992e:testdata/expected/git/small.txt:aws-access-token:15 +93f292c3dfa2649ef91f8925b623e79546fa992e:testdata/expected/git/small.txt:aws-access-token:44 +3a3e13c3b5f85b0116cf2a0cd92529baf22d0ac9:testdata/repos/with_square_and_google/env:generic-api-key:3 +6adc045580c3911a7a936be7b977979a5519aa29:scan/unstaged_test.go:aws-access-token:38 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_208ae46.json:aws-access-token:3 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_208ae46.json:aws-access-token:5 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_ae8db4a2_208ae46.json:aws-access-token:3 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_ae8db4a2_208ae46.json:aws-access-token:5 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results.json:aws-access-token:3 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results.json:aws-access-token:5 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results.json:aws-access-token:20 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results.json:aws-access-token:22 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_depth_1.json:aws-access-token:3 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_depth_1.json:aws-access-token:5 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_files_at_208ae46.json:aws-access-token:3 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_files_at_208ae46.json:aws-access-token:5 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_files_at_208ae46.json:aws-access-token:20 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_files_at_208ae46.json:aws-access-token:22 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_no_git.json:aws-access-token:3 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_no_git.json:aws-access-token:5 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/detect_test.go:aws-access-token:26 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/detect_test.go:aws-access-token:31 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/detect_test.go:aws-access-token:40 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/detect_test.go:aws-access-token:46 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/detect_test.go:aws-access-token:52 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/detect_test.go:discord-api-token:59 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/detect_test.go:discord-api-token:74 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/detect_test.go:discord-api-token:80 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/detect_test.go:discord-api-token:95 +93f292c3dfa2649ef91f8925b623e79546fa992e:detect/detect_test.go:discord-api-token:109 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_no_git.json:aws-access-token:20 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_no_git.json:aws-access-token:22 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/basic/results_unstaged.json:aws-access-token:3 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/repos/basic/secrets.py:generic-api-key:5 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/repos/basic/secrets.py:generic-api-key:6 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/repos/basic/secrets.py:aws-access-token:9 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/repos/basic/secrets.py:aws-access-token:13 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results.json:aws-access-token:3 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results.json:aws-access-token:5 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results.json:aws-access-token:54 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results.json:aws-access-token:56 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results.json:generic-api-key:20 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results.json:generic-api-key:22 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results.json:generic-api-key:37 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results.json:generic-api-key:39 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results_e7c0aff3.json:generic-api-key:3 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results_e7c0aff3.json:generic-api-key:5 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results_e7c0aff3.json:generic-api-key:20 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results_e7c0aff3.json:generic-api-key:22 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results_e7c0aff3.json:aws-access-token:37 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results_e7c0aff3.json:aws-access-token:39 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results_ae8db4a_e7c0aff.json:aws-access-token:37 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results_ae8db4a_e7c0aff.json:aws-access-token:39 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results_ae8db4a_e7c0aff.json:generic-api-key:3 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results_ae8db4a_e7c0aff.json:generic-api-key:5 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results_ae8db4a_e7c0aff.json:generic-api-key:20 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/expect/with_config/results_ae8db4a_e7c0aff.json:generic-api-key:22 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/repos/with_config/secrets.py:generic-api-key:5 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/repos/with_config/secrets.py:generic-api-key:6 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/repos/with_config/secrets.py:aws-access-token:9 +6adc045580c3911a7a936be7b977979a5519aa29:testdata/repos/with_config/secrets.py:aws-access-token:13 +45c898c5ea56ee503a048c1bac1404cf63855edc:test_data/test_repos/test_dir_2/env:generic-api-key:1 +45c898c5ea56ee503a048c1bac1404cf63855edc:test_data/test_repos/test_dir_2/env:generic-api-key:3 +45c898c5ea56ee503a048c1bac1404cf63855edc:test_data/test_dir_one_google_leak_and_square_leak.json:generic-api-key:3 +45c898c5ea56ee503a048c1bac1404cf63855edc:test_data/test_dir_one_google_leak_and_square_leak.json:generic-api-key:20 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:35 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:37 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:259 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:261 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:339 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:51 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:83 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:85 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:131 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:133 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:147 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:149 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:179 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:227 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:229 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:323 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:355 +65f42020afcc65de3b7dfe5a797b9f6396345250:test_data/test_local_owner_aws_leak.json:aws-access-token:357 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_file1_aws_leak.json:aws-access-token:3 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_file1_aws_leak.json:aws-access-token:5 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_dir1_aws_leak.json:aws-access-token:3 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_dir1_aws_leak.json:aws-access-token:5 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_dir1_aws_leak.json:aws-access-token:17 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_dir1_aws_leak.json:aws-access-token:19 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_repos/test_dir_1/server.test.py:aws-access-token:5 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_repos/test_dir_1/server.test2.py:aws-access-token:5 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:99 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:101 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:115 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:117 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:131 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:133 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:147 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:149 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:163 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:165 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:179 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:181 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:195 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:197 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:211 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:213 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:83 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:85 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:99 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:101 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:115 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:117 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:131 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:133 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:147 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:149 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:163 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:165 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:179 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:259 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:261 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:181 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:195 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:197 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:339 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak.json:aws-access-token:341 +2acc34dfdfb0e7f1141eadd940ea933c645e0f86:test_data/test_local_repo_three_leaks_with_report_groups.json:aws-access-token:3 +2acc34dfdfb0e7f1141eadd940ea933c645e0f86:test_data/test_local_repo_three_leaks_with_report_groups.json:aws-access-token:5 +2acc34dfdfb0e7f1141eadd940ea933c645e0f86:test_data/test_local_repo_three_leaks_with_report_groups.json:aws-access-token:18 +2acc34dfdfb0e7f1141eadd940ea933c645e0f86:test_data/test_local_repo_three_leaks_with_report_groups.json:aws-access-token:20 +2acc34dfdfb0e7f1141eadd940ea933c645e0f86:test_data/test_local_repo_three_leaks_with_report_groups.json:aws-access-token:33 +2acc34dfdfb0e7f1141eadd940ea933c645e0f86:test_data/test_local_repo_three_leaks_with_report_groups.json:aws-access-token:35 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:259 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:261 +ad505754fc6283523ef8dfa88ecb6559e0268ec3:test_data/test_local_repo_two_leaks_commit_to_from.json:aws-access-token:3 +ad505754fc6283523ef8dfa88ecb6559e0268ec3:test_data/test_local_repo_two_leaks_commit_to_from.json:aws-access-token:5 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:323 +c50906373cc38206f3d41c57e2c413ce400e61e7:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:325 +ad505754fc6283523ef8dfa88ecb6559e0268ec3:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:18 +ad505754fc6283523ef8dfa88ecb6559e0268ec3:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:20 +fae366a404357fa6c6611756a11e61546ebec4e0:examples/simple_regex_and_allowlist_config.toml:aws-access-token:12 +ad505754fc6283523ef8dfa88ecb6559e0268ec3:test_data/test_local_repo_two_leaks_file_commit_range.json:aws-access-token:3 +ad505754fc6283523ef8dfa88ecb6559e0268ec3:test_data/test_local_repo_two_leaks_file_commit_range.json:aws-access-token:5 +ad505754fc6283523ef8dfa88ecb6559e0268ec3:test_data/test_local_repo_two_leaks_file_commit_range.json:aws-access-token:18 +ad505754fc6283523ef8dfa88ecb6559e0268ec3:test_data/test_local_repo_two_leaks_file_commit_range.json:aws-access-token:20 +fae366a404357fa6c6611756a11e61546ebec4e0:test_data/test_configs/aws_key_aws_allowlisted.toml:aws-access-token:7 +fae366a404357fa6c6611756a11e61546ebec4e0:test_data/test_local_owner_aws_leak.json:aws-access-token:185 +fae366a404357fa6c6611756a11e61546ebec4e0:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:95 +fae366a404357fa6c6611756a11e61546ebec4e0:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:185 +fae366a404357fa6c6611756a11e61546ebec4e0:test_data/test_local_repo_nine_aws_leak.json:aws-access-token:3 +fae366a404357fa6c6611756a11e61546ebec4e0:test_data/test_local_repo_nine_aws_leak.json:aws-access-token:5 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:78 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:80 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_owner_aws_leak.json:aws-access-token:138 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_owner_aws_leak.json:aws-access-token:140 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_owner_aws_leak.json:aws-access-token:153 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_owner_aws_leak.json:aws-access-token:155 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_owner_aws_leak.json:aws-access-token:168 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_owner_aws_leak.json:aws-access-token:170 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:138 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:140 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:153 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:155 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:168 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_owner_aws_leak_allowlist_repo.json:aws-access-token:170 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_repo_eight.json:aws-access-token:3 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_repo_eight.json:aws-access-token:5 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_repo_eight.json:aws-access-token:18 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_repo_eight.json:aws-access-token:20 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_repo_eight.json:aws-access-token:33 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_local_repo_eight.json:aws-access-token:35 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_repos/test_repo_8/dummy.txt:aws-access-token:2 +eabc7b5c12cb89b4a828b7aaef00fdfa694a7e0a:test_data/test_repos/test_repo_8/dummy.txt:aws-access-token:6 +6ca7a11d8846af2a3d26f71c4a9083eb0b3deeee:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:48 +6ca7a11d8846af2a3d26f71c4a9083eb0b3deeee:test_data/test_local_owner_aws_leak.json:aws-access-token:48 +6ca7a11d8846af2a3d26f71c4a9083eb0b3deeee:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:33 +6ca7a11d8846af2a3d26f71c4a9083eb0b3deeee:test_data/test_local_owner_aws_leak.json:aws-access-token:93 +6ca7a11d8846af2a3d26f71c4a9083eb0b3deeee:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:78 +6ca7a11d8846af2a3d26f71c4a9083eb0b3deeee:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:18 +6ca7a11d8846af2a3d26f71c4a9083eb0b3deeee:test_data/test_local_repo_three_leaks.json:aws-access-token:48 +6ca7a11d8846af2a3d26f71c4a9083eb0b3deeee:test_data/test_local_repo_two_leaks_deletion.json:aws-access-token:78 +6ca7a11d8846af2a3d26f71c4a9083eb0b3deeee:test_data/test_local_repo_two_leaks.json:aws-access-token:33 +bdc688dd5351adf1cf5cbf734dfd104ea6bab92b:test_data/test_local_repo_two_whitelist_commits.json:aws-access-token:3 +bdc688dd5351adf1cf5cbf734dfd104ea6bab92b:test_data/test_local_repo_two_whitelist_commits.json:aws-access-token:4 +bdc688dd5351adf1cf5cbf734dfd104ea6bab92b:test_data/test_local_repo_two_whitelist_commits.json:aws-access-token:16 +bdc688dd5351adf1cf5cbf734dfd104ea6bab92b:test_data/test_local_repo_two_whitelist_commits.json:aws-access-token:17 +e0f6399d5ccadd44544cdfe5f630de57fced1473:test_data/test_local_repo_six_leaks_since_date.json:aws-access-token:3 +e0f6399d5ccadd44544cdfe5f630de57fced1473:test_data/test_local_repo_six_leaks_since_date.json:aws-access-token:4 +e0f6399d5ccadd44544cdfe5f630de57fced1473:test_data/test_local_repo_six_leaks_until_date.json:aws-access-token:3 +e0f6399d5ccadd44544cdfe5f630de57fced1473:test_data/test_local_repo_six_leaks_until_date.json:aws-access-token:4 +0d7c18cb39c60efb62a6c7b351f95d46ebe1f63e:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:172 +0d7c18cb39c60efb62a6c7b351f95d46ebe1f63e:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:173 +e0f6399d5ccadd44544cdfe5f630de57fced1473:test_data/test_local_repo_four_leaks_commit_timerange.json:aws-access-token:3 +e0f6399d5ccadd44544cdfe5f630de57fced1473:test_data/test_local_repo_four_leaks_commit_timerange.json:aws-access-token:4 +e0f6399d5ccadd44544cdfe5f630de57fced1473:test_data/test_local_repo_four_leaks_commit_timerange.json:aws-access-token:16 +e0f6399d5ccadd44544cdfe5f630de57fced1473:test_data/test_local_repo_four_leaks_commit_timerange.json:aws-access-token:17 +e0f6399d5ccadd44544cdfe5f630de57fced1473:test_data/test_local_repo_four_leaks_commit_timerange.json:aws-access-token:29 +e0f6399d5ccadd44544cdfe5f630de57fced1473:test_data/test_local_repo_four_leaks_commit_timerange.json:aws-access-token:30 +e0f6399d5ccadd44544cdfe5f630de57fced1473:test_data/test_local_repo_four_leaks_commit_timerange.json:aws-access-token:42 +e0f6399d5ccadd44544cdfe5f630de57fced1473:test_data/test_local_repo_four_leaks_commit_timerange.json:aws-access-token:43 +e0f6399d5ccadd44544cdfe5f630de57fced1473:test_data/test_local_repo_four_leaks_commit_timerange.json:aws-access-token:55 +e0f6399d5ccadd44544cdfe5f630de57fced1473:test_data/test_local_repo_four_leaks_commit_timerange.json:aws-access-token:56 +cfbb600fcb286e1323e2e4d24144eeca163ad396:test_data/test_local_repo_two_leaks_deletion.json:aws-access-token:3 +cfbb600fcb286e1323e2e4d24144eeca163ad396:test_data/test_local_repo_two_leaks_deletion.json:aws-access-token:4 +cfbb600fcb286e1323e2e4d24144eeca163ad396:test_data/test_local_repo_two_leaks_deletion.json:aws-access-token:16 +cfbb600fcb286e1323e2e4d24144eeca163ad396:test_data/test_local_repo_two_leaks_deletion.json:aws-access-token:17 +cfbb600fcb286e1323e2e4d24144eeca163ad396:test_data/test_local_repo_two_leaks_deletion.json:aws-access-token:29 +cfbb600fcb286e1323e2e4d24144eeca163ad396:test_data/test_local_repo_two_leaks_deletion.json:aws-access-token:30 +cfbb600fcb286e1323e2e4d24144eeca163ad396:test_data/test_local_repo_two_leaks_deletion.json:aws-access-token:42 +cfbb600fcb286e1323e2e4d24144eeca163ad396:test_data/test_local_repo_two_leaks_deletion.json:aws-access-token:43 +cfbb600fcb286e1323e2e4d24144eeca163ad396:test_data/test_local_repo_two_leaks_deletion.json:aws-access-token:55 +cfbb600fcb286e1323e2e4d24144eeca163ad396:test_data/test_local_repo_two_leaks_deletion.json:aws-access-token:56 +cfbb600fcb286e1323e2e4d24144eeca163ad396:test_data/test_local_repo_two_leaks_deletion.json:aws-access-token:68 +cfbb600fcb286e1323e2e4d24144eeca163ad396:test_data/test_local_repo_two_leaks_deletion.json:aws-access-token:69 +212232f80a22c4c698dd2864006f398ea1f15107:test_data/test_local_repo_seven_aws_leak_uncommitted.json:aws-access-token:3 +212232f80a22c4c698dd2864006f398ea1f15107:test_data/test_local_repo_seven_aws_leak_uncommitted.json:aws-access-token:4 +212232f80a22c4c698dd2864006f398ea1f15107:test_data/test_repos/test_repo_7/file:aws-access-token:5 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:16 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:17 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:55 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:56 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:16 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:17 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:94 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:95 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:55 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:56 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:134 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:160 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:94 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:95 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:133 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:134 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:172 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:173 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:3 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:4 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:16 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:17 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:29 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:30 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:42 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:43 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:55 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:56 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:68 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:69 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:81 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:82 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:94 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:95 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:107 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:108 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:120 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:121 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:133 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:134 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:146 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:147 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:159 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:160 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:172 +473d0d55a71097894c5c6b9a2968ad478d2e6edf:test_data/test_local_owner_aws_leak_whitelist_repo.json:aws-access-token:173 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:185 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:186 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_five_files_at_commit.json:aws-access-token:29 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_five_files_at_commit.json:aws-access-token:30 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_five_files_at_commit.json:aws-access-token:42 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_five_files_at_commit.json:aws-access-token:43 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:224 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:225 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:211 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:212 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:263 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:264 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:237 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:238 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_one_aws_leak.json:aws-access-token:16 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_one_aws_leak.json:aws-access-token:17 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_one_aws_leak_commit.json:aws-access-token:16 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:276 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:277 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_one_aws_leak_commit.json:aws-access-token:17 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:316 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:342 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks.json:aws-access-token:16 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks.json:aws-access-token:17 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks.json:aws-access-token:55 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks.json:aws-access-token:56 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks.json:aws-access-token:95 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks.json:aws-access-token:121 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_five_files_at_latest_commit.json:aws-access-token:3 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_five_files_at_latest_commit.json:aws-access-token:4 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_five_files_at_latest_commit.json:aws-access-token:16 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_five_files_at_latest_commit.json:aws-access-token:17 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_five_files_at_latest_commit.json:aws-access-token:29 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_five_files_at_latest_commit.json:aws-access-token:30 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_five_files_at_latest_commit.json:aws-access-token:42 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_five_files_at_latest_commit.json:aws-access-token:43 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_one_aws_leak_uncommitted.json:aws-access-token:16 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_one_aws_leak_uncommitted.json:aws-access-token:17 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:17 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:43 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:17 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:43 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks.json:aws-access-token:146 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks.json:aws-access-token:147 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:380 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:381 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:406 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:407 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:445 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_owner_aws_leak.json:aws-access-token:446 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_to.json:aws-access-token:16 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_to.json:aws-access-token:17 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:68 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:69 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_to.json:aws-access-token:55 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_to.json:aws-access-token:56 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks.json:aws-access-token:172 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks.json:aws-access-token:173 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:68 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:69 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_to.json:aws-access-token:95 +94cae90d1eb208410073669de54a21fb65f23742:test_data/test_local_owner_aws_leak.json:aws-access-token:120 +94cae90d1eb208410073669de54a21fb65f23742:test_data/test_local_owner_aws_leak.json:aws-access-token:121 +94cae90d1eb208410073669de54a21fb65f23742:test_data/test_local_repo_two_leaks.json:aws-access-token:107 +94cae90d1eb208410073669de54a21fb65f23742:test_data/test_local_repo_two_leaks.json:aws-access-token:108 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:94 +c6f15b768e19613228b232a712f53a39cc5120a3:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:95 +94cae90d1eb208410073669de54a21fb65f23742:test_data/test_local_repo_three_leaks.json:aws-access-token:81 +94cae90d1eb208410073669de54a21fb65f23742:test_data/test_local_repo_three_leaks.json:aws-access-token:82 +c4b07f5113bf39019374579a64d83959236295e4:audit/util.go:aws-access-token:66 +94cae90d1eb208410073669de54a21fb65f23742:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:55 +94cae90d1eb208410073669de54a21fb65f23742:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:56 +94cae90d1eb208410073669de54a21fb65f23742:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:55 +94cae90d1eb208410073669de54a21fb65f23742:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:56 +c4b07f5113bf39019374579a64d83959236295e4:test_data/test_local_owner_aws_leak.json:aws-access-token:276 +c4b07f5113bf39019374579a64d83959236295e4:test_data/test_local_owner_aws_leak.json:aws-access-token:277 +c4b07f5113bf39019374579a64d83959236295e4:test_data/test_local_repo_one_aws_leak_and_file_leak.json:aws-access-token:16 +c4b07f5113bf39019374579a64d83959236295e4:test_data/test_local_repo_one_aws_leak_and_file_leak.json:aws-access-token:17 +c4b07f5113bf39019374579a64d83959236295e4:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:146 +c4b07f5113bf39019374579a64d83959236295e4:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:147 +c4b07f5113bf39019374579a64d83959236295e4:test_data/test_local_repo_six.json:aws-access-token:31 +c4b07f5113bf39019374579a64d83959236295e4:test_data/test_local_repo_six.json:aws-access-token:33 +c4b07f5113bf39019374579a64d83959236295e4:test_data/test_local_repo_six_filepath.json:aws-access-token:3 +c4b07f5113bf39019374579a64d83959236295e4:test_data/test_local_repo_six_filepath.json:aws-access-token:4 +c4b07f5113bf39019374579a64d83959236295e4:test_data/test_local_repo_six_path_globally_whitelisted.json:aws-access-token:3 +c4b07f5113bf39019374579a64d83959236295e4:test_data/test_local_repo_six_path_globally_whitelisted.json:aws-access-token:4 +c4b07f5113bf39019374579a64d83959236295e4:test_data/test_repos/test_repo_6/server.test.py:aws-access-token:5 +10745be661121f2e6577170419cf8ce3308ae311:test_data/test_local_repo_two_leaks_commit_to.json:aws-access-token:55 +10745be661121f2e6577170419cf8ce3308ae311:test_data/test_local_repo_two_leaks_commit_to.json:aws-access-token:56 +10745be661121f2e6577170419cf8ce3308ae311:test_data/test_local_repo_two_leaks_commit_to.json:aws-access-token:68 +10745be661121f2e6577170419cf8ce3308ae311:test_data/test_local_repo_two_leaks_commit_to.json:aws-access-token:69 +c4b07f5113bf39019374579a64d83959236295e4:test_data/test_repos/test_repo_6/config/application.properties:aws-access-token:3 +d13e0fdfde8f5c6261c9e893461f32861e21e8f5:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:133 +d13e0fdfde8f5c6261c9e893461f32861e21e8f5:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:134 +d13e0fdfde8f5c6261c9e893461f32861e21e8f5:test_data/test_local_owner_aws_leak.json:aws-access-token:250 +d13e0fdfde8f5c6261c9e893461f32861e21e8f5:test_data/test_local_owner_aws_leak.json:aws-access-token:251 +d13e0fdfde8f5c6261c9e893461f32861e21e8f5:test_data/test_local_owner_aws_leak.json:aws-access-token:263 +d13e0fdfde8f5c6261c9e893461f32861e21e8f5:test_data/test_local_owner_aws_leak.json:aws-access-token:264 +d13e0fdfde8f5c6261c9e893461f32861e21e8f5:test_data/test_local_repo_five_files_at_commit.json:aws-access-token:3 +d13e0fdfde8f5c6261c9e893461f32861e21e8f5:test_data/test_local_repo_five_files_at_commit.json:aws-access-token:4 +d13e0fdfde8f5c6261c9e893461f32861e21e8f5:test_data/test_local_repo_five_files_at_commit.json:aws-access-token:16 +d13e0fdfde8f5c6261c9e893461f32861e21e8f5:test_data/test_local_repo_five_files_at_commit.json:aws-access-token:17 +2474c39311296b4d48c284b1ce82e0c270854bde:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:3 +2474c39311296b4d48c284b1ce82e0c270854bde:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:4 +2474c39311296b4d48c284b1ce82e0c270854bde:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:16 +2474c39311296b4d48c284b1ce82e0c270854bde:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:17 +2474c39311296b4d48c284b1ce82e0c270854bde:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:42 +2474c39311296b4d48c284b1ce82e0c270854bde:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:43 +2474c39311296b4d48c284b1ce82e0c270854bde:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:68 +2474c39311296b4d48c284b1ce82e0c270854bde:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:69 +2474c39311296b4d48c284b1ce82e0c270854bde:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:94 +2474c39311296b4d48c284b1ce82e0c270854bde:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:95 +d13e0fdfde8f5c6261c9e893461f32861e21e8f5:test_data/test_repos/test_repo_5/secrets.py:aws-access-token:1 +d13e0fdfde8f5c6261c9e893461f32861e21e8f5:test_data/test_repos/test_repo_5/secrets.py:aws-access-token:4 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:3 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:4 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:16 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:17 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:29 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:30 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:42 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_range.json:aws-access-token:43 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_to.json:aws-access-token:3 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_to.json:aws-access-token:4 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_to.json:aws-access-token:29 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_to.json:aws-access-token:30 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:3 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:4 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:16 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:17 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:29 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:30 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:42 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:43 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:55 +a0f72a4e3595ddb382a77804d2674d54b2b0e880:test_data/test_local_repo_two_leaks_commit_from.json:aws-access-token:56 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_repo_two_leaks.json:aws-access-token:68 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_repo_two_leaks.json:aws-access-token:69 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:81 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:82 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:107 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:108 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_repo_two_leaks.json:aws-access-token:94 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_repo_two_leaks.json:aws-access-token:95 +6e1c41b536a514e1d17768eb42ebf5d87e61247d:examples/simple_regex_and_whitelist_config.toml:aws-access-token:4 +6e1c41b536a514e1d17768eb42ebf5d87e61247d:examples/simple_regex_and_whitelist_config.toml:aws-access-token:12 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:211 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:212 +275232f8c8f3ae4936667602a06a46caa156580b:audit/util.go:aws-access-token:90 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:237 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:238 +275232f8c8f3ae4936667602a06a46caa156580b:test_data/test_configs/aws_key_aws_whitelisted.toml:aws-access-token:6 +275232f8c8f3ae4936667602a06a46caa156580b:test_data/test_regex_whitelist.json.got:aws-access-token:3 +275232f8c8f3ae4936667602a06a46caa156580b:test_data/test_regex_whitelist.json.got:aws-access-token:4 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:315 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:316 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:341 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:342 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:445 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:446 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:471 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:472 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:549 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:550 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:575 +4d04ea079fc4e7f46c9f751fccde8f18c33b9e13:test_data/test_local_owner_aws_leak.json:aws-access-token:576 +e446ba073804cf714316c5dc00d0ac41917b31f7:test_data/test_regex_entropy.json:aws-access-token:3 +e446ba073804cf714316c5dc00d0ac41917b31f7:test_data/test_regex_entropy.json:aws-access-token:4 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:241 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:242 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:255 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:256 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:269 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:270 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:283 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:284 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:297 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:298 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:311 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:312 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:325 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:326 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:339 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:340 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:353 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:354 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:367 +2ccd40677db07877be4a5919a16f6a180fc2a7e4:test_data/test_local_owner_aws_leak.json:aws-access-token:368 +b55d88dc151f7022901cda41a03d43e0e508f2b7:audit/audit_test.go:aws-access-token:207 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_entropy.json:aws-access-token:3 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_one_aws_leak.json:aws-access-token:3 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_one_aws_leak.json:aws-access-token:4 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_one_aws_leak_and_file_leak.json:aws-access-token:17 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_one_aws_leak_and_file_leak.json:aws-access-token:18 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_one_aws_leak_commit.json:aws-access-token:3 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_one_aws_leak_commit.json:aws-access-token:4 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_one_aws_leak_uncommitted.json:aws-access-token:3 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_one_aws_leak_uncommitted.json:aws-access-token:4 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:3 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:4 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:17 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:18 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:31 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:32 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:45 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:46 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:59 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:60 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:73 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:74 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:87 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:88 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:101 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:102 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_repos/test_repo_1/server.test.py:aws-access-token:5 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_two_leaks.json:aws-access-token:3 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_two_leaks.json:aws-access-token:4 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_two_leaks.json:aws-access-token:17 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_two_leaks.json:aws-access-token:18 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_two_leaks.json:aws-access-token:31 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_two_leaks.json:aws-access-token:32 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_two_leaks.json:aws-access-token:45 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_two_leaks.json:aws-access-token:46 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_two_leaks.json:aws-access-token:59 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_two_leaks.json:aws-access-token:60 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_two_leaks.json:aws-access-token:73 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_two_leaks.json:aws-access-token:74 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:3 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:4 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:17 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:18 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:31 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:32 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:45 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:46 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:59 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:60 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:73 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:74 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:87 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:88 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:101 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:102 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:115 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:116 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:129 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:130 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:143 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:144 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:157 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:158 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:171 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:172 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:185 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:186 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:199 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:200 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:213 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:214 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:227 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_owner_aws_leak.json:aws-access-token:228 +7bd55e33b504f76fc2aec27f4f479a5fb2606480:src/constants.go:private-key:32 +e79ffc6ae8f66931d687407e07bc0632fb262091:src/constants.go:private-key:23 +2b995fda446e03b7660c327fec41265f03199c71:config.go:private-key:28 +1f80d14f3f069118a2e578d83bbc8440495fc906:main.go:private-key:118 +fe977ea8577b739c34a0071ba34e98cf6b6fad38:checks_test.go:aws-access-token:52 +4b4fa00b69a9a2e5f7619b1ff1dd6d160f588aec:README.md:aws-access-token:25 +4b4fa00b69a9a2e5f7619b1ff1dd6d160f588aec:README.md:aws-access-token:27 +2186f2299990290e4e19cf3b28906e2ec369d01e:main.go:private-key:44 +9f694531b203663588c68c82dbbfb34d4f71863f:main.go:private-key:44 +1e250f1a14eab6f457d61f5d7650b47c6ff43334:checks_test.go:aws-access-token:19 +1e250f1a14eab6f457d61f5d7650b47c6ff43334:config.yml:private-key:2 +bc26e979c5911cf647c1bede0b3700ebaaa454c8:checks_test.go:aws-access-token:36 +8f352bd840f028b481dc725b77d2f4904b77913b:checks_test.go:aws-access-token:34 +ec2fc9d6cb0954fb3b57201cf6133c48d8ca0d29:checks_test.go:aws-access-token:37 +06c9e824d5985c8e8789321ae70de7ace3b093dc:main.go:aws-access-token:15 +6d801ed61cf4d75d176cbf9062ec6ad706c9d1eb:detect/detect_test.go:private-key:567 +6d801ed61cf4d75d176cbf9062ec6ad706c9d1eb:testdata/repos/symlinks/source_file/id_ed25519:private-key:1 +acce01f2338434a78f6a4a06a097b0fd23280484:README.md:aws-access-token:218 +README.md:aws-access-token:204 +README.md:aws-access-token:205 +README.md:aws-access-token:244 +cmd/generate/config/rules/privatekey.go:private-key:19 +cmd/generate/config/rules/generic.go:clojars-api-token:43 +cmd/generate/config/rules/generic.go:generic-api-key:45 +cmd/generate/config/rules/generic.go:generic-api-key:46 +cmd/generate/config/rules/sidekiq.go:sidekiq-secret:22 +cmd/generate/config/rules/sidekiq.go:sidekiq-secret:23 +cmd/generate/config/rules/sidekiq.go:sidekiq-secret:24 +cmd/generate/config/rules/sidekiq.go:sidekiq-secret:28 +cmd/generate/config/rules/sidekiq.go:sidekiq-secret:29 +cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:46 +cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:48 +cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:50 +cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:52 +cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:54 +cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:55 +cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:56 +cmd/generate/config/rules/sidekiq.go:sidekiq-sensitive-url:57 +config/config_test.go:aws-access-token:31 +detect/detect_test.go:sidekiq-secret:120 +detect/detect_test.go:sidekiq-secret:126 +detect/detect_test.go:sidekiq-secret:142 +detect/detect_test.go:aws-access-token:50 +detect/detect_test.go:aws-access-token:60 +detect/detect_test.go:aws-access-token:61 +detect/detect_test.go:aws-access-token:98 +detect/detect_test.go:aws-access-token:104 +detect/detect_test.go:aws-access-token:105 +detect/detect_test.go:aws-access-token:186 +detect/detect_test.go:aws-access-token:194 +detect/detect_test.go:aws-access-token:202 +detect/detect_test.go:aws-access-token:288 +detect/detect_test.go:aws-access-token:296 +detect/detect_test.go:aws-access-token:359 +detect/detect_test.go:aws-access-token:360 +detect/detect_test.go:aws-access-token:378 +detect/detect_test.go:aws-access-token:379 +detect/detect_test.go:aws-access-token:404 +detect/detect_test.go:aws-access-token:405 +detect/detect_test.go:aws-access-token:480 +detect/detect_test.go:aws-access-token:481 +detect/detect_test.go:aws-access-token:499 +detect/detect_test.go:aws-access-token:500 +detect/detect_test.go:sidekiq-sensitive-url:164 +detect/detect_test.go:sidekiq-sensitive-url:170 +detect/detect_test.go:pypi-upload-token:76 +detect/detect_test.go:pypi-upload-token:82 +detect/detect_test.go:pypi-upload-token:83 +detect/detect_test.go:discord-api-token:211 +detect/detect_test.go:discord-api-token:233 +detect/detect_test.go:discord-api-token:241 +detect/detect_test.go:discord-api-token:263 +detect/detect_test.go:discord-api-token:279 +testdata/config/allow_aws_re.toml:aws-access-token:9 +testdata/config/allow_global_aws_re.toml:aws-access-token:8 +testdata/expected/git/small-branch-foo.txt:aws-access-token:15 +testdata/expected/git/small.txt:aws-access-token:15 +testdata/expected/git/small.txt:aws-access-token:44 +testdata/repos/nogit/main.go:aws-access-token:20 +testdata/repos/nogit/api.go:aws-access-token:20 +testdata/repos/small/api/ignoreCommit.go:aws-access-token:20 +testdata/repos/small/api/ignoreGlobal.go:aws-access-token:20 +3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:513 +3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:492 +3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:414 +3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:388 +3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:366 +3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:discord-api-token:255 +3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:discord-api-token:224 +3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:sidekiq-sensitive-url:177 +3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:sidekiq-secret:154 +3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:sidekiq-secret:130 +3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:107 +3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:pypi-upload-token:84 +3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:62 +96eed6aa0f507fe0c21bb46bec32637dc4cb1a9f:detect/detect_test.go:aws-access-token:62 +96eed6aa0f507fe0c21bb46bec32637dc4cb1a9f:detect/detect_test.go:pypi-upload-token:84 +96eed6aa0f507fe0c21bb46bec32637dc4cb1a9f:detect/detect_test.go:aws-access-token:107 +96eed6aa0f507fe0c21bb46bec32637dc4cb1a9f:detect/detect_test.go:sidekiq-secret:130 +96eed6aa0f507fe0c21bb46bec32637dc4cb1a9f:detect/detect_test.go:sidekiq-secret:154 +96eed6aa0f507fe0c21bb46bec32637dc4cb1a9f:detect/detect_test.go:sidekiq-sensitive-url:177 +96eed6aa0f507fe0c21bb46bec32637dc4cb1a9f:detect/detect_test.go:discord-api-token:224 +96eed6aa0f507fe0c21bb46bec32637dc4cb1a9f:detect/detect_test.go:discord-api-token:255 +96eed6aa0f507fe0c21bb46bec32637dc4cb1a9f:detect/detect_test.go:aws-access-token:366 +96eed6aa0f507fe0c21bb46bec32637dc4cb1a9f:detect/detect_test.go:aws-access-token:388 +96eed6aa0f507fe0c21bb46bec32637dc4cb1a9f:detect/detect_test.go:aws-access-token:414 +96eed6aa0f507fe0c21bb46bec32637dc4cb1a9f:detect/detect_test.go:aws-access-token:492 +96eed6aa0f507fe0c21bb46bec32637dc4cb1a9f:detect/detect_test.go:aws-access-token:513 +bfbc39eb185a217ead81a7174026f1c55f963660:cmd/generate/config/rules/jwt.go:jwt:17 +bfbc39eb185a217ead81a7174026f1c55f963660:cmd/generate/config/rules/jwt.go:jwt:19 +acce01f2338434a78f6a4a06a097b0fd23280484:README.md:aws-access-token:220 +99d620c719020fa24baa831299f0c964da79875e:detect/detect_test.go:aws-access-token:514 +111c8b47b65627db117bd20be85ff0aee8961eb9:detect/detect_test.go:private-key:567 +99d620c719020fa24baa831299f0c964da79875e:detect/detect_test.go:aws-access-token:512 +99d620c719020fa24baa831299f0c964da79875e:detect/detect_test.go:aws-access-token:513 +69f161c0a0cc0f7858f7d0609d656f4edcedd84c:detect/detect_test.go:discord-api-token:326 +9701bf1724d822c0d5bbb7627535dd639a37bf56:testdata/repos/staged/api/api.go:aws-access-token:7 +9701bf1724d822c0d5bbb7627535dd639a37bf56:detect/detect_test.go:aws-access-token:505 +9701bf1724d822c0d5bbb7627535dd639a37bf56:detect/detect_test.go:aws-access-token:506 +9701bf1724d822c0d5bbb7627535dd639a37bf56:detect/detect_test.go:aws-access-token:507 +9701bf1724d822c0d5bbb7627535dd639a37bf56:testdata/repos/staged/api/api.go:aws-access-token:6 +9701bf1724d822c0d5bbb7627535dd639a37bf56:testdata/repos/staged/api/api.go:aws-access-token:7 +91ff8f9b7020a16dac9b8b9ef98560ea1cb464f8:testdata/repos/small/api/ignoreCommit.go:aws-access-token:20 +91ff8f9b7020a16dac9b8b9ef98560ea1cb464f8:testdata/repos/small/api/ignoreGlobal.go:aws-access-token:20 +91ff8f9b7020a16dac9b8b9ef98560ea1cb464f8:testdata/repos/nogit/api.go:aws-access-token:20 +adf617b3b4628e1160fa3d135b4c3dfd45c05e15:testdata/repos/nogit/api.go:aws-access-token:20 +adf617b3b4628e1160fa3d135b4c3dfd45c05e15:testdata/repos/small/api/ignoreGlobal.go:aws-access-token:20 +adf617b3b4628e1160fa3d135b4c3dfd45c05e15:testdata/repos/small/api/ignoreCommit.go:aws-access-token:20 +f361c5ef71853923277e3f284890083bc7825205:detect/reader_test.go:aws-access-token:12 + +8cfa6b2e43544895f52d2e37535ccfe29a6cdfd3:cmd/generate/config/rules/infracost.go:infracost-api-token:24 +8cfa6b2e43544895f52d2e37535ccfe29a6cdfd3:cmd/generate/config/rules/infracost.go:generic-api-key:29 +8cfa6b2e43544895f52d2e37535ccfe29a6cdfd3:cmd/generate/config/rules/discord.go:discord-client-secret:64 +8cfa6b2e43544895f52d2e37535ccfe29a6cdfd3:cmd/generate/config/rules/grafana.go:grafana-cloud-api-token:42 +8cfa6b2e43544895f52d2e37535ccfe29a6cdfd3:cmd/generate/config/rules/grafana.go:grafana-service-account-token:80 +9152eaa57a91d7249bd0b3c3a86b9e837072a828:detect/reader_test.go:aws-access-token:12 +9152eaa57a91d7249bd0b3c3a86b9e837072a828:cmd/generate/config/rules/aws.go:aws-access-token:42 +93acc6e82adb46ef442fcb44bd9fd83489690ca3:cmd/generate/config/rules/1password.go:1password-service-account-token:25 +93acc6e82adb46ef442fcb44bd9fd83489690ca3:cmd/generate/config/rules/1password.go:1password-service-account-token:27 +93acc6e82adb46ef442fcb44bd9fd83489690ca3:cmd/generate/config/rules/1password.go:1password-service-account-token:28 +93acc6e82adb46ef442fcb44bd9fd83489690ca3:cmd/generate/config/rules/1password.go:generic-api-key:37 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:openai-api-key:125 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:curl-auth-header:100 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:curl-auth-header:102 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:curl-auth-header:108 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:curl-auth-header:112 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:curl-auth-header:121 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:curl-auth-header:122 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:curl-auth-header:123 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:curl-auth-header:126 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:curl-auth-header:131 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:curl-auth-header:134 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:curl-auth-header:137 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:curl-auth-header:137 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:generic-api-key:139 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:jwt:134 +83a57244cdd0bfe1634326ab04a29b9b139bf158:cmd/generate/config/rules/curl.go:jwt:170 +cf5334fd61d16fb4af1362856ebfb98397c5d4b3:cmd/generate/config/rules/curl.go:curl-auth-user:32 +cf5334fd61d16fb4af1362856ebfb98397c5d4b3:cmd/generate/config/rules/curl.go:curl-auth-user:33 +cf5334fd61d16fb4af1362856ebfb98397c5d4b3:cmd/generate/config/rules/curl.go:curl-auth-user:40 +cf5334fd61d16fb4af1362856ebfb98397c5d4b3:cmd/generate/config/rules/curl.go:curl-auth-user:42 +cf5334fd61d16fb4af1362856ebfb98397c5d4b3:cmd/generate/config/rules/curl.go:curl-auth-user:43 +cf5334fd61d16fb4af1362856ebfb98397c5d4b3:cmd/generate/config/rules/curl.go:curl-auth-user:44 +cf5334fd61d16fb4af1362856ebfb98397c5d4b3:cmd/generate/config/rules/curl.go:curl-auth-user:45 +aabe3815394d24d5b8198ba6068a99e0ba7601b1:config/config_test.go:aws-access-token:72 +b9f7fdaacb9e3799a9b3e4f7f6181963b8931b73:config/config_test.go:aws-access-token:67 +3ef444d24677c863bb724fa406dd12464b7f2362:config/config_test.go:aws-access-token:39 +8fb39ba8dc9d872d57cc3151b3750e8234b9b980:cmd/generate/config/rules/azure.go:azure-ad-client-secret:33 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/decoder_test.go:generic-api-key:22 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/decoder_test.go:generic-api-key:32 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/decoder_test.go:generic-api-key:37 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/decoder_test.go:generic-api-key:54 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/decoder_test.go:generic-api-key:59 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/decoder_test.go:generic-api-key:64 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/detect_test.go:private-key:23 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/detect_test.go:generic-api-key:39 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/detect_test.go:jwt:32 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/detect_test.go:private-key:365 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/detect_test.go:private-key:366 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/detect_test.go:private-key:368 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/detect_test.go:private-key:393 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/detect_test.go:private-key:394 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/detect_test.go:aws-access-token:407 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/detect_test.go:aws-access-token:408 +2278a2a97e422c8075207d17195b29e7f4ff15e1:detect/detect_test.go:generic-api-key:452 +f8dcd838da26f591c70acbbc3dea8d8b4d1649c7:detect/detect_test.go:generic-api-key:616 +f8dcd838da26f591c70acbbc3dea8d8b4d1649c7:detect/detect_test.go:generic-api-key:618 +f8dcd838da26f591c70acbbc3dea8d8b4d1649c7:testdata/repos/nogit/.env.prod:generic-api-key:4 +032c47e7e1ebfb745d3b66ef448697d4cbf6a406:detect/detect_test.go:generic-api-key:38 +032c47e7e1ebfb745d3b66ef448697d4cbf6a406:detect/detect_test.go:generic-api-key:451 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/detect_test.go:private-key:22 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/detect_test.go:jwt:31 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/decoder_test.go:generic-api-key:22 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/decoder_test.go:generic-api-key:32 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/decoder_test.go:generic-api-key:37 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/decoder_test.go:generic-api-key:54 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/decoder_test.go:generic-api-key:59 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/decoder_test.go:generic-api-key:64 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/detect_test.go:private-key:359 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/detect_test.go:private-key:360 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/detect_test.go:private-key:362 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/detect_test.go:private-key:387 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/detect_test.go:private-key:388 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/detect_test.go:aws-access-token:401 +6c5e01c144b3d5e48422ab66a353f4cbdc14b24a:detect/detect_test.go:aws-access-token:402 +78f7d3f5dfba03f816418f987ff3a78643c1eda7:cmd/generate/config/rules/flyio.go:flyio-access-token:26 +78f7d3f5dfba03f816418f987ff3a78643c1eda7:cmd/generate/config/rules/flyio.go:flyio-access-token:30 +78f7d3f5dfba03f816418f987ff3a78643c1eda7:cmd/generate/config/rules/flyio.go:flyio-access-token:34 +78f7d3f5dfba03f816418f987ff3a78643c1eda7:cmd/generate/config/rules/flyio.go:flyio-access-token:37 +78f7d3f5dfba03f816418f987ff3a78643c1eda7:cmd/generate/config/rules/flyio.go:flyio-access-token:38 +3698060a2bcf926b8ae459270d9bcb7b69339104:cmd/generate/config/rules/kubernetes.go:generic-api-key:56 +3698060a2bcf926b8ae459270d9bcb7b69339104:cmd/generate/config/rules/kubernetes.go:generic-api-key:68 +3698060a2bcf926b8ae459270d9bcb7b69339104:cmd/generate/config/rules/kubernetes.go:generic-api-key:79 +3698060a2bcf926b8ae459270d9bcb7b69339104:cmd/generate/config/rules/kubernetes.go:generic-api-key:147 +3698060a2bcf926b8ae459270d9bcb7b69339104:cmd/generate/config/rules/kubernetes.go:generic-api-key:153 +3698060a2bcf926b8ae459270d9bcb7b69339104:cmd/generate/config/rules/kubernetes.go:generic-api-key:162 +5740eddd75b2a1149374438afe80c7cf2535b35f:README.md:aws-access-token:221 +5740eddd75b2a1149374438afe80c7cf2535b35f:README.md:aws-access-token:223 +5740eddd75b2a1149374438afe80c7cf2535b35f:README.md:aws-access-token:259 +5740eddd75b2a1149374438afe80c7cf2535b35f:cmd/generate/config/rules/generic.go:clojars-api-token:42 +372d99cfdb92a1e177badbfc1cd65bd692df9025:cmd/generate/config/rules/hashicorp.go:hashicorp-tf-api-token:24 +372d99cfdb92a1e177badbfc1cd65bd692df9025:cmd/generate/config/rules/vault.go:vault-service-token:22 +372d99cfdb92a1e177badbfc1cd65bd692df9025:cmd/generate/config/rules/vault.go:vault-service-token:25 +3be7faa1ffc80f1aa071192a09141d5e829e1af3:cmd/generate/config/rules/vault.go:vault-service-token:22 +3be7faa1ffc80f1aa071192a09141d5e829e1af3:cmd/generate/config/rules/hashicorp.go:hashicorp-tf-api-token:24 +3be7faa1ffc80f1aa071192a09141d5e829e1af3:cmd/generate/config/rules/vault.go:vault-service-token:25 +840c3cb24dcf14bf3332793f0702e75085ff5de2:detect/verify_test.go:generic-api-key:39 +840c3cb24dcf14bf3332793f0702e75085ff5de2:detect/verify_test.go:generic-api-key:47 +840c3cb24dcf14bf3332793f0702e75085ff5de2:detect/verify_test.go:generic-api-key:88 +c1345e1b2b63260d860637530ba4d17571d547d6:cmd/generate/config/rules/openshift.go:openshift-user-token:27 +c1345e1b2b63260d860637530ba4d17571d547d6:cmd/generate/config/rules/openshift.go:openshift-user-token:28 +821b2323940b1792d9001d0812a2a4f7480a69c0:cmd/generate/config/rules/cloudflare.go:cloudflare-origin-ca-key:20 +821b2323940b1792d9001d0812a2a4f7480a69c0:cmd/generate/config/rules/cloudflare.go:cloudflare-origin-ca-key:21 +e265bfbbc3b208a23f0e46dd4a96795817068b77:detect/validate_test.go:generic-api-key:93 +9d06c40017ba6710032e9bc08ebbc056175629cf:detect/validate_test.go:github-pat:33 +9d06c40017ba6710032e9bc08ebbc056175629cf:detect/validate_test.go:github-pat:36 +9d06c40017ba6710032e9bc08ebbc056175629cf:detect/validate_test.go:generic-api-key:43 +9d06c40017ba6710032e9bc08ebbc056175629cf:detect/validate_test.go:generic-api-key:49 +9d06c40017ba6710032e9bc08ebbc056175629cf:detect/validate_test.go:generic-api-key:57 +e9135cf0b598738b5fc63ca004db8ab30845393d:cmd/generate/config/rules/jwt.go:jwt:48 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-organization-api-token:85 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-organization-api-token:86 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-organization-api-token:87 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-organization-api-token:88 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-organization-api-token:89 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-organization-api-token:90 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-organization-api-token:91 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-organization-api-token:93 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-organization-api-token:95 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-organization-api-token:96 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-organization-api-token:97 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:30 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:31 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:32 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:33 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:34 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:35 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:36 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:37 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:38 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:39 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:40 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:41 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:42 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:43 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:45 +9fb36b242d75aac1a2bf885724dfd9886db08ea7:cmd/generate/config/rules/huggingface.go:huggingface-access-token:46 +463d24618fa42fc7629dc30c9744ebe36c5df1ab:cmd/generate/config/rules/slack.go:slack-user-token:66 +463d24618fa42fc7629dc30c9744ebe36c5df1ab:cmd/generate/config/rules/slack.go:slack-config-access-token:115 +463d24618fa42fc7629dc30c9744ebe36c5df1ab:cmd/generate/config/rules/slack.go:slack-legacy-workspace-token:210 +025908808f2ad1ce244f9806b8dd593bd9afbab0:detect/detect_test.go:discord-api-token:326 +e002920355ac91770a329cfa69d6359bd665ba66:cmd/generate/config/rules/privatekey.go:private-key:26 +e890a8e8098ee8021d25a429315a6c153126db22:cmd/generate/config/rules/jwt.go:jwt:17 +9326f35380636bcbe61e94b0584d1618c4b5c2c2:detect/detect_test.go:pypi-upload-token:27 +9326f35380636bcbe61e94b0584d1618c4b5c2c2:detect/detect_test.go:pypi-upload-token:32 +9326f35380636bcbe61e94b0584d1618c4b5c2c2:detect/detect_test.go:pypi-upload-token:33 +3a3e13c3b5f85b0116cf2a0cd92529baf22d0ac9:testdata/repos/with_square_and_google/env:gcp-api-key:3 +45c898c5ea56ee503a048c1bac1404cf63855edc:test_data/test_repos/test_dir_2/env:gcp-api-key:3 +45c898c5ea56ee503a048c1bac1404cf63855edc:test_data/test_dir_one_google_leak_and_square_leak.json:gcp-api-key:20 +45c898c5ea56ee503a048c1bac1404cf63855edc:test_data/test_dir_one_google_leak_and_square_leak.json:gcp-api-key:22 +f6460a7365479a48581e0641b40147914542bb24:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:16 +f6460a7365479a48581e0641b40147914542bb24:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:17 +f6460a7365479a48581e0641b40147914542bb24:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:42 +f6460a7365479a48581e0641b40147914542bb24:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:43 +f6460a7365479a48581e0641b40147914542bb24:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:68 +f6460a7365479a48581e0641b40147914542bb24:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:69 +f6460a7365479a48581e0641b40147914542bb24:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:94 +f6460a7365479a48581e0641b40147914542bb24:test_data/test_local_owner_aws_leak_depth_2.json:aws-access-token:95 +7bd55e33b504f76fc2aec27f4f479a5fb2606480:src/constants.go:private-key:42 +e79ffc6ae8f66931d687407e07bc0632fb262091:src/constants.go:private-key:29 +2b995fda446e03b7660c327fec41265f03199c71:config.go:private-key:34 +ccef3680d5552443c999345567ea60909c863c0a:checks_test.go:aws-access-token:52 +1cecd5e0908ab581f217ebb2cb58829204530c79:README.md:aws-access-token:25 +1cecd5e0908ab581f217ebb2cb58829204530c79:README.md:aws-access-token:27 +e56870aecf8ac2733affc82e1ac25e3be9092db3:cmd/generate/config/rules/curl.go:generic-api-key:116 +239e6323639b18ba8a1081f17788a658da075dd7:cmd/generate/config/rules/sumologic.go:sumologic-access-token:72 +239e6323639b18ba8a1081f17788a658da075dd7:cmd/generate/config/rules/sumologic.go:sumologic-access-id:40 +239e6323639b18ba8a1081f17788a658da075dd7:cmd/generate/config/rules/readme.go:readme-api-token:26 +239e6323639b18ba8a1081f17788a658da075dd7:cmd/generate/config/rules/prefect.go:prefect-api-token:26 +239e6323639b18ba8a1081f17788a658da075dd7:cmd/generate/config/rules/databricks.go:databricks-api-token:25 +239e6323639b18ba8a1081f17788a658da075dd7:cmd/generate/config/rules/databricks.go:generic-api-key:22 +c24a3a654b18d689732ec3199f0a19a254505b04:detect/detect_test.go:generic-api-key:52 +c08cb1dc9aee87186dba4eebbc13b8aaa2351f0c:detect/detect_test.go:generic-api-key:52 +ee0da5c7fa5f3c9a7f1617e162497199cac3537f:README.md:generic-api-key:480 +782f3104786efdce0f809bce8a9ff31f2fa1c9ed:README.md:generic-api-key:481 diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..1bd2cb4 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,86 @@ +version: '2' +linters: + default: none + # It might be worth going through some of the disabled linters and enabling + # them and fixing the items they call out e.g.: cyclop, prealloc, + # paralleltest, prealloc, errcheck, dupl, unused, testifylint, gosec, + # gocritic, perfsprint, exptostd, intrange, perfsprint (maybe others?) + disable: + - cyclop + - depguard + - dupl + - err113 + - errcheck + - exhaustive + - exhaustruct + - exptostd + - forbidigo + - funcorder + - funlen + - gochecknoglobals + - gochecknoinits + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - godox + - gosec + - gosmopolitan + - intrange + - lll + - maintidx + - mnd + - musttag + - nestif + - nilerr + - nlreturn + - nonamedreturns + - paralleltest + - perfsprint + - prealloc + - predeclared + - tagliatelle + - testifylint + - testpackage + - tparallel + - unparam + - unused + - varnamelen + - wastedassign + - whitespace + - wrapcheck + - wsl + - zerologlint # doesn't seem to catch gitleaks/v8/logging mistakes + enable: + - inamedparam + - misspell + - revive + - misspell + - inamedparam + - exhaustruct + - inamedparam + - misspell + - nonamedreturns + - staticcheck + - unconvert + exclusions: + rules: + - linters: + - staticcheck + source: 'detector\.Detect\w+\(|sources\.DirectoryTargets\(|detect\.(?:Fragment|RemoteInfo)' + - linters: + - misspell + source: '"(?:addres|busines|clas)",' + settings: + staticcheck: + checks: + - all + - '-QF1001' + - '-ST1000' + - '-ST1003' + - '-ST1018' + - '-ST1020' + - '-ST1021' + revive: + severity: error diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..573d4c9 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,31 @@ +project_name: gitleaks + +builds: + - main: main.go + binary: gitleaks + goos: + - darwin + - linux + - windows + goarch: + - amd64 + - "386" + - arm + - arm64 + goarm: + - "6" + - "7" + tags: + - gore2regex + ldflags: + - -s -w -X=github.com/zricethezav/gitleaks/v8/version.Version={{.Version}} +archives: + - builds: [gitleaks] + format_overrides: + - goos: windows + format: zip + replacements: + amd64: x64 + 386: x32 +release: + prerelease: true diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..66cbc46 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,17 @@ +- id: gitleaks + name: Detect hardcoded secrets + description: Detect hardcoded secrets using Gitleaks + entry: gitleaks git --pre-commit --redact --staged --verbose + language: golang + pass_filenames: false +- id: gitleaks-docker + name: Detect hardcoded secrets + description: Detect hardcoded secrets using Gitleaks + entry: zricethezav/gitleaks git --pre-commit --redact --staged --verbose + language: docker_image + pass_filenames: false +- id: gitleaks-system + name: Detect hardcoded secrets + description: Detect hardcoded secrets using Gitleaks + entry: gitleaks git --pre-commit --redact --staged --verbose + language: system diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0833112 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,102 @@ +# Contribution guidelines + +## General + +### Issues + +If you have a feature or bug fix you would like to contribute please check if +there are any open issues describing your proposed addition. If there are open +issues, make a comment stating you are working on fixing or implementing said +issue. If not, then please open an issue describing your addition. Make sure to +link your PR to an issue. + +### Pull Requests + +Fill out the template as best you can. Make sure your tests pass. If you see a +PR that isn't one you opened and want it introduced in the next release, +give it a :thumbsup: on the PR description. + +## Adding new Gitleaks rules + +If you want to add a new rule to the [default Gitleaks configuration](https://github.com/zricethezav/gitleaks/blob/master/config/gitleaks.toml) then follow these steps. + +1. Create a `cmd/generate/config/rules/{provider}.go` file. + This file is used to generate a new Gitleaks rule. + Let's look at `beamer.go` for example. Comments have been added for context. + + ```golang + func Beamer() *config.Rule { + // Define Rule + r := config.Rule{ + // Human readable description of the rule + Description: "Beamer API token", + + // Unique ID for the rule + RuleID: "beamer-api-token", + + // Regex capture group for the actual secret + + + + // Regex used for detecting secrets. See regex section below for more details + Regex: GenerateSemiGenericRegex([]string{"beamer"}, `b_[a-z0-9=_\-]{44}`, true) + + // Keywords used for string matching on fragments (think of this as a prefilter) + Keywords: []string{"beamer"}, + } + + // validate + tps := []string{ + generateSampleSecret("beamer", "b_"+secrets.NewSecret(alphaNumericExtended("44"))), + } + fps := []string{ + `R21A-A-V010SP13RC181024R16900-CN-B_250K-Release-OTA-97B6C6C59241976086FABDC41472150C.bfu`, + } + return validate(r, tps, fps) + } + ``` + + Feel free to use this example as a template when writing new rules. + This file should be fairly self-explanatory except for a few items; + regex and secret generation. To help with maintence, _most_ rules should + be uniform. The functions, + [`GenerateSemiGenericRegex`](https://github.com/zricethezav/gitleaks/blob/master/cmd/generate/config/rules/rule.go#L31) and [`GenerateUniqueTokenRegex`](https://github.com/zricethezav/gitleaks/blob/master/cmd/generate/config/rules/rule.go#L44) will generate rules + that follow defined patterns. + + The function signatures look like this: + + ```golang + func GenerateSemiGenericRegex(identifiers []string, secretRegex string, isCaseInsensitive bool) *regexp.Regexp + + func GenerateUniqueTokenRegex(secretRegex string, isCaseInsensitive bool) *regexp.Regexp + ``` + + `GenerateSemiGenericRegex` accepts a list of identifiers, a regex, and a boolean indicating whether the pattern should be case-insensitive. + The list of identifiers _should_ match the list of `Keywords` in the rule + definition above. Both `identifiers` in the `GenerateSemiGenericRegex` + function _and_ `Keywords` act as filters for Gitleaks telling the program + "_at least one of these strings must be present to be considered a leak_" + + `GenerateUniqueTokenRegex` just accepts a regex and a boolean indicating whether the pattern should be case-insensitive. If you are writing a rule for a + token that is unique enough not to require an identifier then you can use + this function. For example, Pulumi's API Token has the prefix `pul-` which is + unique enough to use `GenerateUniqueTokenRegex`. But something like Beamer's API + token that has a `b_` prefix is not unique enough to use `GenerateUniqueTokenRegex`, + so instead we use `GenerateSemiGenericRegex` and require a `beamer` + identifier is part of the rule. + If a token's prefix has more than `3` characters then you could + probably get away with using `GenerateUniqueTokenRegex`. + + Last thing you'll want to hit before we move on from this file is the + validation part. You can use `generateSampleSecret` to create a secret for the + true positives (`tps` in the example above) used in `validate`. + +1. Update `cmd/generate/config/main.go`. Extend `configRules` slice with + the `rules.Beamer(),` in `main()`. Try and keep + this alphabetically pretty please. + +1. Run `make config/gitleaks.toml` + +1. Check out your new rules in `config/gitleaks.toml` and see if everything looks good. + +1. Open a PR diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9a9f1fa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.24 AS build +WORKDIR /go/src/github.com/zricethezav/gitleaks +COPY . . +RUN VERSION=$(git describe --tags --abbrev=0) && \ +CGO_ENABLED=0 go build -o bin/gitleaks -ldflags "-X=github.com/zricethezav/gitleaks/v8/version.Version=${VERSION}" + +FROM alpine:3.22 +RUN apk add --no-cache bash git openssh-client +COPY --from=build /go/src/github.com/zricethezav/gitleaks/bin/* /usr/bin/ + +RUN git config --global --add safe.directory '*' + +ENTRYPOINT ["gitleaks"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3c270b3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Zachary Rice + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7c7112c --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +.PHONY: test test-cover failfast profile clean format build + +PKG=github.com/zricethezav/gitleaks +VERSION := `git fetch --tags && git tag | sort -V | tail -1` +LDFLAGS=-ldflags "-X=github.com/zricethezav/gitleaks/v8/version.Version=$(VERSION)" +COVER=--cover --coverprofile=cover.out + +test-cover: + go test -v ./... --race $(COVER) $(PKG) + go tool cover -html=cover.out + +format: + go fmt ./... + +test: config/gitleaks.toml format + go test -v ./... --race $(PKG) + +failfast: format + go test -failfast ./... + +build: config/gitleaks.toml format + go mod tidy + go build $(LDFLAGS) + +lint: + golangci-lint run + +clean: + rm -rf profile + find . -type f -name '*.got.*' -delete + find . -type f -name '*.out' -delete + +profile: build + ./scripts/profile.sh './gitleaks' '.' + +config/gitleaks.toml: $(wildcard cmd/generate/config/**/*) + go generate ./... diff --git a/README.md b/README.md new file mode 100644 index 0000000..6bdd5d9 --- /dev/null +++ b/README.md @@ -0,0 +1,630 @@ +# Gitleaks + +``` +┌─○───┐ +│ │╲ │ +│ │ ○ │ +│ ○ ░ │ +└─░───┘ +``` + +> [!WARNING] +> Gitleaks is feature complete. I'm not merging new features into Gitleaks. Future releases will be security patches only. I'm shifting my focus to [Betterleaks](https://github.com/betterleaks/betterleaks) + +[license]: ./LICENSE +[badge-license]: https://img.shields.io/github/license/gitleaks/gitleaks.svg +[go-docs-badge]: https://pkg.go.dev/badge/github.com/gitleaks/gitleaks/v8?status +[go-docs]: https://pkg.go.dev/github.com/zricethezav/gitleaks/v8 +[badge-build]: https://github.com/gitleaks/gitleaks/actions/workflows/test.yml/badge.svg +[build]: https://github.com/gitleaks/gitleaks/actions/workflows/test.yml +[go-report-card-badge]: https://goreportcard.com/badge/github.com/gitleaks/gitleaks/v8 +[go-report-card]: https://goreportcard.com/report/github.com/gitleaks/gitleaks/v8 +[dockerhub]: https://hub.docker.com/r/zricethezav/gitleaks +[dockerhub-badge]: https://img.shields.io/docker/pulls/zricethezav/gitleaks.svg +[gitleaks-action]: https://github.com/gitleaks/gitleaks-action +[gitleaks-badge]: https://img.shields.io/badge/protected%20by-gitleaks-blue + + +[![GitHub Action Test][badge-build]][build] +[![Docker Hub][dockerhub-badge]][dockerhub] +[![Gitleaks Action][gitleaks-badge]][gitleaks-action] +[![GoDoc][go-docs-badge]][go-docs] +[![GoReportCard][go-report-card-badge]][go-report-card] +[![License][badge-license]][license] + +Gitleaks is a tool for **detecting** secrets like passwords, API keys, and tokens in git repos, files, and whatever else you wanna throw at it via `stdin`. If you wanna learn more about how the detection engine works check out this blog: [Regex is (almost) all you need](https://lookingatcomputer.substack.com/p/regex-is-almost-all-you-need). + +``` +➜ ~/code(master) gitleaks git -v + + ○ + │╲ + │ ○ + ○ ░ + ░ gitleaks + + +Finding: "export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef", +Secret: cafebabe:deadbeef +RuleID: sidekiq-secret +Entropy: 2.609850 +File: cmd/generate/config/rules/sidekiq.go +Line: 23 +Commit: cd5226711335c68be1e720b318b7bc3135a30eb2 +Author: John +Email: john@users.noreply.github.com +Date: 2022-08-03T12:31:40Z +Fingerprint: cd5226711335c68be1e720b318b7bc3135a30eb2:cmd/generate/config/rules/sidekiq.go:sidekiq-secret:23 +``` + + +## 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/gitleaks/gitleaks/releases). In addition, Gitleaks can be implemented as a pre-commit hook directly in your repo or as a GitHub action using [Gitleaks-Action](https://github.com/gitleaks/gitleaks-action). + +### Installing + +```bash +# MacOS +brew install gitleaks + +# Docker (DockerHub) +docker pull zricethezav/gitleaks:latest +docker run -v ${path_to_host_folder_to_scan}:/path zricethezav/gitleaks:latest [COMMAND] [OPTIONS] [SOURCE_PATH] + +# Docker (ghcr.io) +docker pull ghcr.io/gitleaks/gitleaks:latest +docker run -v ${path_to_host_folder_to_scan}:/path ghcr.io/gitleaks/gitleaks:latest [COMMAND] [OPTIONS] [SOURCE_PATH] + +# From Source (make sure `go` is installed) +git clone https://github.com/gitleaks/gitleaks.git +cd gitleaks +make build +``` + +### Pre-Commit + +1. Install pre-commit from https://pre-commit.com/#install +2. Create a `.pre-commit-config.yaml` file at the root of your repository with the following content: + + ``` + repos: + - repo: https://github.com/gitleaks/gitleaks + rev: v8.24.2 + hooks: + - id: gitleaks + ``` + + for a [native execution of gitleaks](https://github.com/gitleaks/gitleaks/releases) or use the [`gitleaks-docker` pre-commit ID](https://github.com/gitleaks/gitleaks/blob/master/.pre-commit-hooks.yaml) for executing gitleaks using the [official Docker images](#docker) + +3. Auto-update the config to the latest repos' versions by executing `pre-commit autoupdate` +4. Install with `pre-commit install` +5. Now you're all set! + +``` +➜ git commit -m "this commit contains a secret" +Detect hardcoded secrets.................................................Failed +``` + +Note: to disable the gitleaks pre-commit hook you can prepend `SKIP=gitleaks` to the commit command +and it will skip running gitleaks + +``` +➜ SKIP=gitleaks git commit -m "skip gitleaks check" +Detect hardcoded secrets................................................Skipped +``` + +## Usage + +``` +Gitleaks scans code, past or present, for secrets + +Usage: + gitleaks [command] + +Available Commands: + completion Generate the autocompletion script for the specified shell + dir scan directories or files for secrets + git scan git repositories for secrets + help Help about any command + stdin detect secrets from stdin + version display gitleaks version + +Flags: + -b, --baseline-path string path to baseline with issues that can be ignored + -c, --config string config file path + order of precedence: + 1. --config/-c + 2. env var GITLEAKS_CONFIG + 3. env var GITLEAKS_CONFIG_TOML with the file content + 4. (target path)/.gitleaks.toml + If none of the four options are used, then gitleaks will use the default config + --diagnostics string enable diagnostics (http OR comma-separated list: cpu,mem,trace). cpu=CPU prof, mem=memory prof, trace=exec tracing, http=serve via net/http/pprof + --diagnostics-dir string directory to store diagnostics output files when not using http mode (defaults to current directory) + --enable-rule strings only enable specific rules by id + --exit-code int exit code when leaks have been encountered (default 1) + -i, --gitleaks-ignore-path string path to .gitleaksignore file or folder containing one (default ".") + -h, --help help for gitleaks + --ignore-gitleaks-allow ignore gitleaks:allow comments + -l, --log-level string log level (trace, debug, info, warn, error, fatal) (default "info") + --max-archive-depth int allow scanning into nested archives up to this depth (default "0", no archive traversal is done) + --max-decode-depth int allow recursive decoding up to this depth (default "0", no decoding is done) + --max-target-megabytes int files larger than this will be skipped + --no-banner suppress banner + --no-color turn off color for verbose output + --redact uint[=100] redact secrets from logs and stdout. To redact only parts of the secret just apply a percent value from 0..100. For example --redact=20 (default 100%) + -f, --report-format string output format (json, csv, junit, sarif, template) + -r, --report-path string report file + --report-template string template file used to generate the report (implies --report-format=template) + --timeout int set a timeout for gitleaks commands in seconds (default "0", no timeout is set) + -v, --verbose show verbose output from scan + --version version for gitleaks + +Use "gitleaks [command] --help" for more information about a command. +``` + +### Commands + +⚠️ v8.19.0 introduced a change that deprecated `detect` and `protect`. Those commands are still available but +are hidden in the `--help` menu. Take a look at this [gist](https://gist.github.com/zricethezav/b325bb93ebf41b9c0b0507acf12810d2) for easy command translations. +If you find v8.19.0 broke an existing command (`detect`/`protect`), please open an issue. + +There are three scanning modes: `git`, `dir`, and `stdin`. + +#### Git + +The `git` command lets you scan local git repos. Under the hood, gitleaks uses the `git log -p` command to scan patches. +You can configure the behavior of `git log -p` with the `log-opts` option. +For example, if you wanted to run gitleaks on a range of commits you could use the following +command: `gitleaks git -v --log-opts="--all commitA..commitB" path_to_repo`. See the [git log](https://git-scm.com/docs/git-log) documentation for more information. +If there is no target specified as a positional argument, then gitleaks will attempt to scan the current working directory as a git repo. + +#### Dir + +The `dir` (aliases include `files`, `directory`) command lets you scan directories and files. Example: `gitleaks dir -v path_to_directory_or_file`. +If there is no target specified as a positional argument, then gitleaks will scan the current working directory. + +#### Stdin + +You can also stream data to gitleaks with the `stdin` command. Example: `cat some_file | gitleaks -v stdin` + +### Creating a baseline + +When scanning large repositories or repositories with a long history, it can be convenient to use a baseline. When using a baseline, +gitleaks will ignore any old findings that are present in the baseline. A baseline can be any gitleaks report. To create a gitleaks report, run gitleaks with the `--report-path` parameter. + +``` +gitleaks git --report-path gitleaks-report.json # This will save the report in a file called gitleaks-report.json +``` + +Once as baseline is created it can be applied when running the detect command again: + +``` +gitleaks git --baseline-path gitleaks-report.json --report-path findings.json +``` + +After running the detect command with the --baseline-path parameter, report output (findings.json) will only contain new issues. + +## 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. + +## Load Configuration + +The order of precedence is: + +1. `--config/-c` option: + ```bash + gitleaks git --config /home/dev/customgitleaks.toml . + ``` +2. Environment variable `GITLEAKS_CONFIG` with the file path: + ```bash + export GITLEAKS_CONFIG="/home/dev/customgitleaks.toml" + gitleaks git . + ``` +3. Environment variable `GITLEAKS_CONFIG_TOML` with the file content: + ```bash + export GITLEAKS_CONFIG_TOML=`cat customgitleaks.toml` + gitleaks git . + ``` +4. A `.gitleaks.toml` file within the target path: + ```bash + gitleaks git . + ``` + +If none of the four options are used, then gitleaks will use the default config. + +## Configuration + +Gitleaks offers a configuration format you can follow to write your own secret detection rules: + +```toml +# Title for the gitleaks configuration file. +title = "Custom Gitleaks configuration" + +# You have basically two options for your custom configuration: +# +# 1. define your own configuration, default rules do not apply +# +# use e.g., the default configuration as starting point: +# https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml +# +# 2. extend a configuration, the rules are overwritten or extended +# +# When you extend a configuration the extended rules take precedence over the +# default rules. I.e., if there are duplicate rules in both the extended +# configuration and the default configuration the extended rules or +# attributes of them will override the default rules. +# Another thing to know with extending configurations is you can chain +# together multiple configuration files to a depth of 2. Allowlist arrays are +# appended and can contain duplicates. + +# useDefault and path can NOT be used at the same time. Choose one. +[extend] +# useDefault will extend the default gitleaks config built in to the binary +# the latest version is located at: +# https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml +useDefault = true +# or you can provide a path to a configuration to extend from. +# The path is relative to where gitleaks was invoked, +# not the location of the base config. +# path = "common_config.toml" +# If there are any rules you don't want to inherit, they can be specified here. +disabledRules = [ "generic-api-key"] + +# An array of tables that contain information that define instructions +# on how to detect secrets +[[rules]] +# Unique identifier for this rule +id = "awesome-rule-1" + +# Short human-readable description of the rule. +description = "awesome 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''' + +# Int used to extract secret from regex match and used as the group that will have +# its entropy checked if `entropy` is set. +secretGroup = 3 + +# Float representing the minimum shannon entropy a regex group must have to be considered a secret. +entropy = 3.5 + +# 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''' + +# Keywords are used for pre-regex check filtering. Rules that contain +# keywords will perform a quick string compare check to make sure the +# keyword(s) are in the content being scanned. Ideally these values should +# either be part of the identiifer or unique strings specific to the rule's regex +# (introduced in v8.6.0) +keywords = [ + "auth", + "password", + "token", +] + +# Array of strings used for metadata and reporting purposes. +tags = ["tag","another tag"] + + # ⚠️ In v8.21.0 `[rules.allowlist]` was replaced with `[[rules.allowlists]]`. + # This change was backwards-compatible: instances of `[rules.allowlist]` still work. + # + # You can define multiple allowlists for a rule to reduce false positives. + # A finding will be ignored if _ANY_ `[[rules.allowlists]]` matches. + [[rules.allowlists]] + description = "ignore commit A" + # When multiple criteria are defined the default condition is "OR". + # e.g., this can match on |commits| OR |paths| OR |stopwords|. + condition = "OR" + commits = [ "commit-A", "commit-B"] + paths = [ + '''go\.mod''', + '''go\.sum''' + ] + # note: stopwords targets the extracted secret, not the entire regex match + # like 'regexes' does. (stopwords introduced in 8.8.0) + stopwords = [ + '''client''', + '''endpoint''', + ] + + [[rules.allowlists]] + # The "AND" condition can be used to make sure all criteria match. + # e.g., this matches if |regexes| AND |paths| are satisfied. + condition = "AND" + # note: |regexes| defaults to check the _Secret_ in the finding. + # Acceptable values for |regexTarget| are "secret" (default), "match", and "line". + regexTarget = "match" + regexes = [ '''(?i)parseur[il]''' ] + paths = [ '''package-lock\.json''' ] + +# You can extend a particular rule from the default config. e.g., gitlab-pat +# if you have defined a custom token prefix on your GitLab instance +[[rules]] +id = "gitlab-pat" +# all the other attributes from the default rule are inherited + + [[rules.allowlists]] + regexTarget = "line" + regexes = [ '''MY-glpat-''' ] + + +# ⚠️ In v8.25.0 `[allowlist]` was replaced with `[[allowlists]]`. +# +# Global allowlists have a higher order of precedence 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. +[[allowlists]] +description = "global allow list" +commits = [ "commit-A", "commit-B", "commit-C"] +paths = [ + '''gitleaks\.toml''', + '''(.*?)(jpg|gif|doc)''' +] +# note: (global) regexTarget defaults to check the _Secret_ in the finding. +# Acceptable values for regexTarget are "match" and "line" +regexTarget = "match" +regexes = [ + '''219-09-9999''', + '''078-05-1120''', + '''(9[0-9]{2}|666)-\d{2}-\d{4}''', +] +# note: stopwords targets the extracted secret, not the entire regex match +# like 'regexes' does. (stopwords introduced in 8.8.0) +stopwords = [ + '''client''', + '''endpoint''', +] + +# ⚠️ In v8.25.0, `[[allowlists]]` have a new field called |targetRules|. +# +# Common allowlists can be defined once and assigned to multiple rules using |targetRules|. +# This will only run on the specified rules, not globally. +[[allowlists]] +targetRules = ["awesome-rule-1", "awesome-rule-2"] +description = "Our test assets trigger false-positives in a couple rules." +paths = ['''tests/expected/._\.json$'''] +``` + +Refer to the default [gitleaks config](https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml) for examples or follow the [contributing guidelines](https://github.com/gitleaks/gitleaks/blob/master/CONTRIBUTING.md) if you would like to contribute to the default configuration. Additionally, you can check out [this gitleaks blog post](https://blog.gitleaks.io/stop-leaking-secrets-configuration-2-3-aeed293b1fbf) which covers advanced configuration setups. + +### Additional Configuration + +#### Composite Rules (Multi-part or `required` Rules) +In v8.28.0 Gitleaks introduced composite rules, which are made up of a single "primary" rule and one or more auxiliary or `required` rules. To create a composite rule, add a `[[rules.required]]` table to the primary rule specifying an `id` and optionally `withinLines` and/or `withinColumns` proximity constraints. A fragment is a chunk of content that Gitleaks processes at once (typically a file, part of a file, or git diff), and proximity matching instructs the primary rule to only report a finding if the auxiliary `required` rules also find matches within the specified area of the fragment. + +**Proximity matching:** Using the `withinLines` and `withinColumns` fields instructs the primary rule to only report a finding if the auxiliary `required` rules also find matches within the specified proximity. You can set: + +- **`withinLines: N`** - required findings must be within N lines (vertically) +- **`withinColumns: N`** - required findings must be within N characters (horizontally) +- **Both** - creates a rectangular search area (both constraints must be satisfied) +- **Neither** - fragment-level matching (required findings can be anywhere in the same fragment) + +Here are diagrams illustrating each proximity behavior: + +``` +p = primary captured secret +a = auxiliary (required) captured secret +fragment = section of data gitleaks is looking at + + + *Fragment-level proximity* + Any required finding in the fragment + ┌────────┐ + ┌──────┤fragment├─────┐ + │ └──────┬─┤ │ ┌───────┐ + │ │a│◀────┼─│✓ MATCH│ + │ ┌─┐└─┘ │ └───────┘ + │┌─┐ │p│ │ + ││a│ ┌─┐└─┘ │ ┌───────┐ + │└─┘ │a│◀──────────┼─│✓ MATCH│ + └─▲─────┴─┴───────────┘ └───────┘ + │ ┌───────┐ + └────│✓ MATCH│ + └───────┘ + + + *Column bounded proximity* + `withinColumns = 3` + ┌────────┐ + ┌────┬─┤fragment├─┬───┐ + │ └──────┬─┤ │ ┌───────────┐ + │ │ │a│◀┼───┼─│+1C ✓ MATCH│ + │ ┌─┐└─┘ │ └───────────┘ + │┌─┐ │ │p│ │ │ +┌──▶│a│ ┌─┐ └─┘ │ ┌───────────┐ +│ │└─┘ ││a│◀────────┼───┼─│-2C ✓ MATCH│ +│ │ ┘ │ └───────────┘ +│ └── -3C ───0C─── +3C ─┘ +│ ┌─────────┐ +│ │ -4C ✗ NO│ +└──│ MATCH │ + └─────────┘ + + + *Line bounded proximity* + `withinLines = 4` + ┌────────┐ + ┌─────┤fragment├─────┐ + +4L─ ─ ┴────────┘─ ─ ─│ + │ │ + │ ┌─┐ │ ┌────────────┐ + │ ┌─┐ │a│◀──┼─│+1L ✓ MATCH │ + 0L ┌─┐ │p│ └─┘ │ ├────────────┤ + │ │a│◀──┴─┴────────┼─│-1L ✓ MATCH │ + │ └─┘ │ └────────────┘ + │ │ ┌─────────┐ + -4L─ ─ ─ ─ ─ ─ ─ ─┌─┐─│ │-5L ✗ NO │ + │ │a│◀┼─│ MATCH │ + └────────────────┴─┴─┘ └─────────┘ + + + *Line and column bounded proximity* + `withinLines = 4` + `withinColumns = 3` + ┌────────┐ + ┌─────┤fragment├─────┐ + +4L ┌└────────┴ ┐ │ + │ ┌─┐ │ ┌───────────────┐ + │ │ │a│◀┼───┼─│+2L/+1C ✓ MATCH│ + │ ┌─┐└─┘ │ └───────────────┘ + 0L │ │p│ │ │ + │ └─┘ │ + │ │ │ │ ┌────────────┐ + -4L ─ ─ ─ ─ ─ ─┌─┐ │ │-5L/+3C ✗ NO│ + │ │a│◀┼─│ MATCH │ + └───-3C────0L───+3C┴─┘ └────────────┘ +``` + +
Some final quick thoughts on composite rules.This is an experimental feature! It's subject to change so don't go sellin' a new B2B SaaS feature built ontop of this feature. Scan type (git vs dir) based context is interesting. I'm monitoring the situation. Composite rules might not be super useful for git scans because gitleaks only looks at additions in the git history. It could be useful to scan non-additions in git history for `required` rules. Oh, right this is a readme, I'll shut up now.
+ +#### gitleaks:allow + +If you are knowingly committing a test secret that gitleaks will catch you can add a `gitleaks:allow` comment to that line which will instruct gitleaks +to ignore that secret. Ex: + +``` +class CustomClass: + discord_client_secret = '8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ' #gitleaks:allow + +``` + +#### .gitleaksignore + +You can ignore specific findings by creating a `.gitleaksignore` file at the root of your repo. In release v8.10.0 Gitleaks added a `Fingerprint` value to the Gitleaks report. Each leak, or finding, has a Fingerprint that uniquely identifies a secret. Add this fingerprint to the `.gitleaksignore` file to ignore that specific secret. See Gitleaks' [.gitleaksignore](https://github.com/gitleaks/gitleaks/blob/master/.gitleaksignore) for an example. Note: this feature is experimental and is subject to change in the future. + +#### Decoding + +Sometimes secrets are encoded in a way that can make them difficult to find +with just regex. Now you can tell gitleaks to automatically find and decode +encoded text. The flag `--max-decode-depth` enables this feature (the default +value "0" means the feature is disabled by default). + +Recursive decoding is supported since decoded text can also contain encoded +text. The flag `--max-decode-depth` sets the recursion limit. Recursion stops +when there are no new segments of encoded text to decode, so setting a really +high max depth doesn't mean it will make that many passes. It will only make as +many as it needs to decode the text. Overall, decoding only minimally increases +scan times. + +The findings for encoded text differ from normal findings in the following +ways: + +- The location points the bounds of the encoded text + - If the rule matches outside the encoded text, the bounds are adjusted to + include that as well +- The match and secret contain the decoded value +- Two tags are added `decoded:` and `decode-depth:` + +Currently supported encodings: + +- **percent** - Any printable ASCII percent encoded values +- **hex** - Any printable ASCII hex encoded values >= 32 characters +- **base64** - Any printable ASCII base64 encoded values >= 16 characters + +#### Archive Scanning + +Sometimes secrets are packaged within archive files like zip files or tarballs, +making them difficult to discover. Now you can tell gitleaks to automatically +extract and scan the contents of archives. The flag `--max-archive-depth` +enables this feature for both `dir` and `git` scan types. The default value of +"0" means this feature is disabled by default. + +Recursive scanning is supported since archives can also contain other archives. +The `--max-archive-depth` flag sets the recursion limit. Recursion stops when +there are no new archives to extract, so setting a very high max depth just +sets the potential to go that deep. It will only go as deep as it needs to. + +The findings for secrets located within an archive will include the path to the +file inside the archive. Inner paths are separated with `!`. + +Example finding (shortened for brevity): + +``` +Finding: DB_PASSWORD=8ae31cacf141669ddfb5da +... +File: testdata/archives/nested.tar.gz!archives/files.tar!files/.env.prod +Line: 4 +Commit: 6e6ee6596d337bb656496425fb98644eb62b4a82 +... +Fingerprint: 6e6ee6596d337bb656496425fb98644eb62b4a82:testdata/archives/nested.tar.gz!archives/files.tar!files/.env.prod:generic-api-key:4 +Link: https://github.com/leaktk/gitleaks/blob/6e6ee6596d337bb656496425fb98644eb62b4a82/testdata/archives/nested.tar.gz +``` + +This means a secret was detected on line 4 of `files/.env.prod.` which is in +`archives/files.tar` which is in `testdata/archives/nested.tar.gz`. + +Currently supported formats: + +The [compression](https://github.com/mholt/archives?tab=readme-ov-file#supported-compression-formats) +and [archive](https://github.com/mholt/archives?tab=readme-ov-file#supported-archive-formats) +formats supported by mholt's [archives package](https://github.com/mholt/archives) +are supported. + +#### Reporting + +Gitleaks has built-in support for several report formats: [`json`](https://github.com/gitleaks/gitleaks/blob/master/testdata/expected/report/json_simple.json), [`csv`](https://github.com/gitleaks/gitleaks/blob/master/testdata/expected/report/csv_simple.csv?plain=1), [`junit`](https://github.com/gitleaks/gitleaks/blob/master/testdata/expected/report/junit_simple.xml), and [`sarif`](https://github.com/gitleaks/gitleaks/blob/master/testdata/expected/report/sarif_simple.sarif). + +If none of these formats fit your need, you can create your own report format with a [Go `text/template` .tmpl file](https://www.digitalocean.com/community/tutorials/how-to-use-templates-in-go#step-4-writing-a-template) and the `--report-template` flag. The template can use [extended functionality from the `Masterminds/sprig` template library](https://masterminds.github.io/sprig/). + +For example, the following template provides a custom JSON output: +```gotemplate +# jsonextra.tmpl +[{{ $lastFinding := (sub (len . ) 1) }} +{{- range $i, $finding := . }}{{with $finding}} + { + "Description": {{ quote .Description }}, + "StartLine": {{ .StartLine }}, + "EndLine": {{ .EndLine }}, + "StartColumn": {{ .StartColumn }}, + "EndColumn": {{ .EndColumn }}, + "Line": {{ quote .Line }}, + "Match": {{ quote .Match }}, + "Secret": {{ quote .Secret }}, + "File": "{{ .File }}", + "SymlinkFile": {{ quote .SymlinkFile }}, + "Commit": {{ quote .Commit }}, + "Entropy": {{ .Entropy }}, + "Author": {{ quote .Author }}, + "Email": {{ quote .Email }}, + "Date": {{ quote .Date }}, + "Message": {{ quote .Message }}, + "Tags": [{{ $lastTag := (sub (len .Tags ) 1) }}{{ range $j, $tag := .Tags }}{{ quote . }}{{ if ne $j $lastTag }},{{ end }}{{ end }}], + "RuleID": {{ quote .RuleID }}, + "Fingerprint": {{ quote .Fingerprint }} + }{{ if ne $i $lastFinding }},{{ end }} +{{- end}}{{ end }} +] +``` + +Usage: +```sh +$ gitleaks dir ~/leaky-repo/ --report-path "report.json" --report-format template --report-template testdata/report/jsonextra.tmpl +``` + +## Sponsorships + +

+

coderabbit.ai

+ + CodeRabbit.ai Sponsorship + +

+ + +## Exit Codes + +You can always set the exit code when leaks are encountered with the --exit-code flag. Default exit codes below: + +``` +0 - no leaks present +1 - leaks or error encountered +126 - unknown flag +``` + +### Join the Discord! [![Discord](https://img.shields.io/discord/1102689410522284044.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/8Hzbrnkr7E) diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..f8881eb --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`gitleaks/gitleaks` +- 原始仓库:https://github.com/gitleaks/gitleaks +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1750c8d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +|---------|-----------| +| Latest | Yes | + +## Reporting a Vulnerability + +If you discover a security vulnerability in gitleaks, please report it responsibly: + +1. **Do not open a public issue.** +2. Use [GitHub's private vulnerability reporting](https://github.com/gitleaks/gitleaks/security/advisories/new) to submit your report directly. +3. Include a description of the vulnerability, steps to reproduce, and any relevant logs or screenshots. + +## Scope + +This policy covers `gitleaks` (this repository). For vulnerabilities in `gitleaks-action`, please report them at [gitleaks/gitleaks-action](https://github.com/gitleaks/gitleaks-action/security/advisories/new). diff --git a/USERS.md b/USERS.md new file mode 100644 index 0000000..3c3cd69 --- /dev/null +++ b/USERS.md @@ -0,0 +1,26 @@ +## Who uses Gitleaks? + +As the Gitleaks Community grows, we'd like to keep a list of our users. + +Here's a running list of some organizations using Gitleaks[^1]: + +1. [GitLab](https://docs.gitlab.com/ee/user/application_security/secret_detection/) +1. [Gitleaks](https://gitleaks.io) +1. [GoReleaser](https://goreleaser.com) +2. [Trendyol](https://trendyol.com) + +Feel free to [add yours](https://github.com/zricethezav/gitleaks/edit/master/USERS.md)! + + + +[^1]: Entries were either added by the companies themselves or by the maintainers after seeing it in the wild. + You can see all public repositories using Gitleaks or Gitleaks-Action by [searching on GitHub](https://github.com/search?q=gitleaks). + + diff --git a/cmd/detect.go b/cmd/detect.go new file mode 100644 index 0000000..9616117 --- /dev/null +++ b/cmd/detect.go @@ -0,0 +1,136 @@ +// The `detect` and `protect` command is now deprecated. Here are some equivalent commands +// to help guide you. + +// OLD CMD: gitleaks detect --source={repo} +// NEW CMD: gitleaks git {repo} + +// OLD CMD: gitleaks protect --source={repo} +// NEW CMD: gitleaks git --pre-commit {repo} + +// OLD CMD: gitleaks protect --staged --source={repo} +// NEW CMD: gitleaks git --pre-commit --staged {repo} + +// OLD CMD: gitleaks detect --no-git --source={repo} +// NEW CMD: gitleaks directory {directory/file} + +// OLD CMD: gitleaks detect --no-git --pipe +// NEW CMD: gitleaks stdin + +package cmd + +import ( + "os" + "time" + + "github.com/spf13/cobra" + + "github.com/zricethezav/gitleaks/v8/cmd/scm" + "github.com/zricethezav/gitleaks/v8/logging" + "github.com/zricethezav/gitleaks/v8/report" + "github.com/zricethezav/gitleaks/v8/sources" +) + +func init() { + rootCmd.AddCommand(detectCmd) + 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") + detectCmd.Flags().Bool("pipe", false, "scan input from stdin, ex: `cat some_file | gitleaks detect --pipe`") + detectCmd.Flags().Bool("follow-symlinks", false, "scan files that are symlinks to other files") + detectCmd.Flags().StringP("source", "s", ".", "path to source") + detectCmd.Flags().String("log-opts", "", "git log options") + detectCmd.Flags().String("platform", "", "the target platform used to generate links (github, gitlab)") +} + +var detectCmd = &cobra.Command{ + Use: "detect", + Short: "detect secrets in code", + Run: runDetect, + Hidden: true, +} + +func runDetect(cmd *cobra.Command, args []string) { + // start timer + start := time.Now() + sourcePath := mustGetStringFlag(cmd, "source") + + // setup config (aka, the thing that defines rules) + initConfig(sourcePath) + initDiagnostics() + cfg := Config(cmd) + + // create detector + detector := Detector(cmd, cfg, sourcePath) + + // parse flags + detector.FollowSymlinks = mustGetBoolFlag(cmd, "follow-symlinks") + exitCode := mustGetIntFlag(cmd, "exit-code") + noGit := mustGetBoolFlag(cmd, "no-git") + fromPipe := mustGetBoolFlag(cmd, "pipe") + // determine what type of scan: + // - git: scan the history of the repo + // - no-git: scan files by treating the repo as a plain directory + var ( + err error + findings []report.Finding + ) + if noGit { + findings, err = detector.DetectSource( + cmd.Context(), &sources.Files{ + Config: &cfg, + FollowSymlinks: detector.FollowSymlinks, + MaxFileSize: detector.MaxTargetMegaBytes * 1_000_000, + Path: sourcePath, + Sema: detector.Sema, + MaxArchiveDepth: detector.MaxArchiveDepth, + }, + ) + + if err != nil { + // don't exit on error, just log it + logging.Error().Err(err).Msg("failed to scan directory") + } + } else if fromPipe { + findings, err = detector.DetectSource( + cmd.Context(), &sources.File{ + Content: os.Stdin, + MaxArchiveDepth: detector.MaxArchiveDepth, + }, + ) + + if err != nil { + // log fatal to exit, no need to continue since a report + // will not be generated when scanning from a pipe...for now + logging.Fatal().Err(err).Msg("failed scan input from stdin") + } + } else { + var ( + gitCmd *sources.GitCmd + scmPlatform scm.Platform + ) + + logOpts := mustGetStringFlag(cmd, "log-opts") + if gitCmd, err = sources.NewGitLogCmdContext(cmd.Context(), sourcePath, logOpts); err != nil { + logging.Fatal().Err(err).Msg("could not create Git cmd") + } + + if scmPlatform, err = scm.PlatformFromString(mustGetStringFlag(cmd, "platform")); err != nil { + logging.Fatal().Err(err).Send() + } + + findings, err = detector.DetectSource( + cmd.Context(), &sources.Git{ + Cmd: gitCmd, + Config: &detector.Config, + Remote: sources.NewRemoteInfoContext(cmd.Context(), scmPlatform, sourcePath), + Sema: detector.Sema, + MaxArchiveDepth: detector.MaxArchiveDepth, + }, + ) + + if err != nil { + // don't exit on error, just log it + logging.Error().Err(err).Msg("failed to scan Git repository") + } + } + + findingSummaryAndExit(detector, findings, exitCode, start, err) +} diff --git a/cmd/diagnostics.go b/cmd/diagnostics.go new file mode 100644 index 0000000..a48ce80 --- /dev/null +++ b/cmd/diagnostics.go @@ -0,0 +1,239 @@ +package cmd + +import ( + "errors" + "fmt" + "net/http" + _ "net/http/pprof" + "os" + "path/filepath" + "runtime" + "runtime/pprof" + "runtime/trace" + "strings" + + "github.com/zricethezav/gitleaks/v8/logging" +) + +// DiagnosticsManager manages various types of diagnostics +type DiagnosticsManager struct { + Enabled bool + DiagTypes []string + OutputDir string + cpuProfile *os.File + memProfile string + traceProfile *os.File +} + +// NewDiagnosticsManager creates a new DiagnosticsManager instance +func NewDiagnosticsManager(diagnosticsFlag string, diagnosticsDir string) (*DiagnosticsManager, error) { + if diagnosticsFlag == "" { + return &DiagnosticsManager{Enabled: false}, nil + } + + dm := &DiagnosticsManager{ + Enabled: true, + DiagTypes: strings.Split(diagnosticsFlag, ","), + OutputDir: diagnosticsDir, + } + + if diagnosticsFlag == "http" { + if len(diagnosticsDir) != 0 { + return nil, errors.New("the diagnostics directory should not be set in http mode") + } + + return dm, nil + } + + // If no output directory is specified, use the current directory + if dm.OutputDir == "" { + var err error + dm.OutputDir, err = os.Getwd() + if err != nil { + return nil, fmt.Errorf("failed to get current directory: %w", err) + } + logging.Debug().Msgf("No diagnostics directory specified, using current directory: %s", dm.OutputDir) + } + + // Create the output directory if it doesn't exist + if err := os.MkdirAll(dm.OutputDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create diagnostics directory: %w", err) + } + + // Make sure the output directory is absolute + if !filepath.IsAbs(dm.OutputDir) { + absPath, err := filepath.Abs(dm.OutputDir) + if err != nil { + return nil, fmt.Errorf("failed to get absolute path for diagnostics directory: %w", err) + } + dm.OutputDir = absPath + } + + logging.Debug().Msgf("Diagnostics enabled: %s", strings.Join(dm.DiagTypes, ",")) + logging.Debug().Msgf("Diagnostics output directory: %s", dm.OutputDir) + + return dm, nil +} + +// StartDiagnostics starts all enabled diagnostics +func (dm *DiagnosticsManager) StartDiagnostics() error { + if !dm.Enabled { + return nil + } + + var err error + + for _, diagType := range dm.DiagTypes { + diagType = strings.TrimSpace(diagType) + switch diagType { + case "cpu": + if err = dm.StartCPUProfile(); err != nil { + return err + } + case "mem": + if err = dm.SetupMemoryProfile(); err != nil { + return err + } + case "trace": + if err = dm.StartTraceProfile(); err != nil { + return err + } + case "http": + if err = dm.StartHttpHandler(); err != nil { + return err + } + default: + logging.Warn().Msgf("Unknown diagnostics type: %s", diagType) + } + } + + return nil +} + +// StopDiagnostics stops all started diagnostics +func (dm *DiagnosticsManager) StopDiagnostics() { + if !dm.Enabled { + return + } + + logging.Debug().Msg("Stopping diagnostics and writing profiling data...") + + for _, diagType := range dm.DiagTypes { + diagType = strings.TrimSpace(diagType) + switch diagType { + case "cpu": + dm.StopCPUProfile() + case "mem": + dm.WriteMemoryProfile() + case "trace": + dm.StopTraceProfile() + case "http": + // No need to stop the http one + } + } +} + +func (dm *DiagnosticsManager) StartHttpHandler() error { + if len(dm.DiagTypes) > 1 { + return errors.New("other diagnostics modes should not be enabled when http mode is enabled") + } + + go func() { + logging.Error().Err(http.ListenAndServe("localhost:6060", nil)).Send() + }() + + logging.Info().Str("url", "http://localhost:6060/debug/pprof/").Msg("Diagnostics server started") + return nil +} + +// StartCPUProfile starts CPU profiling +func (dm *DiagnosticsManager) StartCPUProfile() error { + cpuProfilePath := filepath.Join(dm.OutputDir, "cpu.pprof") + f, err := os.Create(cpuProfilePath) + if err != nil { + return fmt.Errorf("could not create CPU profile at %s: %w", cpuProfilePath, err) + } + + if err := pprof.StartCPUProfile(f); err != nil { + _ = f.Close() + return fmt.Errorf("could not start CPU profile: %w", err) + } + + dm.cpuProfile = f + return nil +} + +// StopCPUProfile stops CPU profiling +func (dm *DiagnosticsManager) StopCPUProfile() { + if dm.cpuProfile != nil { + pprof.StopCPUProfile() + if err := dm.cpuProfile.Close(); err != nil { + logging.Error().Err(err).Msg("Error closing CPU profile file") + } + logging.Info().Msgf("CPU profile written to: %s", dm.cpuProfile.Name()) + dm.cpuProfile = nil + } +} + +// SetupMemoryProfile sets up memory profiling to be written when StopDiagnostics is called +func (dm *DiagnosticsManager) SetupMemoryProfile() error { + memProfilePath := filepath.Join(dm.OutputDir, "mem.pprof") + dm.memProfile = memProfilePath + return nil +} + +// WriteMemoryProfile writes the memory profile to disk +func (dm *DiagnosticsManager) WriteMemoryProfile() { + if dm.memProfile == "" { + return + } + + f, err := os.Create(dm.memProfile) + if err != nil { + logging.Error().Err(err).Msgf("Could not create memory profile at %s", dm.memProfile) + return + } + + // Get memory profile + runtime.GC() // Run GC before taking the memory profile + if err := pprof.WriteHeapProfile(f); err != nil { + logging.Error().Err(err).Msg("Could not write memory profile") + } else { + logging.Info().Msgf("Memory profile written to: %s", dm.memProfile) + } + + if err := f.Close(); err != nil { + logging.Error().Err(err).Msg("Error closing memory profile file") + } + + dm.memProfile = "" +} + +// StartTraceProfile starts execution tracing +func (dm *DiagnosticsManager) StartTraceProfile() error { + traceProfilePath := filepath.Join(dm.OutputDir, "trace.out") + f, err := os.Create(traceProfilePath) + if err != nil { + return fmt.Errorf("could not create trace profile at %s: %w", traceProfilePath, err) + } + + if err := trace.Start(f); err != nil { + _ = f.Close() + return fmt.Errorf("could not start trace profile: %w", err) + } + + dm.traceProfile = f + return nil +} + +// StopTraceProfile stops execution tracing +func (dm *DiagnosticsManager) StopTraceProfile() { + if dm.traceProfile != nil { + trace.Stop() + if err := dm.traceProfile.Close(); err != nil { + logging.Error().Err(err).Msg("Error closing trace profile file") + } + logging.Info().Msgf("Trace profile written to: %s", dm.traceProfile.Name()) + dm.traceProfile = nil + } +} diff --git a/cmd/directory.go b/cmd/directory.go new file mode 100644 index 0000000..d91700f --- /dev/null +++ b/cmd/directory.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "time" + + "github.com/spf13/cobra" + + "github.com/zricethezav/gitleaks/v8/logging" + "github.com/zricethezav/gitleaks/v8/sources" +) + +func init() { + rootCmd.AddCommand(directoryCmd) + directoryCmd.Flags().Bool("follow-symlinks", false, "scan files that are symlinks to other files") +} + +var directoryCmd = &cobra.Command{ + Use: "dir [flags] [path]", + Aliases: []string{"file", "directory"}, + Short: "scan directories or files for secrets", + Run: runDirectory, +} + +func runDirectory(cmd *cobra.Command, args []string) { + // grab source + source := "." + if len(args) == 1 { + source = args[0] + if source == "" { + source = "." + } + } + + initConfig(source) + initDiagnostics() + var err error + + // setup config (aka, the thing that defines rules) + cfg := Config(cmd) + + // start timer + start := time.Now() + + detector := Detector(cmd, cfg, source) + + // set follow symlinks flag + if detector.FollowSymlinks, err = cmd.Flags().GetBool("follow-symlinks"); err != nil { + logging.Fatal().Err(err).Send() + } + + // set exit code + exitCode, err := cmd.Flags().GetInt("exit-code") + if err != nil { + logging.Fatal().Err(err).Msg("could not get exit code") + } + + findings, err := detector.DetectSource( + cmd.Context(), + &sources.Files{ + Config: &cfg, + FollowSymlinks: detector.FollowSymlinks, + MaxFileSize: detector.MaxTargetMegaBytes * 1_000_000, + Path: source, + Sema: detector.Sema, + MaxArchiveDepth: detector.MaxArchiveDepth, + }, + ) + + if err != nil { + logging.Error().Err(err).Msg("failed scan directory") + } + + findingSummaryAndExit(detector, findings, exitCode, start, err) +} diff --git a/cmd/generate/config/base/config.go b/cmd/generate/config/base/config.go new file mode 100644 index 0000000..7e2ff23 --- /dev/null +++ b/cmd/generate/config/base/config.go @@ -0,0 +1,121 @@ +package base + +import ( + "fmt" + "strings" + + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func CreateGlobalConfig() config.Config { + return config.Config{ + Title: "gitleaks config", + Allowlists: []*config.Allowlist{ + { + Description: "global allow lists", + Regexes: []*regexp.Regexp{ + // ----------- General placeholders ----------- + regexp.MustCompile(`(?i)^true|false|null$`), + // Awkward workaround to detect repeated characters. + func() *regexp.Regexp { + var ( + letters = "abcdefghijklmnopqrstuvwxyz*." + patterns []string + ) + for _, char := range letters { + if char == '*' || char == '.' { + patterns = append(patterns, fmt.Sprintf("\\%c+", char)) + } else { + patterns = append(patterns, fmt.Sprintf("%c+", char)) + } + } + return regexp.MustCompile("^(?i:" + strings.Join(patterns, "|") + ")$") + }(), + + // ----------- Environment Variables ----------- + regexp.MustCompile(`^\$(?:\d+|{\d+})$`), + regexp.MustCompile(`^\$(?:[A-Z_]+|[a-z_]+)$`), + regexp.MustCompile(`^\${(?:[A-Z_]+|[a-z_]+)}$`), + + // ----------- Interpolated Variables ----------- + // Ansible (https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html) + regexp.MustCompile(`^\{\{[ \t]*[\w ().|]+[ \t]*}}$`), + // GitHub Actions + // https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables + // https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions + regexp.MustCompile(`^\$\{\{[ \t]*(?:(?:env|github|secrets|vars)(?:\.[A-Za-z]\w+)+[\w "'&./=|]*)[ \t]*}}$`), + // NuGet (https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file#using-environment-variables) + regexp.MustCompile(`^%(?:[A-Z_]+|[a-z_]+)%$`), + // String formatting. + regexp.MustCompile(`^%[+\-# 0]?[bcdeEfFgGoOpqstTUvxX]$`), // Golang (https://pkg.go.dev/fmt) + regexp.MustCompile(`^\{\d{0,2}}$`), // Python (https://docs.python.org/3/tutorial/inputoutput.html) + // Urban Code Deploy (https://www.ibm.com/support/pages/replace-token-step-replaces-replacement-values-windows-variables) + regexp.MustCompile(`^@(?:[A-Z_]+|[a-z_]+)@$`), + + // ----------- Miscellaneous ----------- + // File paths + regexp.MustCompile(`^/Users/(?i)[a-z0-9]+/[\w .-/]+$`), // MacOS + regexp.MustCompile(`^/(?:bin|etc|home|opt|tmp|usr|var)/[\w ./-]+$`), // Linux + // 11980 Jps -Dapplication.home=D:\develop_tools\jdk\jdk1.8.0_131 -Xms8m + //regexp.MustCompile(`^$`), // Windows + }, + Paths: []*regexp.Regexp{ + regexp.MustCompile(`gitleaks\.toml`), + + // ----------- Documents and media ----------- + regexp.MustCompile(`(?i)\.(?:bmp|gif|jpe?g|png|svg|tiff?)$`), // Images + regexp.MustCompile(`(?i)\.(?:eot|[ot]tf|woff2?)$`), // Fonts + regexp.MustCompile(`(?i)\.(?:docx?|xlsx?|pdf|bin|socket|vsidx|v2|suo|wsuo|.dll|pdb|exe|gltf)$`), + + // ----------- Golang files ----------- + regexp.MustCompile(`go\.(?:mod|sum|work(?:\.sum)?)$`), + regexp.MustCompile(`(?:^|/)vendor/modules\.txt$`), + regexp.MustCompile(`(?:^|/)vendor/(?:github\.com|golang\.org/x|google\.golang\.org|gopkg\.in|istio\.io|k8s\.io|sigs\.k8s\.io)(?:/.*)?$`), + + // ----------- Java files ----------- + // Gradle + regexp.MustCompile(`(?:^|/)gradlew(?:\.bat)?$`), + regexp.MustCompile(`(?:^|/)gradle\.lockfile$`), + regexp.MustCompile(`(?:^|/)mvnw(?:\.cmd)?$`), + regexp.MustCompile(`(?:^|/)\.mvn/wrapper/MavenWrapperDownloader\.java$`), + + // ----------- JavaScript files ----------- + // Dependencies and lock files. + regexp.MustCompile(`(?:^|/)node_modules(?:/.*)?$`), + regexp.MustCompile(`(?:^|/)(?:deno\.lock|npm-shrinkwrap\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$`), + regexp.MustCompile(`(?:^|/)bower_components(?:/.*)?$`), + // TODO: Add more common static assets, such as swagger-ui. + regexp.MustCompile(`(?:^|/)(?:angular|bootstrap|jquery(?:-?ui)?|plotly|swagger-?ui)[a-zA-Z0-9.-]*(?:\.min)?\.js(?:\.map)?$`), + regexp.MustCompile(`(?:^|/)javascript\.json$`), + + // ----------- Python files ----------- + // Dependencies and lock files. + regexp.MustCompile(`(?:^|/)(?:Pipfile|poetry)\.lock$`), + // Virtual environments + regexp.MustCompile(`(?i)(?:^|/)(?:v?env|virtualenv)/lib(?:64)?(?:/.*)?$`), + regexp.MustCompile(`(?i)(?:^|/)(?:lib(?:64)?/python[23](?:\.\d{1,2})+|python/[23](?:\.\d{1,2})+/lib(?:64)?)(?:/.*)?$`), + // dist-info directory (https://py-pkgs.org/04-package-structure.html#building-sdists-and-wheels) + regexp.MustCompile(`(?i)(?:^|/)[a-z0-9_.]+-[0-9.]+\.dist-info(?:/.+)?$`), + + // ----------- Ruby files ----------- + regexp.MustCompile(`(?:^|/)vendor/(?:bundle|ruby)(?:/.*?)?$`), + regexp.MustCompile(`\.gem$`), // tar archive + + // Misc + regexp.MustCompile(`verification-metadata\.xml`), + regexp.MustCompile(`Database.refactorlog`), + + // ----------- Git files ------------ + regexp.MustCompile(`(?:^|/)\.git$`), + }, + StopWords: []string{ + "abcdefghijklmnopqrstuvwxyz", // character range + // ----------- Secrets ----------- + // Checkmarx client secret. (https://github.com/checkmarx-ts/checkmarx-python-sdk/blob/86560f6e2a3e46d16322101294da10d5d190312d/README.md?plain=1#L56) + "014df517-39d1-4453-b7b3-9930c563627c", + }, + }, + }, + } +} diff --git a/cmd/generate/config/base/config_test.go b/cmd/generate/config/base/config_test.go new file mode 100644 index 0000000..322df3e --- /dev/null +++ b/cmd/generate/config/base/config_test.go @@ -0,0 +1,203 @@ +package base + +import ( + "testing" +) + +var allowlistRegexTests = map[string]struct { + invalid []string + valid []string +}{ + "general placeholders": { + invalid: []string{ + `true`, `True`, `false`, `False`, `null`, `NULL`, + }, + }, + "general placeholders - repeated characters": { + invalid: []string{ + `aaaaaaaaaaaaaaaaa`, `BBBBBBBBBBbBBBBBBBbBB`, `********************`, + }, + valid: []string{`aaaaaaaaaaaaaaaaaaabaa`, `pas*************d`}, + }, + "environment variables": { + invalid: []string{`$2`, `$GIT_PASSWORD`, `${GIT_PASSWORD}`, `$password`}, + valid: []string{`$yP@R.@=ibxI`, `$2a6WCust9aE`, `${not_complete1`}, + }, + "interpolated variables - ansible": { + invalid: []string{ + `{{ x }}`, `{{ password }}`, `{{password}}`, `{{ data.proxy_password }}`, + `{{ dict1 | ansible.builtin.combine(dict2) }}`, + }, + }, + "interpolated variables - github actions": { + invalid: []string{ + `${{ env.First_Name }}`, + `${{ env.DAY_OF_WEEK == 'Monday' }}`, + `${{env.JAVA_VERSION}}`, + `${{ github.event.issue.title }}`, + `${{ github.repository == "Gattocrucco/lsqfitgp" }}`, + `${{ github.event.pull_request.number || github.ref }}`, + `${{ github.event_name == 'pull_request' && github.event.action == 'unassigned' }}`, + `${{ secrets.SuperSecret }}`, + `${{ vars.JOB_NAME }}`, + `${{ vars.USE_VARIABLES == 'true' }}`, + }, + }, + "interpolated variables - nuget": { + invalid: []string{ + `%MY_PASSWORD%`, `%password%`, + }, + }, + "interpolated variables - string fmt - golang": { + invalid: []string{ + `%b`, `%c`, `%d`, `% d`, `%e`, `%E`, `%f`, `%F`, `%g`, `%G`, `%o`, `%O`, `%p`, `%q`, `%-s`, `%s`, `%t`, `%T`, `%U`, `%#U`, `%+v`, `%#v`, `%v`, `%x`, `%X`, + }, + }, + "interpolated variables - string fmt - python": { + invalid: []string{ + `{}`, `{0}`, `{10}`, + }, + }, + "interpolated variables - ucd": { + invalid: []string{`@password@`, `@LDAP_PASS@`}, + valid: []string{`@username@mastodon.example`}, + }, + "miscellaneous - file paths": { + invalid: []string{ + // MacOS + `/Users/james/Projects/SwiftCode/build/Release`, + // Linux + `/tmp/screen-exchange`, + }, + valid: []string{}, + }, +} + +func TestConfigAllowlistRegexes(t *testing.T) { + cfg := CreateGlobalConfig() + allowlists := cfg.Allowlists + for name, cases := range allowlistRegexTests { + t.Run(name, func(t *testing.T) { + for _, c := range cases.invalid { + for _, a := range allowlists { + if !a.RegexAllowed(c) { + t.Errorf("invalid value not marked as allowed: %s", c) + } + } + } + + for _, c := range cases.valid { + for _, a := range allowlists { + if a.RegexAllowed(c) { + t.Errorf("valid value marked as allowed: %s", c) + } + } + } + }) + } +} + +func BenchmarkConfigAllowlistRegexes(b *testing.B) { + cfg := CreateGlobalConfig() + allowlists := cfg.Allowlists + for n := 0; n < b.N; n++ { + for _, cases := range allowlistRegexTests { + for _, c := range cases.invalid { + for _, a := range allowlists { + a.RegexAllowed(c) + } + } + + for _, c := range cases.valid { + for _, a := range allowlists { + a.RegexAllowed(c) + } + } + } + } +} + +var allowlistPathsTests = map[string]struct { + invalid []string + valid []string +}{ + "javascript - common static assets": { + invalid: []string{ + `tests/e2e/nuget/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js`, + `src/main/static/lib/angular.1.2.16.min.js`, + `src/main/resources/static/jquery-ui-1.12.1/jquery-ui-min.js`, + `src/main/resources/static/js/jquery-ui-1.10.4.min.js`, + `src-static/js/plotly.min.js`, + `swagger/swaggerui/swagger-ui-bundle.js.map`, + `swagger/swaggerui/swagger-ui-es-bundle.js.map`, + `src/main/static/swagger-ui.min.js`, + `swagger/swaggerui/swagger-ui.js`, + }, + }, + "python": { + invalid: []string{ + // lock files + `Pipfile.lock`, `poetry.lock`, + // virtual environments + "env/lib/python3.7/site-packages/urllib3/util/url.py", + "venv/Lib/site-packages/regex-2018.08.29.dist-info/DESCRIPTION.rst", + "venv/lib64/python3.5/site-packages/pynvml.py", + "python/python3/virtualenv/Lib/site-packages/pyphonetics/utils.py", + "virtualenv/lib64/python3.7/base64.py", + // packages + "cde-root/usr/lib64/python2.4/site-packages/Numeric.pth", + "lib/python3.9/site-packages/setuptools/_distutils/msvccompiler.py", + "lib/python3.8/site-packages/botocore/data/alexaforbusiness/2017-11-09/service-2.json", + "code/python/3.7.4/Lib/site-packages/dask/bytes/tests/test_bytes_utils.py", + "python/3.7.4/Lib/site-packages/fsspec/utils.py", + "python/2.7.16.32/Lib/bsddb/test/test_dbenv.py", + "python/lib/python3.8/site-packages/boto3/data/ec2/2016-04-01/resources-1.json", + // distinfo + "libs/PyX-0.15.dist-info/AUTHORS", + }, + }, +} + +func TestConfigAllowlistPaths(t *testing.T) { + cfg := CreateGlobalConfig() + allowlists := cfg.Allowlists + for name, cases := range allowlistPathsTests { + t.Run(name, func(t *testing.T) { + for _, c := range cases.invalid { + for _, a := range allowlists { + if !a.PathAllowed(c) { + t.Errorf("invalid path not marked as allowed: %s", c) + } + } + } + + for _, c := range cases.valid { + for _, a := range allowlists { + if a.PathAllowed(c) { + t.Errorf("valid path marked as allowed: %s", c) + } + } + } + }) + } +} + +func BenchmarkConfigAllowlistPaths(b *testing.B) { + cfg := CreateGlobalConfig() + allowlists := cfg.Allowlists + for n := 0; n < b.N; n++ { + for _, cases := range allowlistPathsTests { + for _, c := range cases.invalid { + for _, a := range allowlists { + a.PathAllowed(c) + } + } + + for _, c := range cases.valid { + for _, a := range allowlists { + a.PathAllowed(c) + } + } + } + } +} diff --git a/cmd/generate/config/main.go b/cmd/generate/config/main.go new file mode 100644 index 0000000..f5a9b96 --- /dev/null +++ b/cmd/generate/config/main.go @@ -0,0 +1,301 @@ +package main + +import ( + "os" + "slices" + "text/template" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/base" + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/rules" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/logging" +) + +const ( + templatePath = "rules/config.tmpl" +) + +//go:generate go run $GOFILE ../../../config/gitleaks.toml + +func main() { + if len(os.Args) < 2 { + _, _ = os.Stderr.WriteString("Specify path to the gitleaks.toml config\n") + os.Exit(2) + } + gitleaksConfigPath := os.Args[1] + + configRules := []*config.Rule{ + rules.OnePasswordSecretKey(), + rules.OnePasswordServiceAccountToken(), + rules.AdafruitAPIKey(), + rules.AdobeClientID(), + rules.AdobeClientSecret(), + rules.AgeSecretKey(), + rules.AirtableApiKey(), + rules.AirtablePersonalAccessToken(), + rules.AlgoliaApiKey(), + rules.AlibabaAccessKey(), + rules.AlibabaSecretKey(), + rules.AmazonBedrockAPIKeyLongLived(), + rules.AmazonBedrockAPIKeyShortLived(), + rules.AnthropicAdminApiKey(), + rules.AnthropicApiKey(), + rules.ArtifactoryApiKey(), + rules.ArtifactoryReferenceToken(), + rules.AsanaClientID(), + rules.AsanaClientSecret(), + rules.Atlassian(), + rules.Authress(), + rules.AWS(), + rules.AzureActiveDirectoryClientSecret(), + rules.BitBucketClientID(), + rules.BitBucketClientSecret(), + rules.BittrexAccessKey(), + rules.BittrexSecretKey(), + rules.Beamer(), + rules.CodecovAccessToken(), + rules.CoinbaseAccessToken(), + rules.ClickHouseCloud(), + rules.Clojars(), + rules.CloudflareAPIKey(), + rules.CloudflareGlobalAPIKey(), + rules.CloudflareOriginCAKey(), + rules.CohereAPIToken(), + rules.ConfluentAccessToken(), + rules.ConfluentSecretKey(), + rules.Contentful(), + rules.CurlHeaderAuth(), + rules.CurlBasicAuth(), + rules.Databricks(), + rules.DatadogtokenAccessToken(), + rules.DefinedNetworkingAPIToken(), + rules.DigitalOceanPAT(), + rules.DigitalOceanOAuthToken(), + rules.DigitalOceanRefreshToken(), + rules.DiscordAPIToken(), + rules.DiscordClientID(), + rules.DiscordClientSecret(), + rules.Doppler(), + rules.DropBoxAPISecret(), + rules.DropBoxLongLivedAPIToken(), + rules.DropBoxShortLivedAPIToken(), + rules.DroneciAccessToken(), + rules.Duffel(), + rules.Dynatrace(), + rules.EasyPost(), + rules.EasyPostTestAPI(), + rules.EtsyAccessToken(), + rules.FacebookSecret(), + rules.FacebookAccessToken(), + rules.FacebookPageAccessToken(), + rules.FastlyAPIToken(), + rules.FinicityClientSecret(), + rules.FinicityAPIToken(), + rules.FlickrAccessToken(), + rules.FinnhubAccessToken(), + rules.FlutterwavePublicKey(), + rules.FlutterwaveSecretKey(), + rules.FlutterwaveEncKey(), + rules.FlyIOAccessToken(), + rules.FrameIO(), + rules.Freemius(), + rules.FreshbooksAccessToken(), + rules.GoCardless(), + // TODO figure out what makes sense for GCP + // rules.GCPServiceAccount(), + rules.GCPAPIKey(), + rules.GitHubPat(), + rules.GitHubFineGrainedPat(), + rules.GitHubOauth(), + rules.GitHubApp(), + rules.GitHubRefresh(), + rules.GitlabCiCdJobToken(), + rules.GitlabDeployToken(), + rules.GitlabFeatureFlagClientToken(), + rules.GitlabFeedToken(), + rules.GitlabIncomingMailToken(), + rules.GitlabKubernetesAgentToken(), + rules.GitlabOauthAppSecret(), + rules.GitlabPat(), + rules.GitlabPatRoutable(), + rules.GitlabPipelineTriggerToken(), + rules.GitlabRunnerRegistrationToken(), + rules.GitlabRunnerAuthenticationToken(), + rules.GitlabRunnerAuthenticationTokenRoutable(), + rules.GitlabScimToken(), + rules.GitlabSessionCookie(), + rules.GitterAccessToken(), + rules.GrafanaApiKey(), + rules.GrafanaCloudApiToken(), + rules.GrafanaServiceAccountToken(), + rules.HarnessApiKey(), + rules.HashiCorpTerraform(), + rules.HashicorpField(), + rules.Heroku(), + rules.HerokuV2(), + rules.HubSpot(), + rules.HuggingFaceAccessToken(), + rules.HuggingFaceOrganizationApiToken(), + rules.Intercom(), + rules.Intra42ClientSecret(), + rules.JFrogAPIKey(), + rules.JFrogIdentityToken(), + rules.JWT(), + rules.JWTBase64(), + rules.KrakenAccessToken(), + rules.KubernetesSecret(), + rules.KucoinAccessToken(), + rules.KucoinSecretKey(), + rules.LaunchDarklyAccessToken(), + rules.LinearAPIToken(), + rules.LinearClientSecret(), + rules.LinkedinClientID(), + rules.LinkedinClientSecret(), + rules.LobAPIToken(), + rules.LobPubAPIToken(), + rules.LookerClientID(), + rules.LookerClientSecret(), + rules.MailChimp(), + rules.MailGunPubAPIToken(), + rules.MailGunPrivateAPIToken(), + rules.MailGunSigningKey(), + rules.MapBox(), + rules.MattermostAccessToken(), + rules.MaxMindLicenseKey(), + rules.Meraki(), + rules.MessageBirdAPIToken(), + rules.MessageBirdClientID(), + rules.NetlifyAccessToken(), + rules.NewRelicUserID(), + rules.NewRelicUserKey(), + rules.NewRelicBrowserAPIKey(), + rules.NewRelicInsertKey(), + rules.Notion(), + rules.NPM(), + rules.NugetConfigPassword(), + rules.NytimesAccessToken(), + rules.OctopusDeployApiKey(), + rules.OktaAccessToken(), + rules.OpenAI(), + rules.OpenshiftUserToken(), + rules.PerplexityAPIKey(), + rules.PlaidAccessID(), + rules.PlaidSecretKey(), + rules.PlaidAccessToken(), + rules.PlanetScalePassword(), + rules.PlanetScaleAPIToken(), + rules.PlanetScaleOAuthToken(), + rules.PostManAPI(), + rules.Prefect(), + rules.PrivateAIToken(), + rules.PrivateKey(), + rules.PrivateKeyPKCS12File(), + rules.PulumiAPIToken(), + rules.PyPiUploadToken(), + rules.RapidAPIAccessToken(), + rules.ReadMe(), + rules.RubyGemsAPIToken(), + rules.ScalingoAPIToken(), + rules.SendbirdAccessID(), + rules.SendbirdAccessToken(), + rules.SendGridAPIToken(), + rules.SendInBlueAPIToken(), + rules.SentryAccessToken(), + rules.SentryOrgToken(), + rules.SentryUserToken(), + rules.SettlemintApplicationAccessToken(), + rules.SettlemintPersonalAccessToken(), + rules.SettlemintServiceAccessToken(), + rules.ShippoAPIToken(), + rules.ShopifyAccessToken(), + rules.ShopifyCustomAccessToken(), + rules.ShopifyPrivateAppAccessToken(), + rules.ShopifySharedSecret(), + rules.SidekiqSecret(), + rules.SidekiqSensitiveUrl(), + rules.SlackBotToken(), + rules.SlackUserToken(), + rules.SlackAppLevelToken(), + rules.SlackConfigurationToken(), + rules.SlackConfigurationRefreshToken(), + rules.SlackLegacyBotToken(), + rules.SlackLegacyWorkspaceToken(), + rules.SlackLegacyToken(), + rules.SlackWebHookUrl(), + rules.Snyk(), + rules.Sonar(), + rules.SourceGraph(), + rules.StripeAccessToken(), + rules.SquareAccessToken(), + rules.SquareSpaceAccessToken(), + rules.SumoLogicAccessID(), + rules.SumoLogicAccessToken(), + rules.TeamsWebhook(), + rules.TelegramBotToken(), + rules.TravisCIAccessToken(), + rules.Twilio(), + rules.TwitchAPIToken(), + rules.TwitterAPIKey(), + rules.TwitterAPISecret(), + rules.TwitterAccessToken(), + rules.TwitterAccessSecret(), + rules.TwitterBearerToken(), + rules.Typeform(), + rules.VaultBatchToken(), + rules.VaultServiceToken(), + rules.YandexAPIKey(), + rules.YandexAWSAccessToken(), + rules.YandexAccessToken(), + rules.ZendeskSecretKey(), + rules.GenericCredential(), + rules.InfracostAPIToken(), + } + + // ensure rules have unique ids + ruleLookUp := make(map[string]config.Rule, len(configRules)) + for _, rule := range configRules { + if err := rule.Validate(); err != nil { + logging.Fatal().Err(err). + Str("rule-id", rule.RuleID). + Msg("Failed to validate rule") + } + + // check if rule is in ruleLookUp + if _, ok := ruleLookUp[rule.RuleID]; ok { + logging.Fatal(). + Str("rule-id", rule.RuleID). + Msg("rule id is not unique") + } + // TODO: eventually change all the signatures to get ride of this + // nasty dereferencing. + ruleLookUp[rule.RuleID] = *rule + + // Slices are de-duplicated with a map, every iteration has a different order. + // This is an awkward workaround. + for _, allowlist := range rule.Allowlists { + slices.Sort(allowlist.Commits) + slices.Sort(allowlist.StopWords) + } + } + + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + logging.Fatal().Err(err).Msg("Failed to parse template") + } + + f, err := os.Create(gitleaksConfigPath) + if err != nil { + logging.Fatal().Err(err).Msg("Failed to create rules.toml") + } + defer f.Close() + + cfg := base.CreateGlobalConfig() + cfg.Rules = ruleLookUp + for _, allowlist := range cfg.Allowlists { + slices.Sort(allowlist.Commits) + slices.Sort(allowlist.StopWords) + } + if err = tmpl.Execute(f, cfg); err != nil { + logging.Fatal().Err(err).Msg("could not execute template") + } +} diff --git a/cmd/generate/config/rules/1password.go b/cmd/generate/config/rules/1password.go new file mode 100644 index 0000000..fe78072 --- /dev/null +++ b/cmd/generate/config/rules/1password.go @@ -0,0 +1,81 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +// https://developer.1password.com/docs/service-accounts/security/?token-example=encoded +func OnePasswordServiceAccountToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "1password-service-account-token", + Description: "Uncovered a possible 1Password service account token, potentially compromising access to secrets in vaults.", + Regex: regexp.MustCompile(`ops_eyJ[a-zA-Z0-9+/]{250,}={0,3}`), + Entropy: 4, + Keywords: []string{"ops_"}, + } + + // validate + tps := []string{ + utils.GenerateSampleSecret("1password", secrets.NewSecret(`ops_eyJ[a-zA-Z0-9+/]{250,}={0,3}`)), + `### 1Password System Vault Name +export OP_SERVICE_ACCOUNT_TOKEN=ops_eyJzaWduSW5BZGRyZXNzIjoibXkuMXBhc3N3b3JkLmNvbSIsInVzZXJBdXRoIjp7Im1ldGhvZCI6IlNSUGctNDA5NiIsImFsZyI6IlBCRVMyZy1IUzI1NiIsIml0ZXJhdGlvbnMiOjY1MdAwMCwic2FsdCI6InE2dE0tYzNtRDhiNUp2OHh1YVzsUmcifSwiZW1haWwiOiJ5Z3hmcm0zb21oY3NtQDFwYXNzd29yZHNlcnZpY2VhY2NvdW50cy5jb20iLCJzcnBYIjoiM2E5NDdhZmZhMDQ5NTAxZjkxYzk5MGFiY2JiYWRlZjFjMjM5Y2Q3YTMxYmI1MmQyZjUzOTA2Y2UxOTA1OTYwYiIsIm11ayI6eyJhbGciOiJBMjU2R0NNIiwiZXh0Ijp0cnVlLCJrIjoiVVpleERsLVgyUWxpa0VqRjVUUjRoODhOd29ZcHRqSHptQmFTdlNrWGZmZyIsImtleV9vcHMiOlsiZW5jcnlwdCIsImRlY3J5cHQiXSwia3R5Ijoib2N0Iiwia2lkIjoibXAifSwic2VjcmV0S2V5IjoiQTMtNDZGUUVNLUVZS1hTQS1NUU0yUy04U0JSUS01QjZGUC1HS1k2ViIsInRocm90dGxlU2VjcmV0Ijp7InNlZWQiOiJjZmU2ZTU0NGUxZTlmY2NmZjJlYjBhYWZmYTEzNjZlMmE2ZmUwZDVlZGI2ZTUzOTVkZTljZmY0NDY3NDUxOGUxIiwidXVpZCI6IjNVMjRMNVdCNkpFQ0pEQlhJNFZOSTRCUzNRIn0sImRldmljZVV1aWQiOiJqaGVlY3F4cm41YTV6ZzRpMnlkbjRqd3U3dSJ9 +`, + `PYTEST_SVC_ACCT_TOKEN=ops_eyJzaWduSW5BZGRyZXNzIjoiemFjaC1hbmQtbGVhbm5lLjFwYXNzd29yZC5jb20iLCJ1c2VyQXV0aCI6eyJtZXRob2QiOiJTUlBnLTQwOTYiLCJhbGciOiJQQkVLMmctSFMyNTYiLCJpdGVyYXRpb25zIjo2NTAwMDAsInNhbHQiOiJlYUZRQmNVemJyTHhnM2d4bHFQLVVBIn0sImVtYWlsIjoiMm9iNGRpeDdiNTdrYUAxcGFzc3dvcmRzZXJ2aWNlYWNjb3VudHMuY29tIiwic3JwWCI6ImVmZDY4YjNhZTkwMmRjZjRiMzEzYjE5MjYwZmY0OGUzMjU2ZDlhOGNkM2JmMmY3YzI2YzU1ZWJkNjZlZGU4NWEiLCJtdWsiOnsiYWxnIjoiQTI1NkdDTSIsImV4dCI6dHJ1ZSwiayI6IlMwaGE0SDhqbEhRblJCWmxvYnBmR1BneERmbS1pRGNkZWY0bFdYU0VSbmMiLCJrZXlfb3BzIjpbImRlY3J5cHQiLCJlbmNyeXB0Il0sImt0eSI6Im9jdCIsImtpZCI6Im1wIn0sInNlY3JldEtleSI6IkEzLUdHOUVRNi1LUzQ0QVctQU5QVkYtUkdQTDktQlNKUTMtR1NHR0giLCJ0aHJvdHRsZVNlY3JldCI6eyJzZWVkIjoiN2I0OTMxMmJiOTlkZTFiNjU5ODZkYzIzOWU4YWNmZWMxMTU0M2E2OGQxYmYwMjZmZTgzMjg3NWYxNmJlOWY2NiIsInV1aWQiOiJDV1RHQ0hMNlNWRkdSTlg0SzNENUJVSDZDSSJ9LCJkZXZpY2VVdWlkIjoiMnFld3JpaGtqbmt1Zmh6ZGdmZ2hnNmM1cGUifQ`, + ` "sourceContent": "ops_eyJlbWFpbCI6ImVqd2U2NHFtbHhocmlAMXBhc3N3b3Jkc2VydmljZWFjY291bnRzLmxjbCIsIm11ayI6eyJhbGciOiJBMjU2R0NNIiwiZXh0Ijp0cnVlLCJrIjoiTThWUGZJYzhWRWZUaGNNWExhS0NLRjhzTWg1Sk1ac1BBdHU5MmZRTmItbyIsImtleV9vcHMiOlsiZW5jcnlwdCIsImRlY3J5cHQiXSwia3R5Ijoib2N0Iiwia2lkIjoibXAifSwic2VjcmV0S2V5IjoiQTMtQzRaSk1OLVBRVFpUTC1IR0w4NC1HNjRNNy1LVlpSTi00WlZQNiIsInNycFgiOiI4NzBkNjdhOWU2MjY2MjVkOWUzNjg1MDc4MDRjOWMzMmU2NjFjNTdlN2U1NTg3NzgyOTFiZjI5ZDVhMjc5YWUxIiwic2lnbkluQWRkcmVzcyI6ImdvdGhhbS5iNWxvY2FsLmNvbTo0MDAwIiwidXNlckF1dGgiOnsibWV0aG9kIjoiU1JQZy00MDk2IiwiYWxnIjoiUEJFUzJnLUhTMjU2IiwiaXRlcmF0aW9ucyI6MTAwMDAwLCJzYWx0IjoiRk1SVVBpeXJONFhmXzhIb2g2WVJYUSJ9fQ\n\nops_token is secret.\n",`, + } + fps := []string{ + // Invalid + ` login: + serviceAccountToken: + fn::secret: ops_eyJzaWduSW5B..[Redacted]`, + `: To start using this service account, run the following command: +: +: export OP_SERVICE_ACCOUNT_TOKEN=ops_eyJzaWduSW5BZGRyZXNzIjoiaHR0cHM6...`, + // Low entropy. + `ops_eyJxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`, + } + return utils.Validate(r, tps, fps) +} + +// Reference: +// - https://1passwordstatic.com/files/security/1password-white-paper.pdf +func OnePasswordSecretKey() *config.Rule { + // 1Password secret keys include several hyphens but these are only for readability + // and are stripped during 1Password login. This means that the following are technically + // the same valid key: + // - A3ASWWYB798JRYLJVD423DC286TVMH43EB + // - A-3-A-S-W-W-Y-B-7-9-8-J-R-Y-L-J-V-D-4-2-3-D-C-2-8-6-T-V-M-H-4-3-E-B + // But in practice, when these keys are added to a vault, exported in an emergency kit, or + // copied, they have hyphens that follow one of two patterns I can find: + // - A3-ASWWYB-798JRY-LJVD4-23DC2-86TVM-H43EB (every key I've generated has this pattern) + // - A3-ASWWYB-798JRYLJVD4-23DC2-86TVM-H43EB (the whitepaper includes this example, which could just be a typo) + // To avoid a complicated regex that checks for every possible situation it's probably best + // to scan for the these two patterns. + r := config.Rule{ + Description: "Uncovered a possible 1Password secret key, potentially compromising access to secrets in vaults.", + RuleID: "1password-secret-key", + Regex: regexp.MustCompile(`\bA3-[A-Z0-9]{6}-(?:(?:[A-Z0-9]{11})|(?:[A-Z0-9]{6}-[A-Z0-9]{5}))-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}\b`), + Entropy: 3.8, + Keywords: []string{"A3-"}, + } + + // validate + tps := utils.GenerateSampleSecrets("1password", secrets.NewSecret(`A3-[A-Z0-9]{6}-[A-Z0-9]{11}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}`)) + tps = append(tps, utils.GenerateSampleSecrets("1password", secrets.NewSecret(`A3-[A-Z0-9]{6}-[A-Z0-9]{6}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}`))...) + tps = append(tps, + // from whitepaper + `A3-ASWWYB-798JRYLJVD4-23DC2-86TVM-H43EB`, + `A3-ASWWYB-798JRY-LJVD4-23DC2-86TVM-H43EB`, + ) + fps := []string{ + // low entropy + `A3-XXXXXX-XXXXXXXXXXX-XXXXX-XXXXX-XXXXX`, + // lowercase + `A3-xXXXXX-XXXXXX-XXXXX-XXXXX-XXXXX-XXXXX`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/adafruit.go b/cmd/generate/config/rules/adafruit.go new file mode 100644 index 0000000..b769205 --- /dev/null +++ b/cmd/generate/config/rules/adafruit.go @@ -0,0 +1,21 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func AdafruitAPIKey() *config.Rule { + // define rule + r := config.Rule{ + Description: "Identified a potential Adafruit API Key, which could lead to unauthorized access to Adafruit services and sensitive data exposure.", + RuleID: "adafruit-api-key", + Regex: utils.GenerateSemiGenericRegex([]string{"adafruit"}, utils.AlphaNumericExtendedShort("32"), true), + Keywords: []string{"adafruit"}, + } + + // validate + tps := utils.GenerateSampleSecrets("adafruit", secrets.NewSecret(utils.AlphaNumericExtendedShort("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/adobe.go b/cmd/generate/config/rules/adobe.go new file mode 100644 index 0000000..1287b04 --- /dev/null +++ b/cmd/generate/config/rules/adobe.go @@ -0,0 +1,39 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func AdobeClientID() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "adobe-client-id", + Description: "Detected a pattern that resembles an Adobe OAuth Web Client ID, posing a risk of compromised Adobe integrations and data breaches.", + Regex: utils.GenerateSemiGenericRegex([]string{"adobe"}, utils.Hex("32"), true), + Entropy: 2, + Keywords: []string{"adobe"}, + } + + // validate + tps := utils.GenerateSampleSecrets("adobe", secrets.NewSecret(utils.Hex("32"))) + return utils.Validate(r, tps, nil) +} + +func AdobeClientSecret() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "adobe-client-secret", + Description: "Discovered a potential Adobe Client Secret, which, if exposed, could allow unauthorized Adobe service access and data manipulation.", + Regex: utils.GenerateUniqueTokenRegex(`p8e-(?i)[a-z0-9]{32}`, false), + Entropy: 2, + Keywords: []string{"p8e-"}, + } + + // validate + tps := []string{ + "adobeClient := \"p8e-" + secrets.NewSecret(utils.Hex("32")) + "\"", + } + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/age.go b/cmd/generate/config/rules/age.go new file mode 100644 index 0000000..6908aa7 --- /dev/null +++ b/cmd/generate/config/rules/age.go @@ -0,0 +1,21 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func AgeSecretKey() *config.Rule { + // define rule + r := config.Rule{ + Description: "Discovered a potential Age encryption tool secret key, risking data decryption and unauthorized access to sensitive information.", + RuleID: "age-secret-key", + Regex: regexp.MustCompile(`AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}`), + Keywords: []string{"AGE-SECRET-KEY-1"}, + } + + // validate + tps := utils.GenerateSampleSecrets("age", `AGE-SECRET-KEY-1QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ`) // gitleaks:allow + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/airtable.go b/cmd/generate/config/rules/airtable.go new file mode 100644 index 0000000..354ad19 --- /dev/null +++ b/cmd/generate/config/rules/airtable.go @@ -0,0 +1,37 @@ +package rules + +import ( + "regexp" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func AirtableApiKey() *config.Rule { + // define rule + r := config.Rule{ + Description: "Uncovered a possible Airtable API Key, potentially compromising database access and leading to data leakage or alteration.", + RuleID: "airtable-api-key", + Regex: utils.GenerateSemiGenericRegex([]string{"airtable"}, utils.AlphaNumeric("17"), true), + Keywords: []string{"airtable"}, + } + + // validate + tps := utils.GenerateSampleSecrets("airtable", secrets.NewSecret(utils.AlphaNumeric("17"))) + return utils.Validate(r, tps, nil) +} + +func AirtablePersonalAccessToken() *config.Rule { + // define rule + r := config.Rule{ + Description: "Uncovered a possible Airtable Personal AccessToken, potentially compromising database access and leading to data leakage or alteration.", + RuleID: "airtable-personnal-access-token", + Regex: regexp.MustCompile(`\b(pat[[:alnum:]]{14}\.[a-f0-9]{64})\b`), + Keywords: []string{"airtable"}, + } + + // validate + tps := utils.GenerateSampleSecrets("airtable", "pat"+secrets.NewSecret(utils.AlphaNumeric("14")+"\\."+utils.Hex("64"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/algolia.go b/cmd/generate/config/rules/algolia.go new file mode 100644 index 0000000..03b5058 --- /dev/null +++ b/cmd/generate/config/rules/algolia.go @@ -0,0 +1,21 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func AlgoliaApiKey() *config.Rule { + // define rule + r := config.Rule{ + Description: "Identified an Algolia API Key, which could result in unauthorized search operations and data exposure on Algolia-managed platforms.", + RuleID: "algolia-api-key", + Regex: utils.GenerateSemiGenericRegex([]string{"algolia"}, `[a-z0-9]{32}`, true), + Keywords: []string{"algolia"}, + } + + // validate + tps := utils.GenerateSampleSecrets("algolia", secrets.NewSecret(utils.Hex("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/alibaba.go b/cmd/generate/config/rules/alibaba.go new file mode 100644 index 0000000..67d5739 --- /dev/null +++ b/cmd/generate/config/rules/alibaba.go @@ -0,0 +1,40 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func AlibabaAccessKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "alibaba-access-key-id", + Description: "Detected an Alibaba Cloud AccessKey ID, posing a risk of unauthorized cloud resource access and potential data compromise.", + Regex: utils.GenerateUniqueTokenRegex(`LTAI(?i)[a-z0-9]{20}`, false), + Entropy: 2, + Keywords: []string{"LTAI"}, + } + + // validate + tps := []string{ + "alibabaKey := \"LTAI" + secrets.NewSecret(utils.Hex("20")) + "\"", + } + return utils.Validate(r, tps, nil) +} + +// TODO +func AlibabaSecretKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "alibaba-secret-key", + Description: "Discovered a potential Alibaba Cloud Secret Key, potentially allowing unauthorized operations and data access within Alibaba Cloud.", + Regex: utils.GenerateSemiGenericRegex([]string{"alibaba"}, utils.AlphaNumeric("30"), true), + Entropy: 2, + Keywords: []string{"alibaba"}, + } + + // validate + tps := utils.GenerateSampleSecrets("alibaba", secrets.NewSecret(utils.AlphaNumeric("30"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/anthropic.go b/cmd/generate/config/rules/anthropic.go new file mode 100644 index 0000000..a3d956e --- /dev/null +++ b/cmd/generate/config/rules/anthropic.go @@ -0,0 +1,69 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func AnthropicApiKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "anthropic-api-key", + Description: "Identified an Anthropic API Key, which may compromise AI assistant integrations and expose sensitive data to unauthorized access.", + Regex: utils.GenerateUniqueTokenRegex(`sk-ant-api03-[a-zA-Z0-9_\-]{93}AA`, false), + Keywords: []string{ + "sk-ant-api03", + }, + } + + // validate + tps := []string{ + // Valid API key example + "sk-ant-api03-abc123xyz-456def789ghij-klmnopqrstuvwx-3456yza789bcde-1234fghijklmnopby56aaaogaopaaaabc123xyzAA", + // Generate additional random test keys + utils.GenerateSampleSecret("anthropic", "sk-ant-api03-"+secrets.NewSecret(utils.AlphaNumericExtendedShort("93"))+"AA"), + } + + fps := []string{ + // Too short key (missing characters) + "sk-ant-api03-abc123xyz-456de-klMnopqrstuvwx-3456yza789bcde-1234fghijklmnopAA", + // Wrong suffix + "sk-ant-api03-abc123xyz-456def789ghij-klmnopqrstuvwx-3456yza789bcde-1234fghijklmnopby56aaaogaopaaaabc123xyzBB", + // Wrong prefix (admin key, not API key) + "sk-ant-admin01-abc123xyz-456def789ghij-klmnopqrstuvwx-3456yza789bcde-1234fghijklmnopby56aaaogaopaaaabc123xyzAA", + } + + return utils.Validate(r, tps, fps) +} + +func AnthropicAdminApiKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "anthropic-admin-api-key", + Description: "Detected an Anthropic Admin API Key, risking unauthorized access to administrative functions and sensitive AI model configurations.", + Regex: utils.GenerateUniqueTokenRegex(`sk-ant-admin01-[a-zA-Z0-9_\-]{93}AA`, false), + Keywords: []string{ + "sk-ant-admin01", + }, + } + + // validate + tps := []string{ + // Valid admin key example + "sk-ant-admin01-abc12fake-456def789ghij-klmnopqrstuvwx-3456yza789bcde-12fakehijklmnopby56aaaogaopaaaabc123xyzAA", + // Generate additional random test keys + utils.GenerateSampleSecret("anthropic", "sk-ant-admin01-"+secrets.NewSecret(utils.AlphaNumericExtendedShort("93"))+"AA"), + } + + fps := []string{ + // Too short key (missing characters) + "sk-ant-admin01-abc123xyz-456de-klMnopqrstuvwx-3456yza789bcde-1234fghijklmnopAA", + // Wrong suffix + "sk-ant-admin01-abc123xyz-456def789ghij-klmnopqrstuvwx-3456yza789bcde-1234fghijklmnopby56aaaogaopaaaabc123xyzBB", + // Wrong prefix (API key, not admin key) + "sk-ant-api03-abc123xyz-456def789ghij-klmnopqrstuvwx-3456yza789bcde-1234fghijklmnopby56aaaogaopaaaabc123xyzAA", + } + + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/artifactory.go b/cmd/generate/config/rules/artifactory.go new file mode 100644 index 0000000..63c5ec8 --- /dev/null +++ b/cmd/generate/config/rules/artifactory.go @@ -0,0 +1,58 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func ArtifactoryApiKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "artifactory-api-key", + Description: "Detected an Artifactory api key, posing a risk unauthorized access to the central repository.", + Regex: regexp.MustCompile(`\bAKCp[A-Za-z0-9]{69}\b`), + Entropy: 4.5, + Keywords: []string{"AKCp"}, + } + + // validate + tps := []string{ + "artifactoryApiKey := \"AKCp" + secrets.NewSecret(utils.AlphaNumeric("69")) + "\"", + } + // false positives + fps := []string{ + `lowEntropy := AKCpXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`, + "wrongStart := \"AkCp" + secrets.NewSecret(utils.AlphaNumeric("69")) + "\"", + "wrongLength := \"AkCp" + secrets.NewSecret(utils.AlphaNumeric("59")) + "\"", + "partOfAlongUnrelatedBlob gYnkgAkCp" + secrets.NewSecret(utils.AlphaNumeric("69")) + "VyZSB2", + } + + return utils.Validate(r, tps, fps) +} + +func ArtifactoryReferenceToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "artifactory-reference-token", + Description: "Detected an Artifactory reference token, posing a risk of impersonation and unauthorized access to the central repository.", + Regex: regexp.MustCompile(`\bcmVmd[A-Za-z0-9]{59}\b`), + Entropy: 4.5, + Keywords: []string{"cmVmd"}, + } + + // validate + tps := []string{ + "artifactoryRefToken := \"cmVmd" + secrets.NewSecret(utils.AlphaNumeric("59")) + "\"", + } + // false positives + fps := []string{ + `lowEntropy := cmVmdXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`, + "wrongStart := \"cmVMd" + secrets.NewSecret(utils.AlphaNumeric("59")) + "\"", + "wrongLength := \"cmVmd" + secrets.NewSecret(utils.AlphaNumeric("49")) + "\"", + "partOfAlongUnrelatedBlob gYnkgcmVmd" + secrets.NewSecret(utils.AlphaNumeric("59")) + "VyZSB2", + } + + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/asana.go b/cmd/generate/config/rules/asana.go new file mode 100644 index 0000000..def28ce --- /dev/null +++ b/cmd/generate/config/rules/asana.go @@ -0,0 +1,36 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func AsanaClientID() *config.Rule { + // define rule + r := config.Rule{ + Description: "Discovered a potential Asana Client ID, risking unauthorized access to Asana projects and sensitive task information.", + RuleID: "asana-client-id", + Regex: utils.GenerateSemiGenericRegex([]string{"asana"}, utils.Numeric("16"), true), + Keywords: []string{"asana"}, + } + + // validate + tps := utils.GenerateSampleSecrets("asana", secrets.NewSecret(utils.Numeric("16"))) + return utils.Validate(r, tps, nil) +} + +func AsanaClientSecret() *config.Rule { + // define rule + r := config.Rule{ + Description: "Identified an Asana Client Secret, which could lead to compromised project management integrity and unauthorized access.", + RuleID: "asana-client-secret", + Regex: utils.GenerateSemiGenericRegex([]string{"asana"}, utils.AlphaNumeric("32"), true), + + Keywords: []string{"asana"}, + } + + // validate + tps := utils.GenerateSampleSecrets("asana", secrets.NewSecret(utils.AlphaNumeric("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/atlassian.go b/cmd/generate/config/rules/atlassian.go new file mode 100644 index 0000000..ba8b4cb --- /dev/null +++ b/cmd/generate/config/rules/atlassian.go @@ -0,0 +1,36 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func Atlassian() *config.Rule { + // define rule + r := config.Rule{ + Description: "Detected an Atlassian API token, posing a threat to project management and collaboration tool security and data confidentiality.", + RuleID: "atlassian-api-token", + Regex: utils.MergeRegexps( + utils.GenerateSemiGenericRegex( + []string{"(?-i:ATLASSIAN|[Aa]tlassian)", "(?-i:CONFLUENCE|[Cc]onfluence)", "(?-i:JIRA|[Jj]ira)"}, + `[a-z0-9]{20}[a-f0-9]{4}`, // The last 4 characters are an MD5 hash. + true, + ), + utils.GenerateUniqueTokenRegex(`ATATT3[A-Za-z0-9_\-=]{186}`, false), + ), + Entropy: 3.5, + Keywords: []string{"atlassian", "confluence", "jira", "atatt3"}, + } + + // validate + tps := utils.GenerateSampleSecrets("atlassian", secrets.NewSecret(utils.AlphaNumeric("20")+"[a-f0-9]{4}")) + tps = append(tps, utils.GenerateSampleSecrets("confluence", secrets.NewSecret(utils.AlphaNumeric("20")+"[a-f0-9]{4}"))...) + tps = append(tps, utils.GenerateSampleSecrets("jira", secrets.NewSecret(utils.AlphaNumeric("20")+"[a-f0-9]{4}"))...) + tps = append(tps, `JIRA_API_TOKEN=HXe8DGg1iJd2AopzyxkFB7F2`) + tps = append(tps, utils.GenerateSampleSecrets("jira", "ATATT3xFfGF0K3irG5tKKi-6u-wwaXQFeGwZ-IHR-hQ3CulkKtMSuteRQFfLZ6jihHThzZCg_UjnDt-4Wl_gIRf4zrZJs5JqaeuBhsfJ4W5GD6yGg3W7903gbvaxZPBjxIQQ7BgFDSkPS8oPispw4KLz56mdK-G6CIvLO6hHRrZHY0Q3tvJ6JxE=C63992E6")...) + + fps := []string{"getPagesInConfluenceSpace,searchConfluenceUsingCql"} + + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/authress.go b/cmd/generate/config/rules/authress.go new file mode 100644 index 0000000..c304332 --- /dev/null +++ b/cmd/generate/config/rules/authress.go @@ -0,0 +1,31 @@ +package rules + +import ( + "fmt" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func Authress() *config.Rule { + // Rule Definition + // (Note: When changes are made to this, rerun `go generate ./...` and commit the config/gitleaks.toml file + r := config.Rule{ + RuleID: "authress-service-client-access-key", + Description: "Uncovered a possible Authress Service Client Access Key, which may compromise access control services and sensitive data.", + Regex: utils.GenerateUniqueTokenRegex(`(?:sc|ext|scauth|authress)_(?i)[a-z0-9]{5,30}\.[a-z0-9]{4,6}\.(?-i:acc)[_-][a-z0-9-]{10,32}\.[a-z0-9+/_=-]{30,120}`, false), + Entropy: 2, + Keywords: []string{"sc_", "ext_", "scauth_", "authress_"}, + } + + // validate + // https://authress.io/knowledge-base/docs/authorization/service-clients/secrets-scanning/#1-detection + service_client_id := "sc_" + utils.AlphaNumeric("10") + access_key_id := utils.AlphaNumeric("4") + account_id := "acc_" + utils.AlphaNumeric("10") + signature_key := utils.AlphaNumericExtendedShort("40") + + tps := utils.GenerateSampleSecrets("authress", secrets.NewSecret(fmt.Sprintf(`%s\.%s\.%s\.%s`, service_client_id, access_key_id, account_id, signature_key))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/aws.go b/cmd/generate/config/rules/aws.go new file mode 100644 index 0000000..545fd68 --- /dev/null +++ b/cmd/generate/config/rules/aws.go @@ -0,0 +1,109 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func AWS() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "aws-access-token", + Description: "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.", + Regex: regexp.MustCompile(`\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\b`), + Entropy: 3, + Keywords: []string{ + // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-unique-ids + "A3T", // todo: might not be a valid AWS token + "AKIA", // Access key + "ASIA", // Temporary (AWS STS) access key + "ABIA", // AWS STS service bearer token + "ACCA", // Context-specific credential + }, + Allowlists: []*config.Allowlist{ + { + Regexes: []*regexp.Regexp{ + regexp.MustCompile(`.+EXAMPLE$`), + }, + }, + }, + } + + // validate + tps := utils.GenerateSampleSecrets("AWS", "AKIALALEMEL33243OLIB") // gitleaks:allow + // current AWS tokens cannot contain [0,1,8,9], so their entropy is slightly lower than expected. + tps = append(tps, utils.GenerateSampleSecrets("AWS", "AKIA"+secrets.NewSecret("[A-Z2-7]{16}"))...) + tps = append(tps, utils.GenerateSampleSecrets("AWS", "ASIA"+secrets.NewSecret("[A-Z2-7]{16}"))...) + tps = append(tps, utils.GenerateSampleSecrets("AWS", "ABIA"+secrets.NewSecret("[A-Z2-7]{16}"))...) + tps = append(tps, utils.GenerateSampleSecrets("AWS", "ACCA"+secrets.NewSecret("[A-Z2-7]{16}"))...) + fps := []string{ + `key = AKIAXXXXXXXXXXXXXXXX`, // Low entropy + `aws_access_key: AKIAIOSFODNN7EXAMPLE`, // Placeholder + `msgstr "Näytä asiakirjamallikansio."`, // Lowercase + `TODAYINASIAASACKOFRICEFELLOVER`, // wrong length + `CTTCATAGGGTTCACGCTGTGTAAT-ACG--CCTGAGGC-CACA-AGGGGACTTCAGCAACCGTCGGG-GATTC-ATTGCCA-A--TGGAAGCAATC-TA-TGGGTTA-TCGCGGAGTCCGCAAAGACGGCCAGTATG-AAGCAGATTTCGCAC-CAATGTGACTGCATTTCGTG-ATCGGGGTAAGTA-TC-GCCGATTC-GC--CCGTCCA-AGT-CGAAG-TA--GGCAATATAAAGCTGC-CATTGCCGAAGCTATCTCGCTA-TACTTGAT-AATCGGCGG-TAG-CACAG-GTCGCAGTATCG-AC-T--AGG-CCTCTCAAAAGTT-GGGTCCCGGCCTCTGGGAAAAACACCTCT-A-AGCGTCAATCAGCTCGGTTTCGCATATTA-TGATATCCCCCGTTGACCAATTGA--TAGTACCCGAGCTTACCGTCGG-ATTCTGGAGTCTT-ATGAGGTTACCGACGA-CGCAGTACCATAAGT-GCGCAATTTGACTGTTCCCGTCGAGTAACCA-AGCTTTGCTCA-CCGGGATGCGCGCCGATGTGACCAGGGGGCGCATGTTACATTGAC-A-GCTGGATCATGTTATGAC-GTGGGTC-ATGCTAAAAGCCTAAAGGACGGT-GCATTAGTAT-TACCGGGACCTCATATCAATGCGCTCGCTAGTTCCTCTTCTCTTGATAACGTATATGCGTCAGGCGCCCGTCCGCCTCCAATACGTG-ACAACGTC-AGTACTGAGCCTC--AA-ACATCGTCTTGTTCG-CC-TACAAAGGATCGGTAGAAAACTCAATATTCGGGTATAAGGTCGTAGGAAGTGTGTCGCCCAGGGCCG-CTAGA-AGCGCACACAAGCG-CTCCTGTCAAGGAGTTG-GTGAAAA-ATGAAC--GACT-ATTGCGTCAC--CTACCTCT-AAGTTTTT-GACAATTTCATGGACGAATTGA-AGCGTCCACAAGCATCTGCCGTAGATATGCGGTAGGTTTTTACATATG-TCACTGCAGAGTCACGGACA-CACATCGCTGTCAAAATGCTCGTACCTAGT-GT-TTGCGATCCCCC-GCGGCATTA-TCTTTTGAACCCTCGTCCCTGTGG-CTCTGATGATTGAG-GTCTGTA-TTCCCTCGTTGTGGGGGGATTGGACCTT-TGTATAGGTTCTTTAACCG-ATGGGGGGCCG--ATCGA-A-TA-TGCTCCTGTTTGCCCCGAACCTT-ACCTCGG-TCCAGACA-CTAAGAAAAACCCC-C-ACTGTAAGGTGCTGAGCCTTTGGATAGCC-CGCGAATGAT-CC-TAGTTGACAA-CTGAACGCGCTCGAACA-TGCCC-GCCCTCTGA--CTGCTGTCTG-GCACCTTTAGACACGCGTCGAC-CATATATT-AGCGCTGTCTGTGG-AGGT-TGTGTCTTGTTGCTCA-CT-CATTATCTGT-AACTGGCTCC-CTC-CCAT-TGGCGTCTTTACACCAACCGCTAGGTTACAGTGCA-TCTAGCGCCTATTATCAGGGCGT-TTGCAGCGGCGCGGTGGCTATGT-GTTAGACATATC-CTTACACTGTATGCTAG-AGCAAGCCAC-TCTGAATGGGTTGC-CGATGAATGA-TCTTGATC-GAGCTCGCA-AC---TACATGGAGTCCGAAGTGAACCTACGGATGATCGTATTCCAACACGAGGATC-TATACGTATAGG-A-GGCG-TAATCCACAATTTAGTAACTCTTGACGC---GGATGAAAAT-GTCGTTACACCTTCCAGAGGCTCGG-GTATATATATGACCT--TGTGATTGAGGACGATCTAGAATAA-CT-GT-G-CT-AAAGTACAGTAGTTTCTATGT-GGTAGGTGGAGAATACAGAGTAG-ATGATTC-GTGGGCCACA-C--T-ACTTTCAT-TAGAGCAGAGA-C-GTGAGTGAGTTTTACACTAGCCAGATGGACCG-GTGA-AGTCTAACAGCCACCGCTT-GTGAGGTCGTTTCCCAGTC-ACCCTACTACAGGCAAAAACTCAGTGT-CC-GTGA-GTGCGTTAGTGATATTCCCTAACGGTTAGGTAACT-CATGAATTCA-AT-TAAGCGTGTCC-CGGT-CACGCCCCCATGGGGGCCTTCTTGGGAGG--AGCATCTTAT--AT-GCTCACGTGGTT-GATAGG-A-T-AATACACTTTTAGTCAGTCCATCAATAAC-AAAGGAAC---CAGGTGGTCGCAGATA-TCCCGCTGATATAGCACTGTGTAAACTCAGGTGATA-CTAAGC--GCTCTAAT-ACG-CTTAATGGCAATGCCCAGTTC--ACGACTAGCTTATGAGGCCCAGCTATGGACTGCGGC-GGCATGTCGGC-GATGGTTGCCCTCGCCCTAAATTATGTACGA-T-ACCGCCT-CTTGTTCT-CCGCCCATAGGGT-C--AGCAGGCGATAGACTCCCAGAAATTTCCTCGTCGT-CCGAATAAGACTAACACGACTA-TT-CCTCTAC-GT-G-AA-CTTATCA-CAAATG-GCT-TACC-TAGGTGGTGGCAGATCACTTTCCGGTG-TATTACGAATTGACGCATACCGAC-A-CGC-GCTTGTTGGATAATCGACTCTAACCTCCTCTCTGGCACATGT-GCTGGATTACCTC-TATTTT-TCTCGCTTAG--GGAACG-T-CCTCTGTCGCGTGAG-GTACGTTTCACGGGAG-CGGCTTGTTCATGCCACGTCCATTATCGA-AGTG-C-GTAAGG-A-GAGCCCTA--GACTCTACACGGAAA-TC-AAC-GTAGAAGGCTC-A-CT`, + } + return utils.Validate(r, tps, fps) +} + +func AmazonBedrockAPIKeyLongLived() *config.Rule { + // https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys-how.html + // https://medium.com/@adan.alvarez/api-keys-for-bedrock-a-brief-security-overview-2133ed9a2b3f + r := config.Rule{ + RuleID: "aws-amazon-bedrock-api-key-long-lived", + Description: "Identified a pattern that may indicate long-lived Amazon Bedrock API keys, risking unauthorized Amazon Bedrock usage", + Regex: utils.GenerateUniqueTokenRegex(`ABSK[A-Za-z0-9+/]{109,269}={0,2}`, false), + Entropy: 3, + Keywords: []string{ + "ABSK", // Amazon Bedrock API Key (long-lived) + }, + } + + // validate + tps := []string{ + // Valid API key example + "ABSKQmVkcm9ja0FQSUtleS1EXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXM=", + // Generate additional random test keys + utils.GenerateSampleSecret("bedrock", "ABSKQmVkcm9ja0FQSUtleS1"+secrets.NewSecret(utils.AlphaNumeric("108"))+"="), + utils.GenerateSampleSecret("bedrock", "ABSKQmVkcm9ja0FQSUtleS1"+secrets.NewSecret(utils.AlphaNumeric("246"))), + } + + fps := []string{ + // Too short key (missing characters) + "ABSKQmVkcm9ja0FQSUtleS1EXAMPLE", + // Too long + "ABSKQmVkcm9ja0FQSUtleS1EXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLE=", + // Wrong prefix + "AXSKQmVkcm9ja0FQSUtleS1EXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXM=", + } + + return utils.Validate(r, tps, fps) +} + +func AmazonBedrockAPIKeyShortLived() *config.Rule { + // https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys-how.html + // https://github.com/aws/aws-bedrock-token-generator-js/blob/86277e1489354192c64ffc8f995601daacc1f715/src/token.ts#L21 + r := config.Rule{ + RuleID: "aws-amazon-bedrock-api-key-short-lived", + Description: "Identified a pattern that may indicate short-lived Amazon Bedrock API keys, risking unauthorized Amazon Bedrock usage", + Regex: regexp.MustCompile(`bedrock-api-key-YmVkcm9jay5hbWF6b25hd3MuY29t`), + Entropy: 3, + Keywords: []string{ + "bedrock-api-key-", // Amazon Bedrock API Key (short lived) + }, + } + + // validate + tps := utils.GenerateSampleSecrets("AmazonBedrockAPIKeyShortLived", `bedrock-api-key-YmVkcm9jay5hbWF6b25hd3MuY29t`) + + fps := []string{ + // Too short key (missing characters) + "bedrock-api-key-", + // Wrong prefix + "bedrock-api-key-YmVkcm9jay5hbWF6b25hd3MuY29x", + } + + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/azure.go b/cmd/generate/config/rules/azure.go new file mode 100644 index 0000000..8537856 --- /dev/null +++ b/cmd/generate/config/rules/azure.go @@ -0,0 +1,65 @@ +package rules + +import ( + "fmt" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +// References: +// - https://learn.microsoft.com/en-us/microsoft-365/compliance/sit-defn-azure-ad-client-secret +// - https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#add-credentials +func AzureActiveDirectoryClientSecret() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "azure-ad-client-secret", + Description: "Azure AD Client Secret", + // After inspecting dozens of secrets, I'm fairly confident that they start with `xxx\dQ~`. + // However, this may not be (entirely) true, and this rule might need to be further refined in the future. + // Furthermore, it's possible that secrets have a checksum that could be used to further constrain this pattern. + Regex: regexp.MustCompile(`(?:^|[\\'"\x60\s>=:(,)])([a-zA-Z0-9_~.]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\\'"\x60\s<),])`), // wtf, Go? https://github.com/golang/go/issues/18221 + Entropy: 3, + Keywords: []string{ + "Q~", + }, + } + + // validate + tps := []string{ + `client_secret=bP88Q~rcBcYjzzOhg1Hnn76Wm3jGgakZiZ.8vMgR`, // gitleaks:allow + `client_secret=bP88Q~rcBcYjzzOhg1Hnn76Wm3jGgakZiZ.8vMgR +`, // gitleaks:allow + `client_secret: .IQ8Q~79R7TOWOspFnWcEG-dYt4KXqFqxK16cxr`, // gitleaks:allow + `AUTH_CLIENTSECRET = _V28Q~IC8qxmlWNpHuDm34JlbKv9LXV5MvUR3a-P`, // gitleaks:allow + `~Gg8Q~nVhlLi2vpg_nXBGqFsbGK-t~Hus1JmTa0y`, // gitleaks:allow + `"CLIENT_SECRET": "YYz7Q~Sudoqwap1PnzEBA3zqBK~i5uesDIv.C"`, // gitleaks:allow + `Set-PSUAuthenticationMethod -Type 'OpenIDConnect' -CallbackPath '/auth/oidc' -ClientId 'fake' -ClientSecret '2Vq7Q~q5VgKljZ7cb3.0sp0Apz.vOjRIPyeTr'`, // gitleaks:allow + `client-secret: "t028Q~-aLbmQuinnZtzbgtlEAYstnBWEmGPAoBm"`, // gitleaks:allow + `"cas.authn.azure-active-directory.client-secret=qHF8Q~PCM5HhMoyTFc5TYEomnzR6Kim9UJhe8a.P",`, // gitleaks:allow + `"line": "client_srt = \"qpF8Q~PCM5MhMoyTFc5TYEomnYRUKim9UJhe8a2P\";",`, // gitleaks:allow + `"client_secret": acctest.Representation{RepType: acctest.Required, Create: 'dO29Q~F5-VwnW.lZdd11xFF_t5NAXCaGwDl9NbT1'},`, // gitleaks:allow + `Example= GN.7Q~4AkLZBNEbz4Jxlm~O5G6SsyFxYg6zMR`, // gitleaks:allow + `"the_value": "QtT8Q~9C-_Ij~RouHVpD2Tuf3oHWGh.DQ3kcjbAn"`, // gitleaks:allow + `QtT8Q~9C-_Ij~RouHVpD2Tuf3oHWGh.DQ3kcjbAn`, // gitleaks:allow + `(use the client secret: QtT8Q~9C-_Ij~RouHVpD2Tuf3oHWGh.DQ3kcjbAn)`, // gitleaks:allow + `(QtT8Q~9C-_Ij~RouHVpD2Tuf3oHWGh.DQ3kcjbAn)`, // gitleaks:allow + `\"pass\": \"` + fmt.Sprintf("%s%sQ~%s", secrets.NewSecret(`[\w~.]{3}`), secrets.NewSecret(utils.Numeric("1")), secrets.NewSecret(`[\w~.-]{31,34}`)), + } + fps := []string{ + `![图源:《深入拆解Tomcat & Jetty》](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6a9e704af49b4380bb686f0c96d33b81~tplv-k3u1fbpfcp-watermark.image)`, + `~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~`, + `~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~`, + `.ui.visible.right.sidebar~.ui.visible.left.sidebar~.pusher{transform:translate3d(0,0,0)}`, + `buf.WriteString("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n::\n\n")`, + `'url': 'http://www.2doc.nl/speel~VARA_101375237~mh17-het-verdriet-van-nederland~.html',`, + `@ar = split("~", "965f1453~47~09414c93e4cef985416f472549220827da3ba6fed8ad28e29ef6ad170ad53a69051e9b06f439ef6da5df8670181f7eb2481650");`, + `'#!/registries/5/portainer.demo~2Fportainerregistrytesting~2Falpine',`, + `// CloudFront-Signature: Ixn4bF1LLrLcB8XG-t5bZbIB0vfwSF2s4gkef~PcNBdx73MVvZD3v8DZ5GzcqNrybMiqdYJY5KqK6vTsf5JXDgwFFz-h98wdsbV-izcuonPdzMHp4Ay4qyXM6Ed5jB9dUWYGwMkA6rsWXpftfX8xmk4tG1LwFuJV6nAsx4cfpuKwo4vU2Hyr2-fkA7MZG8AHkpDdVUnjm1q-Re9HdG0nCq-2lnBAdOchBpJt37narOj-Zg6cbx~6rzQLVQd8XIv-Bn7VTc1tkBAJVtGOHb0Q~PLzSRmtNGYTnpL0z~gp3tq8lhZc2HuvJW5-tZaYP9yufeIzk5bqsT6DT4iDuclKKw__, , , false`, + `+ ""`, + `client_secret=bP88Q~xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/beamer.go b/cmd/generate/config/rules/beamer.go new file mode 100644 index 0000000..e401507 --- /dev/null +++ b/cmd/generate/config/rules/beamer.go @@ -0,0 +1,25 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func Beamer() *config.Rule { + // define rule + r := config.Rule{ + Description: "Detected a Beamer API token, potentially compromising content management and exposing sensitive notifications and updates.", + RuleID: "beamer-api-token", + Regex: utils.GenerateSemiGenericRegex([]string{"beamer"}, + `b_[a-z0-9=_\-]{44}`, true), + Keywords: []string{"beamer"}, + } + + // validate + tps := utils.GenerateSampleSecrets("beamer", "b_"+secrets.NewSecret(utils.AlphaNumericExtended("44"))) + fps := []string{ + `│   ├── R21A-A-V010SP13RC181024R16900-CN-B_250K-Release-OTA-97B6C6C59241976086FABDC41472150C.bfu`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/bitbucket.go b/cmd/generate/config/rules/bitbucket.go new file mode 100644 index 0000000..be529fc --- /dev/null +++ b/cmd/generate/config/rules/bitbucket.go @@ -0,0 +1,36 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func BitBucketClientID() *config.Rule { + // define rule + r := config.Rule{ + Description: "Discovered a potential Bitbucket Client ID, risking unauthorized repository access and potential codebase exposure.", + RuleID: "bitbucket-client-id", + Regex: utils.GenerateSemiGenericRegex([]string{"bitbucket"}, utils.AlphaNumeric("32"), true), + Keywords: []string{"bitbucket"}, + } + + // validate + tps := utils.GenerateSampleSecrets("bitbucket", secrets.NewSecret(utils.AlphaNumeric("32"))) + return utils.Validate(r, tps, nil) +} + +func BitBucketClientSecret() *config.Rule { + // define rule + r := config.Rule{ + Description: "Discovered a potential Bitbucket Client Secret, posing a risk of compromised code repositories and unauthorized access.", + RuleID: "bitbucket-client-secret", + Regex: utils.GenerateSemiGenericRegex([]string{"bitbucket"}, utils.AlphaNumericExtended("64"), true), + + Keywords: []string{"bitbucket"}, + } + + // validate + tps := utils.GenerateSampleSecrets("bitbucket", secrets.NewSecret(utils.AlphaNumeric("64"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/bittrex.go b/cmd/generate/config/rules/bittrex.go new file mode 100644 index 0000000..29a6d25 --- /dev/null +++ b/cmd/generate/config/rules/bittrex.go @@ -0,0 +1,36 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func BittrexAccessKey() *config.Rule { + // define rule + r := config.Rule{ + Description: "Identified a Bittrex Access Key, which could lead to unauthorized access to cryptocurrency trading accounts and financial loss.", + RuleID: "bittrex-access-key", + Regex: utils.GenerateSemiGenericRegex([]string{"bittrex"}, utils.AlphaNumeric("32"), true), + Keywords: []string{"bittrex"}, + } + + // validate + tps := utils.GenerateSampleSecrets("bittrex", secrets.NewSecret(utils.AlphaNumeric("32"))) + return utils.Validate(r, tps, nil) +} + +func BittrexSecretKey() *config.Rule { + // define rule + r := config.Rule{ + Description: "Detected a Bittrex Secret Key, potentially compromising cryptocurrency transactions and financial security.", + RuleID: "bittrex-secret-key", + Regex: utils.GenerateSemiGenericRegex([]string{"bittrex"}, utils.AlphaNumeric("32"), true), + + Keywords: []string{"bittrex"}, + } + + // validate + tps := utils.GenerateSampleSecrets("bittrex", secrets.NewSecret(utils.AlphaNumeric("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/clickhouse.go b/cmd/generate/config/rules/clickhouse.go new file mode 100644 index 0000000..190c1e0 --- /dev/null +++ b/cmd/generate/config/rules/clickhouse.go @@ -0,0 +1,30 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func ClickHouseCloud() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "clickhouse-cloud-api-secret-key", + Description: "Identified a pattern that may indicate clickhouse cloud API secret key, risking unauthorized clickhouse cloud api access and data breaches on ClickHouse Cloud platforms.", + Regex: regexp.MustCompile(`\b(4b1d[A-Za-z0-9]{38})\b`), + Entropy: 3, + Keywords: []string{ + "4b1d", // Prefix + }, + } + + // validate + tps := utils.GenerateSampleSecrets("ClickHouse", "4b1dbRdW3rOcB7xLthrM4BTBGK1qPLkHigpN1bXD6z") + tps = append(tps, utils.GenerateSampleSecrets("ClickHouse", "4b1d"+secrets.NewSecret("[A-Za-z0-9]{38}"))...) + fps := []string{ + `key = 4b1dXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`, // Low entropy + `key = adf4b1dbRdW3rOcB7xLthrM4BTBGK1qPLkHigpN1bXD6z`, // Not start of a word + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/clojars.go b/cmd/generate/config/rules/clojars.go new file mode 100644 index 0000000..d39c7f2 --- /dev/null +++ b/cmd/generate/config/rules/clojars.go @@ -0,0 +1,23 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func Clojars() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "clojars-api-token", + Description: "Uncovered a possible Clojars API token, risking unauthorized access to Clojure libraries and potential code manipulation.", + Regex: regexp.MustCompile(`(?i)CLOJARS_[a-z0-9]{60}`), + Entropy: 2, + Keywords: []string{"clojars_"}, + } + + // validate + tps := utils.GenerateSampleSecrets("clojars", "CLOJARS_"+secrets.NewSecret(utils.AlphaNumeric("60"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/cloudflare.go b/cmd/generate/config/rules/cloudflare.go new file mode 100644 index 0000000..a876bd6 --- /dev/null +++ b/cmd/generate/config/rules/cloudflare.go @@ -0,0 +1,81 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +var global_keys = []string{ + `cloudflare_global_api_key = "d3d1443e0adc9c24564c6c5676d679d47e2ca"`, // gitleaks:allow + `CLOUDFLARE_GLOBAL_API_KEY: 674538c7ecac77d064958a04a83d9e9db068c`, // gitleaks:allow + `cloudflare: "0574b9f43978174cc2cb9a1068681225433c4"`, // gitleaks:allow +} + +var api_keys = []string{ + `cloudflare_api_key = "Bu0rrK-lerk6y0Suqo1qSqlDDajOk61wZchCkje4"`, // gitleaks:allow + `CLOUDFLARE_API_KEY: 5oK0U90ME14yU6CVxV90crvfqVlNH2wRKBwcLWDc`, // gitleaks:allow + `cloudflare: "oj9Yoyq0zmOyWmPPob1aoY5YSNNuJ0fbZSOURBlX"`, // gitleaks:allow +} + +var origin_ca_keys = []string{ + `CLOUDFLARE_ORIGIN_CA: v1.0-aaa334dc886f30631ba0a610-0d98ef66290d7e50aac7c27b5986c99e6f3f1084c881d8ac0eae5de1d1aa0644076ff57022069b3237d19afe60ad045f207ef2b16387ee37b749441b2ae2e9ebe5b4606e846475d4a5`, + `CLOUDFLARE_ORIGIN_CA: v1.0-15d20c7fccb4234ac5cdd756-d5c2630d1b606535cf9320ae7456b090e0896cec64169a92fae4e931ab0f72f111b2e4ffed5b2bb40f6fba6b2214df23b188a23693d59ce3fb0d28f7e89a2206d98271b002dac695ed`, +} + +var identifiers = []string{"cloudflare"} + +func CloudflareGlobalAPIKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "cloudflare-global-api-key", + Description: "Detected a Cloudflare Global API Key, potentially compromising cloud application deployments and operational security.", + Regex: utils.GenerateSemiGenericRegex(identifiers, utils.Hex("37"), true), + Entropy: 2, + Keywords: identifiers, + } + + // validate + tps := utils.GenerateSampleSecrets("cloudflare", secrets.NewSecret(utils.Hex("37"))) + tps = append(tps, global_keys...) + fps := append(api_keys, origin_ca_keys...) + + return utils.Validate(r, tps, fps) +} + +func CloudflareAPIKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "cloudflare-api-key", + Description: "Detected a Cloudflare API Key, potentially compromising cloud application deployments and operational security.", + Regex: utils.GenerateSemiGenericRegex(identifiers, utils.AlphaNumericExtendedShort("40"), true), + Entropy: 2, + Keywords: identifiers, + } + + // validate + tps := utils.GenerateSampleSecrets("cloudflare", secrets.NewSecret(utils.AlphaNumericExtendedShort("40"))) + tps = append(tps, api_keys...) + fps := append(global_keys, origin_ca_keys...) + + return utils.Validate(r, tps, fps) +} + +func CloudflareOriginCAKey() *config.Rule { + ca_identifiers := append(identifiers, "v1.0-") + // define rule + r := config.Rule{ + Description: "Detected a Cloudflare Origin CA Key, potentially compromising cloud application deployments and operational security.", + RuleID: "cloudflare-origin-ca-key", + Regex: utils.GenerateUniqueTokenRegex(`v1\.0-`+utils.Hex("24")+"-"+utils.Hex("146"), false), + Entropy: 2, + Keywords: ca_identifiers, + } + + // validate + tps := utils.GenerateSampleSecrets("cloudflare", "v1.0-aaa334dc886f30631ba0a610-0d98ef66290d7e50aac7c27b5986c99e6f3f1084c881d8ac0eae5de1d1aa0644076ff57022069b3237d19afe60ad045f207ef2b16387ee37b749441b2ae2e9ebe5b4606e846475d4a5") + tps = append(tps, origin_ca_keys...) + fps := append(global_keys, api_keys...) + + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/codecov.go b/cmd/generate/config/rules/codecov.go new file mode 100644 index 0000000..63c41e8 --- /dev/null +++ b/cmd/generate/config/rules/codecov.go @@ -0,0 +1,23 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func CodecovAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "codecov-access-token", + Description: "Found a pattern resembling a Codecov Access Token, posing a risk of unauthorized access to code coverage reports and sensitive data.", + Regex: utils.GenerateSemiGenericRegex([]string{"codecov"}, utils.AlphaNumeric("32"), true), + Keywords: []string{ + "codecov", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("codecov", secrets.NewSecret(utils.AlphaNumeric("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/cohere.go b/cmd/generate/config/rules/cohere.go new file mode 100644 index 0000000..d803eb0 --- /dev/null +++ b/cmd/generate/config/rules/cohere.go @@ -0,0 +1,32 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func CohereAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "cohere-api-token", + Description: "Identified a Cohere Token, posing a risk of unauthorized access to AI services and data manipulation.", + Regex: utils.GenerateSemiGenericRegex([]string{"cohere", "CO_API_KEY"}, `[a-zA-Z0-9]{40}`, false), + Entropy: 4, + Keywords: []string{ + "cohere", + "CO_API_KEY", + }, + } + + // validate + tps := []string{ + utils.GenerateSampleSecret("cohere", secrets.NewSecret(`[a-zA-Z0-9]{40}`)), + // https://github.com/cohere-ai/cohere-go/blob/abe8044073ed498ffbb206a602d03c2414b64512/client/client.go#L38C30-L38C40 + `export CO_API_KEY=` + secrets.NewSecret(`[a-zA-Z0-9]{40}`), + } + fps := []string{ + `CO_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/coinbase.go b/cmd/generate/config/rules/coinbase.go new file mode 100644 index 0000000..5d15922 --- /dev/null +++ b/cmd/generate/config/rules/coinbase.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func CoinbaseAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "coinbase-access-token", + Description: "Detected a Coinbase Access Token, posing a risk of unauthorized access to cryptocurrency accounts and financial transactions.", + Regex: utils.GenerateSemiGenericRegex([]string{"coinbase"}, + utils.AlphaNumericExtendedShort("64"), true), + Keywords: []string{ + "coinbase", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("coinbase", secrets.NewSecret(utils.AlphaNumericExtendedShort("64"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/config.tmpl b/cmd/generate/config/rules/config.tmpl new file mode 100644 index 0000000..9a39a15 --- /dev/null +++ b/cmd/generate/config/rules/config.tmpl @@ -0,0 +1,75 @@ +# This file has been auto-generated. Do not edit manually. +# If you would like to contribute new rules, please use +# cmd/generate/config/main.go and follow the contributing guidelines +# at https://github.com/gitleaks/gitleaks/blob/master/CONTRIBUTING.md +# +# How the hell does secret scanning work? Read this: +# https://lookingatcomputer.substack.com/p/regex-is-almost-all-you-need +# +# This is the default gitleaks configuration file. +# Rules and allowlists are defined within this file. +# Rules instruct gitleaks on what should be considered a secret. +# Allowlists instruct gitleaks on what is allowed, i.e. not a secret. + +title = "{{.Title}}" + +# minVersion indicates the minimum Gitleaks version required to use this config. +# If the running version is older, a warning will be logged and not all +# config-enabled features are guaranteed to work. +minVersion = "v8.25.0" + +{{ with .Allowlists }}{{ range $i, $allowlist := . }}{{ if or $allowlist.Regexes $allowlist.Paths $allowlist.Commits $allowlist.StopWords }}# TODO: change to [[allowlists]]{{println}}[allowlist] +{{- with .Description }}{{println}}description = "{{ . }}"{{ end }} +{{- with .MatchCondition }}{{println}}condition = "{{ .String }}"{{ end }} +{{- with .Commits -}}{{println}}commits = [ + {{ range $j, $commit := . }}"{{ $commit }}",{{ end }} +]{{ end }} +{{- with .Paths }}{{println}}paths = [{{ range $j, $path := . }} + '''{{ $path }}''',{{ end }} +]{{ end }} +{{- if and .RegexTarget .Regexes }}{{println}}regexTarget = "{{ .RegexTarget }}"{{ end -}} +{{- with .Regexes }}{{println}}regexes = [{{ range $i, $regex := . }} + '''{{ $regex }}''',{{ end }} +]{{ end }} +{{- with .StopWords }}{{println}}stopwords = [{{ range $j, $stopword := . }} + "{{ $stopword }}",{{ end }} +]{{ end }}{{ end }}{{ end }}{{ end }}{{println}} + +{{- range $i, $rule := .Rules }}{{println}}[[rules]] +id = "{{$rule.RuleID}}" +description = "{{$rule.Description}}" +{{- with $rule.Regex }} +regex = '''{{ . }}'''{{ end -}} +{{- with $rule.Path }} +path = '''{{ . }}'''{{ end -}} +{{- with $rule.SecretGroup }} +secretGroup = {{ . }}{{ end -}} +{{- with $rule.Entropy }} +entropy = {{ . }}{{ end -}} +{{- with $rule.Keywords }} +{{- if gt (len .) 1}} +keywords = [{{ range $j, $keyword := . }} + "{{ $keyword }}",{{ end }} +]{{else}} +keywords = [{{ range $j, $keyword := . }}"{{ $keyword }}"{{ end }}]{{end}}{{ end }} +{{- with $rule.Tags }} +tags = [ + {{ range $j, $tag := . }}"{{ $tag }}",{{ end }} +]{{ end }} +{{- with $rule.Allowlists }}{{ range $i, $allowlist := . }}{{ if or $allowlist.Regexes $allowlist.Paths $allowlist.Commits $allowlist.StopWords }}{{println}}[[rules.allowlists]] +{{- with .Description }}{{println}}description = "{{ . }}"{{ end }} +{{- with .MatchCondition }}{{println}}condition = "{{ .String }}"{{ end }} +{{- with .Commits -}}{{println}}commits = [ + {{ range $j, $commit := . }}"{{ $commit }}",{{ end }} +]{{ end }} +{{- with .Paths }}{{println}}paths = [ + {{ range $j, $path := . }}'''{{ $path }}''',{{ end }} +]{{ end }} +{{- if and .RegexTarget .Regexes }}{{println}}regexTarget = "{{ .RegexTarget }}"{{ end -}} +{{- with .Regexes }}{{println}}regexes = [{{ range $i, $regex := . }} + '''{{ $regex }}''',{{ end }} +]{{ end }} +{{- with .StopWords }}{{println}}stopwords = [{{ range $j, $stopword := . }} + "{{ $stopword }}",{{ end }} +]{{ end }}{{ end }}{{ end }}{{ end }} +{{ end }} diff --git a/cmd/generate/config/rules/confluent.go b/cmd/generate/config/rules/confluent.go new file mode 100644 index 0000000..8395f9e --- /dev/null +++ b/cmd/generate/config/rules/confluent.go @@ -0,0 +1,40 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func ConfluentSecretKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "confluent-secret-key", + Description: "Found a Confluent Secret Key, potentially risking unauthorized operations and data access within Confluent services.", + Regex: utils.GenerateSemiGenericRegex([]string{"confluent"}, utils.AlphaNumeric("64"), true), + Keywords: []string{ + "confluent", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("confluent", secrets.NewSecret(utils.AlphaNumeric("64"))) + return utils.Validate(r, tps, nil) +} + +func ConfluentAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "confluent-access-token", + Description: "Identified a Confluent Access Token, which could compromise access to streaming data platforms and sensitive data flow.", + Regex: utils.GenerateSemiGenericRegex([]string{"confluent"}, utils.AlphaNumeric("16"), true), + + Keywords: []string{ + "confluent", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("confluent", secrets.NewSecret(utils.AlphaNumeric("16"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/contentful.go b/cmd/generate/config/rules/contentful.go new file mode 100644 index 0000000..64ee58d --- /dev/null +++ b/cmd/generate/config/rules/contentful.go @@ -0,0 +1,22 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func Contentful() *config.Rule { + // define rule + r := config.Rule{ + Description: "Discovered a Contentful delivery API token, posing a risk to content management systems and data integrity.", + RuleID: "contentful-delivery-api-token", + Regex: utils.GenerateSemiGenericRegex([]string{"contentful"}, + utils.AlphaNumericExtended("43"), true), + Keywords: []string{"contentful"}, + } + + // validate + tps := utils.GenerateSampleSecrets("contentful", secrets.NewSecret(utils.AlphaNumeric("43"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/curl.go b/cmd/generate/config/rules/curl.go new file mode 100644 index 0000000..be7572f --- /dev/null +++ b/cmd/generate/config/rules/curl.go @@ -0,0 +1,201 @@ +package rules + +import ( + "fmt" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +// https://curl.se/docs/manpage.html#-u +func CurlBasicAuth() *config.Rule { + r := config.Rule{ + RuleID: "curl-auth-user", + Description: "Discovered a potential basic authorization token provided in a curl command, which could compromise the curl accessed resource.", + Regex: regexp.MustCompile(`\bcurl\b(?:.*|.*(?:[\r\n]{1,2}.*){1,5})[ \t\n\r](?:-u|--user)(?:=|[ \t]{0,5})("(:[^"]{3,}|[^:"]{3,}:|[^:"]{3,}:[^"]{3,})"|'([^:']{3,}:[^']{3,})'|((?:"[^"]{3,}"|'[^']{3,}'|[\w$@.-]+):(?:"[^"]{3,}"|'[^']{3,}'|[\w${}@.-]+)))(?:\s|\z)`), + Keywords: []string{"curl"}, + Entropy: 2, + Allowlists: []*config.Allowlist{ + { + Regexes: []*regexp.Regexp{ + regexp.MustCompile(`[^:]+:(?:change(?:it|me)|pass(?:word)?|pwd|test|token|\*+|x+)`), // common placeholder passwords + regexp.MustCompile(`['"]?<[^>]+>['"]?:['"]?<[^>]+>|<[^:]+:[^>]+>['"]?`), // + regexp.MustCompile(`[^:]+:\[[^]]+]`), // [placeholder] + regexp.MustCompile(`['"]?[^:]+['"]?:['"]?\$(?:\d|\w+|\{(?:\d|\w+)})['"]?`), // $1 or $VARIABLE + regexp.MustCompile(`\$\([^)]+\):\$\([^)]+\)`), // $(cat login.txt) + regexp.MustCompile(`['"]?\$?{{[^}]+}}['"]?:['"]?\$?{{[^}]+}}['"]?`), // ${{ secrets.FOO }} or {{ .Values.foo }} + }, + }, + }, + } + + // validate + tps := []string{ + // short + `curl --cacert ca.crt -u elastic:P@ssw0rd$1 https://localhost:9200`, // same lines, no quotes + `sh-5.0$ curl -k -X POST https://infinispan:11222/rest/v2/caches/default/hello \ + -H 'Content-type: text/plain' \ + -d 'world' \ + -u developer:yqDVtkqPECriaLRi`, // different line + `curl -u ":d2LkV78zLx!t" https://localhost:9200`, // empty username + `curl -u "d2LkV78zLx!t:" https://localhost:9200`, // empty password + + // long + `curl -sw '%{http_code}' -X POST --user 'johns:h0pk1ns~21s' $GItHUB_API_URL/$GIT_COMMIT --data`, + `curl --user roger23@gmail.com:pQ9wTxu4Fg https://www.dropbox.com/cli_link?host_id=abcdefg -v`, // same line, no quotes + `curl -s --user 'api:d2LkV78zLx!t' \ + https://api.mailgun.net/v2/sandbox91d3515882ecfaa1c65be642.mailgun.org/messages`, // same line, single quotes + `curl -s -v --user "j.smith:dB2yF6@qL9vZm1P#4J" "https://api.contoso.org/user/me"`, // same line, double quotes + `curl -X POST --user "{acd3c08b-74e8-4f44-a2d0-80694le24f46}":"{ZqL5kVrX1n8tA2}" --header "Accept: application/json" --data "{\"text\":\"Hello, world\",\"source\":\"en\",\"target\":\"es\"}" https://gateway.watsonplatform.net/language-translator/api`, + `curl --user kevin:'pRf7vG2h1L8nQkW9' -iX PATCH -H "Content-Type: application/json" -d`, // same line, mixed quoting + `$ curl https://api.dropbox.com/oauth2/token \ + --user c28wlsosanujy2z:qgsnai0xokrw4j1 --data grant_type=authorization_code`, // different line + + // TODO + //` curl -s --insecure --url "imaps://whatever.imap.server" --user\ + //"myuserid:mypassword" --request "STATUS INBOX (UNSEEN)"`, + } + fps := []string{ + // short + `curl -i -u 'test:test'`, + ` curl -sL --user "$1:$2" "$3" > "$4"`, // environment variable + `curl -u https://test.com/endpoint`, // placeholder + `curl --user neo4j:[PASSWORD] http://[IP]:7474/db/data/`, // placeholder + `curl -u "myusername" http://localhost:15130/api/check_user/`, // no password + `curl -u username:token`, + `curl -u "${_username}:${_password}"`, + `curl -u "${username}":"${password}"`, + `curl -k -X POST -I -u "SRVC_JENKINS:${APPID}"`, + `curl -u ":" https://localhost:9200`, // empty username and password + + // long + `curl -sw '%{http_code}' -X POST --user '$USERNAME:$PASSWORD' $GItHUB_API_URL/$GIT_COMMIT --data`, + `curl --user "xxx:yyy"`, + ` curl -sL --user "$GITHUB_USERNAME:$GITHUB_PASSWORD" "$GITHUB_URL" > "$TESTS_PATH"`, // environment variable + // variable interpolation + `curl --silent --fail {{- if and $.Values.username $.Values.password }} --user "{{ $.Values.username }}:{{ $.Values.password }}"`, + `curl -XGET -i -u "${{ env.ELK_ID }}:${{ build.env.ELK_PASS }}"`, + `curl -XGET -i -u "${{needs.vault.outputs.account_id}}:${{needs.vault.outputs.account_password}}"`, + `curl -XGET -i -u "${{ steps.vault.outputs.account_id }}:${{ steps.vault.outputs.account_password }}"`, + `curl -X POST --user "$(cat ./login.txt):$(cat ./password.txt)"`, // command + `curl http://127.0.0.1:5000/file --user user:pass --digest # digest auth`, // placeholder + ` curl -X GET --insecure --user "username:password" \`, // placeholder + `curl --silent --insecure --user ${f5user}:${f5pass} \`, // placeholder + `curl --insecure --ssl-reqd "smtps://smtp.gmail.com" --mail-from "src@gmail.com" --mail-rcpt "dst@gmail.com" --user "src@gmail.com" --upload-file out.txt`, // no password + + // different command + `#HTTP command line test +curl -X POST -H "Content-Type: application/json" -d '{"id":12345,"geo":{"latitude":28.50,"longitude":-81.14}}' http://:8080/serve + +#UDP command line test +echo -n '{"type":"serve","channel":"/","data":{"site_id":8,"post_id":12345,"geo":{"lat":28.50,"long":-81.14}}}' >/dev/udp/127.0.0.1/41234 + +#UDP Listener (for confirmation) +nc -u -l 41234`, + } + return utils.Validate(r, tps, fps) +} + +// https://curl.se/docs/manpage.html#-H +func CurlHeaderAuth() *config.Rule { + // language=regexp + authPat := `(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \t]{0,5}([\w=~@.+/-]{8,}))` + r := config.Rule{ + RuleID: "curl-auth-header", + Description: "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.", + Regex: regexp.MustCompile( + // language=regexp + fmt.Sprintf(`\bcurl\b(?:.*?|.*?(?:[\r\n]{1,2}.*?){1,5})[ \t\n\r](?:-H|--header)(?:=|[ \t]{0,5})(?:"%s"|'%s')(?:\B|\s|\z)`, authPat, authPat)), + Entropy: 2.75, + Keywords: []string{"curl"}, + //Allowlists: []*config.Allowlist{ + // { + // Regexes: []*regexp.Regexp{}, + // }, + //}, + } + + tps := []string{ + `curl --header 'Authorization: 5eb4223e-5008-46e5-be67-c7b8f2732305'`, + // Short flag. + `curl -H 'Authorization: Basic YnJvd3Nlcjo=' \`, // same line, single quotes + // TODO: Handle short flags combined. + //`TOKEN=$(curl -sH "Authorization: Basic $BASIC_TOKEN" "https://$REGISTRY/oauth2/token?service=$REGISTRY&scope=repository:$REPO:pull" | jq -r .access_token)`, + + // Long flag. + `curl -k -X POST --header "Authorization: Basic djJlNEpYa0NJUHZ5a2FWT0VRXzRqZmZUdDkwYTp2emNBZGFzZWpmlWZiUDc2VUJjNDNNVDExclVh" "https://api-qa.example.com:8243/token" -d "grant_type=client_credentials"`, // same line, double quotes + + // Basic auth. + `curl -X POST -H "Content-Type: application/json" \ + -H "Authorization: Basic MzUzYjMwMmM0NDU3NGY1NjUwNDU2ODdlNTM0ZTdkNmE6Mjg2OTI0Njk3ZTYxNWE2NzJhNjQ2YTQ5MzU0NTY0NmM=" \ + -d '{"user":{"emailAddress":"test@example.com"}, "password":"password"}' \ + 'http://localhost:8080/oauth2-provider/v1.0/users'`, // different line, double quotes + `#curl -X POST \ +# https://api.mailgun.net/v3/sandbox7dbcabccd4314c123e8b23599d35f5b6.mailgun.org/messages \ +# -H 'Authorization: Basic YXBpOmtleS1hN2MzNDJ3MzNhNWQxLTU2M2U3MjlwLTZhYjI3YzYzNzM0Ng==' \ +# -F from='Excited User ' \ +# -F to='joe@example.com' \ +# -F subject='Hello' \ +# -F text='Testing some Mailgun awesomness!'`, // different line, single quotes + + // Bearer auth + `# curl -X GET "http://localhost:3000/api/cron/status" -H "Authorization: Bearer cfcabd11c7ed9a41b1a3e063c32d5114"`, // same line, double quotes + `curl -X PUT -H 'Authorization: Bearer jC+6TUUjCNHcVtAXpcqBCgxnA8r+qD6MatnYaf/+289y7HWpK0BWPyLHv/K4DMN32fufwmeVVjlo8zjgBh8kx3GfS6IqO70w1DVMSCTwX7fhEpiXaxzv0mhSMHDX9Kw63Q6DkavUWUV+MDNhCF5wGQrcdQNncVRF3YkuDHDT/xw2YWyZ/DX8k+gAYiC8gcD8Ueg0ljBVS1IDwPjuGoFPESJVxYr0MDPF2D8Pn2S5rq692U4D9ZLuluS46VA4DK6ig5P7QM5XVXi4V7vXM8qpN/zqneyz+w4PUh6NIX7QG6JczMhYd9maWRWVat5jDdyII63P6sNAy9QZjw+ClW211Q==' -d 'user={"account":"user@domain.com", "roles":["user"]}' http://127.0.0.1:8443/desks/1/occupy`, // same line, single quotes + `curl https://api.openai.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer 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" \`, // different line, double quotes + `curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \ + -H "Authorization: Bearer _FXNljbSRYMWx3TWrd7lgKhLtVZX6iskC8Wcbb4b" \ + -H "Content-Type:application/json"`, + `curl -H "Authorization: Bearer sha256~bRLFnzd59Z3XpZH5_seJPHALOuvbWiKwbFKSsoALkgp"`, + + // Token auth + `curl -H "Authorization: Api-Token 22cb987851bc5659l29114c62e60c79abd0d2c08" --request PUT https://appsecclass.report/api/use/635`, // token + `curl -H "Authorization: Token 22cb987851bc5659229114c62e60c79abd0d2c08" --request PUT https://appsecclass.report/api/use/635`, // token + + // Nothing + `curl -L -H "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb25maWRlbmNlIjowLjh9.kvRPfjAhLhtRTczoRgctVGp7KY1QVH3UBZM-gM0x8ec" service1.local -> correct jwt `, // no prefix + `curl -L -H "Authorization: sha256~bRLFnzd5@=-.a+/hgdS"`, // no prefix + + // Non-authorization headers. + `curl -X GET \ + -H "apikey: c4ed6c21-9dd5-4a05-8e3f-c56d1151cce8" \ + -H "Accept: application/json" \`, // apikey + `curl -X POST --header "Api-Token: Sk94HG7f6KB"`, // api-token + `curl -XPOST http://localhost:8080/api/tasks -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "Token: 3fea6af1349166ea" -d "content=hello-curl"`, // token + `curl -X GET https://octopus.corp.net/ + -H "X-Octopus-ApiKey: 3a16750d-d363-41a4-8ebd-035408f7730f" \`, // X-$thing-ApiKey + } + fps := []string{ + // Placeholders + `curl https://example.com/micropub -d h=entry -d "content=Hello World" -H "Authorization: Bearer XXXXXXXXXXXX"`, + `curl -X POST https://accounts.spotify.com/api/token -d grant_type=client_credentials --header "Authorization: Basic ..."`, + `curl \ + -H "Authorization: Bearer " \ + "https://api.openverse.org/v1/audio/?q=test"`, + `curl -v -v -v -X POST https://domain/api/v1/authentication/sso/login-url/ \ + -H 'Content-Type: application/json' \ + -H "Authorization: Token **********" \ + -d '{"username": "test", "next": "/luna/"}'`, + + // Variables + `curl -XPOST http://localhost:8080/api/token -H "Authorization: basic {base64(email:password[\n])}" => token`, // same line, invalid base64 + `curl -X GET \ + -H "apikey: $API_KEY" \ + -H "Accept: $FORMAT" \ +"$API_URL/rest/v1/stats_derniere_labellisation"`, // API Key placeholder + `$ curl -X POST "http://localhost:8000/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "chatglm3-6b-32k", + "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"}] + }'`, // different line, placeholder + `curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer $(gcloud auth print-access-token)" https://workflowexecutions.googleapis.com/v1/projects/244283331594/locations/us-central1/workflows/sample-workflow/executions/43c925aa-514a-44c1-a0a4-a9f8f26fd2cb/callbacks/1705791f-d446-4e92-a6d0-a13622422e80_31864a51-8c13-4b03-ad4d-945cdc8d0631`, // script + + // Not valid BASIC + `curl -X POST -H "Content-Type: application/json" -H "Authorization: Basic eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb25maWRlbmNlIjowLjh9.kvRPfjAhLhtRTczoRgctVGp7KY1QVH3UBZM-gM0x8ec" \`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/databricks.go b/cmd/generate/config/rules/databricks.go new file mode 100644 index 0000000..ca01f67 --- /dev/null +++ b/cmd/generate/config/rules/databricks.go @@ -0,0 +1,26 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func Databricks() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "databricks-api-token", + Description: "Uncovered a Databricks API token, which may compromise big data analytics platforms and sensitive data processing.", + Regex: utils.GenerateUniqueTokenRegex(`dapi[a-f0-9]{32}(?:-\d)?`, false), + Entropy: 3, + Keywords: []string{"dapi"}, + } + + // validate + tps := utils.GenerateSampleSecrets("databricks", "dapi"+secrets.NewSecret(utils.Hex("32"))) + tps = append(tps, `token = dapif13ac4b49d1cb31f69f678e39602e381-2`) // gitleaks:ignore + fps := []string{ + `DATABRICKS_TOKEN=dapi123456789012345678a9bc01234defg5`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/datadog.go b/cmd/generate/config/rules/datadog.go new file mode 100644 index 0000000..c33fa81 --- /dev/null +++ b/cmd/generate/config/rules/datadog.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func DatadogtokenAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "datadog-access-token", + Description: "Detected a Datadog Access Token, potentially risking monitoring and analytics data exposure and manipulation.", + Regex: utils.GenerateSemiGenericRegex([]string{"datadog"}, + utils.AlphaNumeric("40"), true), + Keywords: []string{ + "datadog", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("datadog", secrets.NewSecret(utils.AlphaNumeric("40"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/definednetworking.go b/cmd/generate/config/rules/definednetworking.go new file mode 100644 index 0000000..17fb54c --- /dev/null +++ b/cmd/generate/config/rules/definednetworking.go @@ -0,0 +1,28 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func DefinedNetworkingAPIToken() *config.Rule { + // Define Rule + r := config.Rule{ + // Human redable description of the rule + Description: "Identified a Defined Networking API token, which could lead to unauthorized network operations and data breaches.", + + // Unique ID for the rule + RuleID: "defined-networking-api-token", + + // Regex used for detecting secrets. See regex section below for more details + Regex: utils.GenerateSemiGenericRegex([]string{"dnkey"}, `dnkey-[a-z0-9=_\-]{26}-[a-z0-9=_\-]{52}`, true), + + // Keywords used for string matching on fragments (think of this as a prefilter) + Keywords: []string{"dnkey"}, + } + + // validate + tps := utils.GenerateSampleSecrets("dnkey", "dnkey-"+secrets.NewSecret(utils.AlphaNumericExtended("26"))+"-"+secrets.NewSecret(utils.AlphaNumericExtended("52"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/digitalocean.go b/cmd/generate/config/rules/digitalocean.go new file mode 100644 index 0000000..1bc2262 --- /dev/null +++ b/cmd/generate/config/rules/digitalocean.go @@ -0,0 +1,46 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func DigitalOceanPAT() *config.Rule { + r := config.Rule{ + RuleID: "digitalocean-pat", + Description: "Discovered a DigitalOcean Personal Access Token, posing a threat to cloud infrastructure security and data privacy.", + Regex: utils.GenerateUniqueTokenRegex(`dop_v1_[a-f0-9]{64}`, false), + Entropy: 3, + Keywords: []string{"dop_v1_"}, + } + + tps := utils.GenerateSampleSecrets("do", "dop_v1_"+secrets.NewSecret(utils.Hex("64"))) + return utils.Validate(r, tps, nil) +} + +func DigitalOceanOAuthToken() *config.Rule { + r := config.Rule{ + RuleID: "digitalocean-access-token", + Description: "Found a DigitalOcean OAuth Access Token, risking unauthorized cloud resource access and data compromise.", + Entropy: 3, + Regex: utils.GenerateUniqueTokenRegex(`doo_v1_[a-f0-9]{64}`, false), + Keywords: []string{"doo_v1_"}, + } + + tps := utils.GenerateSampleSecrets("do", "doo_v1_"+secrets.NewSecret(utils.Hex("64"))) + return utils.Validate(r, tps, nil) +} + +func DigitalOceanRefreshToken() *config.Rule { + r := config.Rule{ + Description: "Uncovered a DigitalOcean OAuth Refresh Token, which could allow prolonged unauthorized access and resource manipulation.", + RuleID: "digitalocean-refresh-token", + + Regex: utils.GenerateUniqueTokenRegex(`dor_v1_[a-f0-9]{64}`, true), + Keywords: []string{"dor_v1_"}, + } + + tps := utils.GenerateSampleSecrets("do", "dor_v1_"+secrets.NewSecret(utils.Hex("64"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/discord.go b/cmd/generate/config/rules/discord.go new file mode 100644 index 0000000..b458dbe --- /dev/null +++ b/cmd/generate/config/rules/discord.go @@ -0,0 +1,61 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func DiscordAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "discord-api-token", + Description: "Detected a Discord API key, potentially compromising communication channels and user data privacy on Discord.", + Regex: utils.GenerateSemiGenericRegex([]string{"discord"}, utils.Hex("64"), true), + Keywords: []string{"discord"}, + } + + // validate + tps := utils.GenerateSampleSecrets("discord", secrets.NewSecret(utils.Hex("64"))) + return utils.Validate(r, tps, nil) +} + +func DiscordClientID() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "discord-client-id", + Description: "Identified a Discord client ID, which may lead to unauthorized integrations and data exposure in Discord applications.", + Regex: utils.GenerateSemiGenericRegex([]string{"discord"}, utils.Numeric("18"), true), + Entropy: 2, + Keywords: []string{"discord"}, + } + + // validate + tps := utils.GenerateSampleSecrets("discord", secrets.NewSecret(utils.Numeric("18"))) + fps := []string{ + // Low entropy + `discord=000000000000000000`, + } + return utils.Validate(r, tps, fps) +} + +func DiscordClientSecret() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "discord-client-secret", + Description: "Discovered a potential Discord client secret, risking compromised Discord bot integrations and data leaks.", + Regex: utils.GenerateSemiGenericRegex([]string{"discord"}, utils.AlphaNumericExtended("32"), true), + Entropy: 2, + Keywords: []string{"discord"}, + } + + // validate + tps := utils.GenerateSampleSecrets("discord", secrets.NewSecret(utils.Numeric("32"))) + fps := []string{ + // Low entropy + `discord=00000000000000000000000000000000`, + // TODO: + //`discord=01234567890123456789012345678901`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/doppler.go b/cmd/generate/config/rules/doppler.go new file mode 100644 index 0000000..c5d2dde --- /dev/null +++ b/cmd/generate/config/rules/doppler.go @@ -0,0 +1,26 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func Doppler() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "doppler-api-token", + Description: "Discovered a Doppler API token, posing a risk to environment and secrets management security.", + Regex: regexp.MustCompile(`dp\.pt\.(?i)[a-z0-9]{43}`), + Entropy: 2, + Keywords: []string{`dp.pt.`}, + } + + // validate + tps := utils.GenerateSampleSecrets("doppler", "dp.pt."+secrets.NewSecret(utils.AlphaNumeric("43"))) + return utils.Validate(r, tps, nil) +} + +// TODO add additional doppler formats: +// https://docs.doppler.com/reference/auth-token-formats diff --git a/cmd/generate/config/rules/droneci.go b/cmd/generate/config/rules/droneci.go new file mode 100644 index 0000000..e16aa85 --- /dev/null +++ b/cmd/generate/config/rules/droneci.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func DroneciAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "droneci-access-token", + Description: "Detected a Droneci Access Token, potentially compromising continuous integration and deployment workflows.", + Regex: utils.GenerateSemiGenericRegex([]string{"droneci"}, utils.AlphaNumeric("32"), true), + + Keywords: []string{ + "droneci", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("droneci", secrets.NewSecret(utils.AlphaNumeric("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/dropbox.go b/cmd/generate/config/rules/dropbox.go new file mode 100644 index 0000000..affd2b4 --- /dev/null +++ b/cmd/generate/config/rules/dropbox.go @@ -0,0 +1,48 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func DropBoxAPISecret() *config.Rule { + // define rule + r := config.Rule{ + Description: "Identified a Dropbox API secret, which could lead to unauthorized file access and data breaches in Dropbox storage.", + RuleID: "dropbox-api-token", + Regex: utils.GenerateSemiGenericRegex([]string{"dropbox"}, utils.AlphaNumeric("15"), true), + + Keywords: []string{"dropbox"}, + } + + // validate + tps := utils.GenerateSampleSecrets("dropbox", secrets.NewSecret(utils.AlphaNumeric("15"))) + return utils.Validate(r, tps, nil) +} + +func DropBoxShortLivedAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "dropbox-short-lived-api-token", + Description: "Discovered a Dropbox short-lived API token, posing a risk of temporary but potentially harmful data access and manipulation.", + Regex: utils.GenerateSemiGenericRegex([]string{"dropbox"}, `sl\.[a-z0-9\-=_]{135}`, true), + Keywords: []string{"dropbox"}, + } + + // validate TODO + return &r +} + +func DropBoxLongLivedAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "dropbox-long-lived-api-token", + Description: "Found a Dropbox long-lived API token, risking prolonged unauthorized access to cloud storage and sensitive data.", + Regex: utils.GenerateSemiGenericRegex([]string{"dropbox"}, `[a-z0-9]{11}(AAAAAAAAAA)[a-z0-9\-_=]{43}`, true), + Keywords: []string{"dropbox"}, + } + + // validate TODO + return &r +} diff --git a/cmd/generate/config/rules/duffel.go b/cmd/generate/config/rules/duffel.go new file mode 100644 index 0000000..e7522dd --- /dev/null +++ b/cmd/generate/config/rules/duffel.go @@ -0,0 +1,23 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func Duffel() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "duffel-api-token", + Description: "Uncovered a Duffel API token, which may compromise travel platform integrations and sensitive customer data.", + Regex: regexp.MustCompile(`duffel_(?:test|live)_(?i)[a-z0-9_\-=]{43}`), + Entropy: 2, + Keywords: []string{"duffel_"}, + } + + // validate + tps := utils.GenerateSampleSecrets("duffel", "duffel_test_"+secrets.NewSecret(utils.AlphaNumericExtended("43"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/dynatrace.go b/cmd/generate/config/rules/dynatrace.go new file mode 100644 index 0000000..5f1c12b --- /dev/null +++ b/cmd/generate/config/rules/dynatrace.go @@ -0,0 +1,23 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func Dynatrace() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "dynatrace-api-token", + Description: "Detected a Dynatrace API token, potentially risking application performance monitoring and data exposure.", + Regex: regexp.MustCompile(`dt0c01\.(?i)[a-z0-9]{24}\.[a-z0-9]{64}`), + Entropy: 4, + Keywords: []string{"dt0c01."}, + } + + // validate + tps := utils.GenerateSampleSecrets("dynatrace", "dt0c01."+secrets.NewSecret(utils.AlphaNumeric("24"))+"."+secrets.NewSecret(utils.AlphaNumeric("64"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/easypost.go b/cmd/generate/config/rules/easypost.go new file mode 100644 index 0000000..509e2db --- /dev/null +++ b/cmd/generate/config/rules/easypost.go @@ -0,0 +1,55 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func EasyPost() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "easypost-api-token", + Description: "Identified an EasyPost API token, which could lead to unauthorized postal and shipment service access and data exposure.", + Regex: regexp.MustCompile(`\bEZAK(?i)[a-z0-9]{54}\b`), + Entropy: 2, + Keywords: []string{"EZAK"}, + } + + // validate + tps := utils.GenerateSampleSecrets("EZAK", "EZAK"+secrets.NewSecret(`[a-zA-Z0-9]{54}`)) + tps = append(tps, + "EZAK"+secrets.NewSecret(`[a-zA-Z0-9]{54}`), + "example.com?t=EZAK"+secrets.NewSecret(`[a-zA-Z0-9]{54}`)+"&q=1", + ) + fps := []string{ + // random base64 encoded string + `...6wqX6fNUXA/rYqRvfQ+EZAKGqQRiRyqAFRQshGPWOIAwNWGORfKHSBnVNFtVmWYoW6PH23lkqbbDWep95C/3VmWq/edti6...`, // gitleaks:allow + } + return utils.Validate(r, tps, fps) +} + +func EasyPostTestAPI() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "easypost-test-api-token", + Description: "Detected an EasyPost test API token, risking exposure of test environments and potentially sensitive shipment data.", + Regex: regexp.MustCompile(`\bEZTK(?i)[a-z0-9]{54}\b`), + Entropy: 2, + Keywords: []string{"EZTK"}, + } + + // validate + tps := utils.GenerateSampleSecrets("EZTK", secrets.NewSecret(`EZTK[a-zA-Z0-9]{54}`)) + tps = append(tps, secrets.NewSecret(`EZTK[a-zA-Z0-9]{54}`)) + tps = append(tps, + "EZTK"+secrets.NewSecret(`[a-zA-Z0-9]{54}`), + "example.com?t=EZTK"+secrets.NewSecret(`[a-zA-Z0-9]{54}`)+"&q=1", + ) + fps := []string{ + // random base64 encoded string + `...6wqX6fNUXA/rYqRvfQ+EZTKGqQRiRyqAFRQshGPWOIAwNWGORfKHSBnVNFtVmWYoW6PH23lkqbbDWep95C/3VmWq/edti6...`, // gitleaks:allow + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/etsy.go b/cmd/generate/config/rules/etsy.go new file mode 100644 index 0000000..3107a8c --- /dev/null +++ b/cmd/generate/config/rules/etsy.go @@ -0,0 +1,34 @@ +package rules + +import ( + "fmt" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func EtsyAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "etsy-access-token", + Description: "Found an Etsy Access Token, potentially compromising Etsy shop management and customer data.", + Regex: utils.GenerateSemiGenericRegex([]string{"(?-i:ETSY|[Ee]tsy)"}, utils.AlphaNumeric("24"), true), + Entropy: 3, + Keywords: []string{ + "etsy", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("ETSY", secrets.NewSecret(utils.AlphaNumeric("24"))) + tps = append(tps, utils.GenerateSampleSecrets("etsy", secrets.NewSecret(utils.AlphaNumeric("24")))...) + tps = append(tps, utils.GenerateSampleSecrets("Etsy", secrets.NewSecret(utils.AlphaNumeric("24")))...) + fps := []string{ + fmt.Sprintf(`SetSysctl = "%s"`, secrets.NewSecret(utils.AlphaNumeric("24"))), + ` if err := sysctl.SetSysctl(sysctlBridgeCallIPTables); err != nil {`, + `g6Rib2R5hqhkZXRhY2hlZMOpaGFzaF90eXBlCqNrZXnEIwEgETSYcPQGcaAxl8vuQDLahSfhxkEEHu2flbF9ErAooEoKp3BheWxvYWTFAwB7ImJvZHkiOnsia2V5Ijp7ImVsZGVzdF9raWQiOiIwMTIwMTEzNDk4NzBmNDA2NzFhMDMxOTdjYmVlNDAzMmRhODUyN2UxYzY0MTA0MWVlZDlmOTViMTdkMTJiMDI4YTA0YTBhIiwiaG9zdCI6ImtleWJhc2UuaW8iLCJraWQiOiIwMTIwMTEzNDk4NzBmNDA2NzFhMDMxOTdjYmVlNDAzMmRhODUyN2UxYzY0MTA0MWVlZDlmOTViMTdkMTJiMDI4YTA0YTBhIiwidWlkIjoiYzUyZjc2M2MxNzYyNWZiMTI5YWU1ZDZmZThhMGUzMTkiLCJ1c2VybmFtZSI6ImttYXJla3NwYXJ0eiJ9LCJzZXJ2aWNlIjp7Imhvc3RuYW1lIjoia3lsZS5tYXJlay1zcGFydHoub3JnIiwicHJvdG9jb2wiOiJodHRwOiJ9LCJ0eXBlIjoid2ViX3NlcnZpY2VfYmluZGluZyIsInZlcnNpb24iOjF9LCJjbGllbnQiOnsibmFtZSI6ImtleWJhc2UuaW8gZ28gY2xpZW50IiwidmVyc2lvbiI6IjEuMC4xNCJ9LCJjdGltZSI6MTQ1ODU5MDYyMSwiZXhwaXJlX2luIjo1MDQ1NzYwMDAsIm1lcmtsZV9yb290Ijp7ImN0aW1lIjoxNDU4NTkwNTgzLCJoYXNoIjoiODQ0ZWRkNGU0OTQ3MWUzNWQxZTFkOTM5YTc0ZjUwMDc5Nzg3NzljMTAwYzY1NGE2OGI1NDNhYzY2Y2NlYTQ1MGFjNTllNmY3Yjc4ZGZiN2MyYzdjMmYwMzJiYTA2MzdjMzVjZDk1ZGYyZmRiNjFlNjgxMjVmNDkxNjVlZDkwNzMiLCJzZXFubyI6NDE3Mjk5fSwicHJldiI6IjdmNWFkMGZlZmQxNjM4ZjBlOTc1MTk3NzA5YTk2OTVkZmQ1NzU0MTA4NTYxZGUzMDM0ODc2NDcxODdhMDkyYzUiLCJzZXFubyI6OSwidGFnIjoic2lnbmF0dXJlIn2jc2lnxEDDVCB/SdOzo+BznIUCCa5DgISbH+0noUjyAJ4r0sH/tj8lYNpHw3WR93SBCufeElsl7KrxVdg5qU5ADYj26wgOqHNpZ190eXBlIKN0YWfNAgKndmVyc2lvbgE=`, + `in XCBuild.XCBBuildServiceSession.setSystemInfo(operatingSystemVersion: __C.NSOperatingSystemVersion, productBuildVersion: Swift.String, nativeArchitecture: Swift.String, completion: (Swift.Bool) -> ()) -> ()`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/facebook.go b/cmd/generate/config/rules/facebook.go new file mode 100644 index 0000000..e53dfcc --- /dev/null +++ b/cmd/generate/config/rules/facebook.go @@ -0,0 +1,78 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +// This rule includes both App Secret and Client Access Token +// https://developers.facebook.com/docs/facebook-login/guides/access-tokens/ +func FacebookSecret() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "facebook-secret", + Description: "Discovered a Facebook Application secret, posing a risk of unauthorized access to Facebook accounts and personal data exposure.", + Regex: utils.GenerateSemiGenericRegex([]string{"facebook"}, utils.Hex("32"), true), + Entropy: 3, + Keywords: []string{"facebook"}, + } + + // validate + tps := utils.GenerateSampleSecrets("facebook", secrets.NewSecret(utils.Hex("32"))) + tps = append(tps, + `facebook_app_secret = "6dca6432e45d933e13650d1882bd5e69"`, // gitleaks:allow + `facebook_client_access_token: 26f5fd13099f2c1331aafb86f6489692`, // gitleaks:allow + ) + return utils.Validate(r, tps, nil) +} + +// https://developers.facebook.com/docs/facebook-login/guides/access-tokens/#apptokens +func FacebookAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "facebook-access-token", + Description: "Discovered a Facebook Access Token, posing a risk of unauthorized access to Facebook accounts and personal data exposure.", + Regex: utils.GenerateUniqueTokenRegex(`\d{15,16}(\||%)[0-9a-z\-_]{27,40}`, true), + Keywords: []string{"facebook"}, + Entropy: 3, + } + + // validate + tps := []string{ + `{"facebook access_token":"911602140448729|AY-lRJZq9BoDLobvAiP25L7RcMg","token_type":"bearer"}`, // gitleaks:allow + `facebook 1308742762612587|rhoK1cbv0DOU_RTX_87O4MkX7AI`, // gitleaks:allow + `facebook 1477036645700765|wRPf2v3mt2JfMqCLK8n7oltrEmc`, // gitleaks:allow + } + return utils.Validate(r, tps, nil) +} + +// https://developers.facebook.com/docs/facebook-login/guides/access-tokens/#pagetokens +func FacebookPageAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "facebook-page-access-token", + Description: "Discovered a Facebook Page Access Token, posing a risk of unauthorized access to Facebook accounts and personal data exposure.", + Regex: utils.GenerateUniqueTokenRegex("EAA[MC](?i)[a-z0-9]{100,}", false), + Entropy: 4, + Keywords: []string{"EAAM", "EAAC"}, + } + + // validate + tps := []string{ + `EAAM9GOnCB9kBO2frzOAWGN2zMnZClQshlWydZCrBNdodesbwimx1mfVJgqZBP5RSpMfUzWhtjTTXHG5I1UlvlwRZCgjm3ZBVGeTYiqAAoxyED6HaUdhpGVNoPUwAuAWWFsi9OvyYBQt22DGLqMIgD7VktuCTTZCWKasz81Q822FPhMTB9VFFyClNzQ0NLZClt9zxpsMMrUZCo1VU1rL3CKavir5QTfBjfCEzHNlWAUDUV2YZD`, // gitleaks:allow + `EAAM9GOnCB9kBO2zXpAtRBmCrsPPjdA3KeBl4tqsEpcYd09cpjm9MZCBIklZBjIQBKGIJgFwm8IE17G5pipsfRBRBEHMWxvJsL7iHLUouiprxKRQfAagw8BEEDucceqxTiDhVW2IZAQNNbf0d1JhcapAGntx5S1Csm4j0GgZB3DuUfI2HJ9aViTtdfH2vjBy0wtpXm2iamevohGfoF4NgyRHusDLjqy91uYMkfrkc`, // gitleaks:allow + `- name: FACEBOOK_TOKEN + value: "EAACEdEose0cBA1bad3afsf286JZCOV1XmV4NobAXqUXZA7U9F1UaaOdQZABvz73030MJoC3gGoQrE8IEoMl4gFA6MmQadlJQBqtRsgIcIhtelIJOJaew"`, // gitleaks:allow + } + fps := []string{ + `eaaaC0b75a9329fded2ffa9a02b47e0117831b82`, + `"strict-uri-encode@npm:^2.0.0": + version: 2.0.0 + resolution: "strict-uri-encode@npm:2.0.0" + checksum: eaac4cf978b6fbd480f1092cab8b233c9b949bcabfc9b598dd79a758f7243c28765ef7639c876fa72940dac687181b35486ea01ff7df3e65ce3848c64822c581 + languageName: node + linkType: hard`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/fastly.go b/cmd/generate/config/rules/fastly.go new file mode 100644 index 0000000..6686da7 --- /dev/null +++ b/cmd/generate/config/rules/fastly.go @@ -0,0 +1,22 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func FastlyAPIToken() *config.Rule { + // define rule + r := config.Rule{ + Description: "Uncovered a Fastly API key, which may compromise CDN and edge cloud services, leading to content delivery and security issues.", + RuleID: "fastly-api-token", + Regex: utils.GenerateSemiGenericRegex([]string{"fastly"}, utils.AlphaNumericExtended("32"), true), + + Keywords: []string{"fastly"}, + } + + // validate + tps := utils.GenerateSampleSecrets("fastly", secrets.NewSecret(utils.AlphaNumericExtended("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/finicity.go b/cmd/generate/config/rules/finicity.go new file mode 100644 index 0000000..14b61c9 --- /dev/null +++ b/cmd/generate/config/rules/finicity.go @@ -0,0 +1,37 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func FinicityClientSecret() *config.Rule { + // define rule + r := config.Rule{ + Description: "Identified a Finicity Client Secret, which could lead to compromised financial service integrations and data breaches.", + RuleID: "finicity-client-secret", + Regex: utils.GenerateSemiGenericRegex([]string{"finicity"}, utils.AlphaNumeric("20"), true), + + Keywords: []string{"finicity"}, + } + + // validate + tps := utils.GenerateSampleSecrets("finicity", secrets.NewSecret(utils.AlphaNumeric("20"))) + return utils.Validate(r, tps, nil) +} + +func FinicityAPIToken() *config.Rule { + // define rule + r := config.Rule{ + Description: "Detected a Finicity API token, potentially risking financial data access and unauthorized financial operations.", + RuleID: "finicity-api-token", + Regex: utils.GenerateSemiGenericRegex([]string{"finicity"}, utils.Hex("32"), true), + + Keywords: []string{"finicity"}, + } + + // validate + tps := utils.GenerateSampleSecrets("finicity", secrets.NewSecret(utils.Hex("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/finnhub.go b/cmd/generate/config/rules/finnhub.go new file mode 100644 index 0000000..4826903 --- /dev/null +++ b/cmd/generate/config/rules/finnhub.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func FinnhubAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "finnhub-access-token", + Description: "Found a Finnhub Access Token, risking unauthorized access to financial market data and analytics.", + Regex: utils.GenerateSemiGenericRegex([]string{"finnhub"}, utils.AlphaNumeric("20"), true), + + Keywords: []string{ + "finnhub", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("finnhub", secrets.NewSecret(utils.AlphaNumeric("20"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/flickr.go b/cmd/generate/config/rules/flickr.go new file mode 100644 index 0000000..575d54a --- /dev/null +++ b/cmd/generate/config/rules/flickr.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func FlickrAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "flickr-access-token", + Description: "Discovered a Flickr Access Token, posing a risk of unauthorized photo management and potential data leakage.", + Regex: utils.GenerateSemiGenericRegex([]string{"flickr"}, utils.AlphaNumeric("32"), true), + + Keywords: []string{ + "flickr", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("flickr", secrets.NewSecret(utils.AlphaNumeric("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/flutterwave.go b/cmd/generate/config/rules/flutterwave.go new file mode 100644 index 0000000..3db76d5 --- /dev/null +++ b/cmd/generate/config/rules/flutterwave.go @@ -0,0 +1,53 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func FlutterwavePublicKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "flutterwave-public-key", + Description: "Detected a Finicity Public Key, potentially exposing public cryptographic operations and integrations.", + Regex: regexp.MustCompile(`FLWPUBK_TEST-(?i)[a-h0-9]{32}-X`), + Entropy: 2, + Keywords: []string{"FLWPUBK_TEST"}, + } + + // validate + tps := utils.GenerateSampleSecrets("flutterwavePubKey", "FLWPUBK_TEST-"+secrets.NewSecret(utils.Hex("32"))+"-X") + return utils.Validate(r, tps, nil) +} + +func FlutterwaveSecretKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "flutterwave-secret-key", + Description: "Identified a Flutterwave Secret Key, risking unauthorized financial transactions and data breaches.", + Regex: regexp.MustCompile(`FLWSECK_TEST-(?i)[a-h0-9]{32}-X`), + Entropy: 2, + Keywords: []string{"FLWSECK_TEST"}, + } + + // validate + tps := utils.GenerateSampleSecrets("flutterwavePubKey", "FLWSECK_TEST-"+secrets.NewSecret(utils.Hex("32"))+"-X") + return utils.Validate(r, tps, nil) +} + +func FlutterwaveEncKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "flutterwave-encryption-key", + Description: "Uncovered a Flutterwave Encryption Key, which may compromise payment processing and sensitive financial information.", + Regex: regexp.MustCompile(`FLWSECK_TEST-(?i)[a-h0-9]{12}`), + Entropy: 2, + Keywords: []string{"FLWSECK_TEST"}, + } + + // validate + tps := utils.GenerateSampleSecrets("flutterwavePubKey", "FLWSECK_TEST-"+secrets.NewSecret(utils.Hex("12"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/flyio.go b/cmd/generate/config/rules/flyio.go new file mode 100644 index 0000000..9830a10 --- /dev/null +++ b/cmd/generate/config/rules/flyio.go @@ -0,0 +1,57 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +// https://fly.io/docs/security/tokens/ +// https://github.com/trufflesecurity/trufflehog/pull/2381/files#r1565860579 +// https://github.com/superfly/macaroon-elixir/blob/8b42043b0a24aada5c8b8eb8505dbf1590557f1b/test/vectors.json#L7 +func FlyIOAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "flyio-access-token", + Description: "Uncovered a Fly.io API key", // TODO + Regex: utils.GenerateUniqueTokenRegex(`(?:fo1_[\w-]{43}|fm1[ar]_[a-zA-Z0-9+\/]{100,}={0,3}|fm2_[a-zA-Z0-9+\/]{100,}={0,3})`, false), + Entropy: 4, + Keywords: []string{"fo1_", "fm1", "fm2_"}, + } + + // validate + // fo1_ + tps := utils.GenerateSampleSecrets("fly", secrets.NewSecret(`fo1_[\w-]{43}`)) + tps = append(tps, + `Fly access token: fo1_8rz-j7r2eqJ2U7affEOO3HJN0j63DInyog3eV-glQSc +`, + `============================================================================================================= + +fo1_BtKlzvfztw0M2hlLgTdsfPgDFiwM2jJjQXXy6I2pjuQ +fly deploy`) + // fm1 + tps = append(tps, utils.GenerateSampleSecrets("fly", secrets.NewSecret(`fm1[ar]_[a-zA-Z0-9+\/]{100,}={0,3}`))...) + tps = append(tps, + `ENV FLY_API_TOKEN="FlyV1 fm1r_lJPECAAAAAAAAMqcxBBLMJKXYKJiT0CI58XmukX/wrVodHlwczovL2FwaS5mbHkuaW8vdjGWAJLOAAFmXh8Lk7lodHRwczovL2FwaS5mbHkuaW8vYWFhL3YxxDy5OfA2M6K6aLEoEDKxehojbj+8ZT9IrXCF5sL/r8m6/1gylwySsNxpD40wnpd/G2ZdjwVaQev1kEuFUgzERxPbtWHDNa+NYIZwbKN6b7/JxdbUprq0M10HI4fwtlxhqhdA/mMaMw70EC4TsfJyghIL98KP4ry5AaXiroRdjrSsFExc/xRCDZKUA5GBzgATuNsfBZGCp2J1aWxkZXIfondnHwHEIMa6NWc4b52S+UY7vjPdwKrz00Uzrc1830mOHzQNLun7,fm1a_lJPERxPbtWHDNa+NYIZwbKN6b7/JxdbUprq0M10HI4fwtlxhqhdA/mMaMw70EC4TsfJyghIL98KP4ry4AaXiroRdjrSsFExc/xRCxBCVlAoRzKV/+qYkxuipIbIcw7lodHRwczovL2FwaS5mbHkuaW8vYWFhL3YxlgSSzmS4Y7nPAAAAASCwgdcKkc4AAUktDMQQURck2h+upbiOrW66Nf5SA8QgrD03xlWju1WQi0AUhlk7YYFzOLDfhRyJ6nEziO37NUE="`, + ) + // fm2 + tps = append(tps, utils.GenerateSampleSecrets("fly", secrets.NewSecret(`fm2_[a-zA-Z0-9+\/]{100,}={0,3}`))...) + tps = append(tps, + `# FLY_API_TOKEN: FlyV1 fm2_lJPECAAAAAAAAyZtxBD1hSZ7L5leXsj64ZbDlkm/wrVodHRwczovL2FwaS5mbHkuaW8vdjGWAJLOAAwMDB8Lk7lodHRwczovL2FwaS5mbHkuaW8vYWFhL3YxxDwDnhgJj/ML/nRKMiAYgnvXfNacrGWffj5TdfgGY2LU0ZetT7WzTLQQMO8cN2nRTztl/xLjnnZg5pBwFonETmhNA6Yl0X1tatt8ezA0UjVQiJr93VQ7qAmD5GG2Ce5txhbQv3tmIGsvaC7BOkIqAiR273bhZkO44AYsrCPr2XF8W6Twk7NyU+3UUeDwjw2SlAORgc4APu7vHwWRgqdidWlsZGVyH6J3Zx8BxCAlmLbu1HQDg8ZAGKKmEt4Mbnbqli6lbzBDHsawhcUF4A==,fm2_lJPETmhNA6Yl0X1tatt8ezA0UjVQiJr93VQ7qAmD5GG2Ce5txhbQv3tmIGsvaC7BOkIqAij273bhZkO44AYsrCPr2XF8W6Twk7NyU+3UUeDwj8QQbn07DOV+7SmoLj/uT+dbr8O5aHR0cHM6Ly9hcGkuZmx5LmlvL2FhYS92MZgEks5mqfbvzwAAAAE9PYz9F84AC7QACpHOAAu0AAzEEFfW3B+SzffV/KrAYa8qqpnEIIlD6DqZMZQ9Kt7fEenCCOLA+tUSJ+kmEFIUcc83npOI`, + `"BindToParentToken": "FlyV1 fm2_lJPEEKnzKy0lkwV3B+WIlmrdwejEEFv5qmevHU4fMs+2Gr6oOiPC2SAyOTc0NWI4ZmJlNjBlNjJmZTgzNTkxOThhZWE4MjY0M5IMxAMBAgPEIH7VG8u74KwO62hmx8SZO8WaU5o1g3W2IVc7QN6T1VTr",`, + ) + fps := []string{ + // fo1_ + `resource "doppler_integration_flyio" "prod" { + name = "TF Fly.io" + api_key = "fo1_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +}`, // https://github.com/DopplerHQ/terraform-provider-doppler/blob/a012e1a7903cce391be511b391850b29ebfdeb68/docs/resources/integration_flyio.md?plain=1#L17 + `pub const MINIDUMP_SYSMEMINFO1_PERF_CCTOTALDIRTYPAGES_CCDIRTYPAGETHRESHOLD: u32 = 4u32;`, // https://github.com/microsoft/windows-rs/blob/0f7466c34e774e547d21c579b58b60168c4ee6bc/crates/libs/sys/src/Windows/Win32/System/Diagnostics/Debug/mod.rs#L1258 + ``, + // fm1 + `signature":"I7l2ZXhw9ajjIE2w9tjHNvYjcHg7E2qldMMhoQKjborcWIj8c40rMj83venoy6gXsg6V6B7dlBWxUmzYJR3lJKGECtCSM5BqBvWhL6wX2CN2lZlvwNCyPjG4PCt5MAK1yV1Xv4fqfz8EfT_U49vOzfM1a_nfhXOzrvdg9XgLkAotWBI31vPKjMBrvPqiLcZ12MDNTSK7ubRpVaehSNxiGYHpLhWTkun9qm_APYYXJBjhJYkej50Qcp7Ou8fs3kH02prYIJt4JbWelr5vUDkgMH3AwEx1eYu5NI_8sESlqosl1nhSDx7zq3X1FV1iJlAYCNwGWzW1tjo8PCaJrIHVZZnhBPMN-6ahmOpKb8GViqd4fCQuNe4VUSOeJg8i97kMuk-4r7hwIubR0XfCGzxr7uGDQBdFANi3c4dLlzBAJNifa6b_hT4Xzqja6RCFSv6Cnalyx3hbSkjbyThnXFavJIiR7cvgTcdECg9VxkaxqqRhfAkLAS3hpXQAIYL_bw61M3LN37WHwdgxN_6yZhbSbOsYPmTxiWvdlDaCP_iaCgXgJNfdQ6kep_I89slynE9gdDZ6NSjFJH2Soml4pR6HnQKPjA3OpoTwPSmZjxXY5I78xvrRqRkjdnVzeufqG8LyA-sAEtC0G_312JOxV4GZINquPGk1qFx8WN59Rxw28Tg",`, + // fm2 + `

🗣️ Adobe illustrator软件基础精讲课程

🏷️ #设计师 #资源 #夸克网盘

👉 https://www.ahhhhfs.com/62409/

`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/frameio.go b/cmd/generate/config/rules/frameio.go new file mode 100644 index 0000000..ea1e974 --- /dev/null +++ b/cmd/generate/config/rules/frameio.go @@ -0,0 +1,22 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func FrameIO() *config.Rule { + // define rule + r := config.Rule{ + Description: "Found a Frame.io API token, potentially compromising video collaboration and project management.", + RuleID: "frameio-api-token", + Regex: regexp.MustCompile(`fio-u-(?i)[a-z0-9\-_=]{64}`), + Keywords: []string{"fio-u-"}, + } + + // validate + tps := utils.GenerateSampleSecrets("frameio", "fio-u-"+secrets.NewSecret(utils.AlphaNumericExtended("64"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/freemius.go b/cmd/generate/config/rules/freemius.go new file mode 100644 index 0000000..3716ffe --- /dev/null +++ b/cmd/generate/config/rules/freemius.go @@ -0,0 +1,47 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func Freemius() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "freemius-secret-key", + Description: "Detected a Freemius secret key, potentially exposing sensitive information.", + Regex: regexp.MustCompile(`(?i)["']secret_key["']\s*=>\s*["'](sk_[\S]{29})["']`), + Keywords: []string{"secret_key"}, + Path: regexp.MustCompile(`(?i)\.php$`), + } + + // validate + tps := map[string]string{ + "file.php": `$config = array( + "secret_key" => "sk_ubb4yN3mzqGR2x8#P7r5&@*xC$utE", + );`, + } + // It's only used in PHP SDK snippet. + // see https://freemius.com/help/documentation/wordpress-sdk/integrating-freemius-sdk/ + fps := map[string]string{ + // Invalid format: missing quotes around `secret_key`. + "foo.php": `$config = array( + secret_key => "sk_abcdefghijklmnopqrstuvwxyz123", + );`, + // Invalid format: missing quotes around the key value. + "bar.php": `$config = array( + "secret_key" => sk_abcdefghijklmnopqrstuvwxyz123, + );`, + // Invalid: different key name. + "baz.php": `$config = array( + "other_key" => "sk_abcdefghijklmnopqrstuvwxyz123", + );`, + // Invalid: file extension, should validate only .php files. + "foo.html": `$config = array( + "secret_key" => "sk_ubb4yN3mzqGR2x8#P7r5&@*xC$utE", + );`, + } + + return utils.ValidateWithPaths(r, tps, fps) +} diff --git a/cmd/generate/config/rules/freshbooks.go b/cmd/generate/config/rules/freshbooks.go new file mode 100644 index 0000000..f63d414 --- /dev/null +++ b/cmd/generate/config/rules/freshbooks.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func FreshbooksAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "freshbooks-access-token", + Description: "Discovered a Freshbooks Access Token, posing a risk to accounting software access and sensitive financial data exposure.", + Regex: utils.GenerateSemiGenericRegex([]string{"freshbooks"}, utils.AlphaNumeric("64"), true), + + Keywords: []string{ + "freshbooks", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("freshbooks", secrets.NewSecret(utils.AlphaNumeric("64"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/gcp.go b/cmd/generate/config/rules/gcp.go new file mode 100644 index 0000000..d6ef8a5 --- /dev/null +++ b/cmd/generate/config/rules/gcp.go @@ -0,0 +1,90 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +// TODO this one could probably use some work +func GCPServiceAccount() *config.Rule { + // define rule + r := config.Rule{ + Description: "Google (GCP) Service-account", + RuleID: "gcp-service-account", + Regex: regexp.MustCompile(`\"type\": \"service_account\"`), + Keywords: []string{`\"type\": \"service_account\"`}, + } + + // validate + tps := []string{ + `"type": "service_account"`, + } + return utils.Validate(r, tps, nil) +} + +func GCPAPIKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "gcp-api-key", + Description: "Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches.", + Regex: utils.GenerateUniqueTokenRegex(`AIza[\w-]{35}`, false), + Entropy: 4, + Keywords: []string{"AIza"}, + Allowlists: []*config.Allowlist{ + { + Regexes: []*regexp.Regexp{ + // example keys from https://github.com/firebase/firebase-android-sdk + regexp.MustCompile(`AIzaSyabcdefghijklmnopqrstuvwxyz1234567`), + regexp.MustCompile(`AIzaSyAnLA7NfeLquW1tJFpx_eQCxoX-oo6YyIs`), + regexp.MustCompile(`AIzaSyCkEhVjf3pduRDt6d1yKOMitrUEke8agEM`), + regexp.MustCompile(`AIzaSyDMAScliyLx7F0NPDEJi1QmyCgHIAODrlU`), + regexp.MustCompile(`AIzaSyD3asb-2pEZVqMkmL6M9N6nHZRR_znhrh0`), + regexp.MustCompile(`AIzayDNSXIbFmlXbIE6mCzDLQAqITYefhixbX4A`), + regexp.MustCompile(`AIzaSyAdOS2zB6NCsk1pCdZ4-P6GBdi_UUPwX7c`), + regexp.MustCompile(`AIzaSyASWm6HmTMdYWpgMnjRBjxcQ9CKctWmLd4`), + regexp.MustCompile(`AIzaSyANUvH9H9BsUccjsu2pCmEkOPjjaXeDQgY`), + regexp.MustCompile(`AIzaSyA5_iVawFQ8ABuTZNUdcwERLJv_a_p4wtM`), + regexp.MustCompile(`AIzaSyA4UrcGxgwQFTfaI3no3t7Lt1sjmdnP5sQ`), + regexp.MustCompile(`AIzaSyDSb51JiIcB6OJpwwMicseKRhhrOq1cS7g`), + regexp.MustCompile(`AIzaSyBF2RrAIm4a0mO64EShQfqfd2AFnzAvvuU`), + regexp.MustCompile(`AIzaSyBcE-OOIbhjyR83gm4r2MFCu4MJmprNXsw`), + regexp.MustCompile(`AIzaSyB8qGxt4ec15vitgn44duC5ucxaOi4FmqE`), + regexp.MustCompile(`AIzaSyA8vmApnrHNFE0bApF4hoZ11srVL_n0nvY`), + }, + }, + }, + } + + // validate + tps := utils.GenerateSampleSecrets("gcp", secrets.NewSecret(`AIza[\w-]{35}`)) + tps = append(tps, + // non-word character at end + `AIzaSyNHxIf32IQ1a1yjl3ZJIqKZqzLAK1XhDk-`, // gitleaks:allow + ) + fps := []string{ + `GWw4hjABFzZCGiRpmlDyDdo87Jn9BN9THUA47muVRNunLxsa82tMAdvmrhOqNkRKiYMEAFbTJAIzaTesb6Tscfcni8vIpWZqNCXFDFslJtVSvFDq`, // text boundary start + `AIzaTesb6Tscfcni8vIpWZqNCXFDFslJtVSvFDqabcd123`, // text boundary end + `apiKey: "AIzaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"`, // not enough entropy + `AIZASYCO2CXRMC9ELSKLHLHRMBSWDEVEDZTLO2O`, // incorrect case + // example keys from https://github.com/firebase/firebase-android-sdk + `AIzaSyabcdefghijklmnopqrstuvwxyz1234567`, + `AIzaSyAnLA7NfeLquW1tJFpx_eQCxoX-oo6YyIs`, + `AIzaSyCkEhVjf3pduRDt6d1yKOMitrUEke8agEM`, + `AIzaSyDMAScliyLx7F0NPDEJi1QmyCgHIAODrlU`, + `AIzaSyD3asb-2pEZVqMkmL6M9N6nHZRR_znhrh0`, + `AIzayDNSXIbFmlXbIE6mCzDLQAqITYefhixbX4A`, + `AIzaSyAdOS2zB6NCsk1pCdZ4-P6GBdi_UUPwX7c`, + `AIzaSyASWm6HmTMdYWpgMnjRBjxcQ9CKctWmLd4`, + `AIzaSyANUvH9H9BsUccjsu2pCmEkOPjjaXeDQgY`, + `AIzaSyA5_iVawFQ8ABuTZNUdcwERLJv_a_p4wtM`, + `AIzaSyA4UrcGxgwQFTfaI3no3t7Lt1sjmdnP5sQ`, + `AIzaSyDSb51JiIcB6OJpwwMicseKRhhrOq1cS7g`, + `AIzaSyBF2RrAIm4a0mO64EShQfqfd2AFnzAvvuU`, + `AIzaSyBcE-OOIbhjyR83gm4r2MFCu4MJmprNXsw`, + `AIzaSyB8qGxt4ec15vitgn44duC5ucxaOi4FmqE`, + `AIzaSyA8vmApnrHNFE0bApF4hoZ11srVL_n0nvY`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/generic.go b/cmd/generate/config/rules/generic.go new file mode 100644 index 0000000..43fded7 --- /dev/null +++ b/cmd/generate/config/rules/generic.go @@ -0,0 +1,299 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func GenericCredential() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "generic-api-key", + Description: "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", + Regex: utils.GenerateSemiGenericRegex([]string{ + "access", + "auth", + `(?-i:[Aa]pi|API)`, + "credential", + "creds", + "key", + "passw(?:or)?d", + "secret", + "token", + }, `[\w.=-]{10,150}|[a-z0-9][a-z0-9+/]{11,}={0,3}`, true), + Keywords: []string{ + "access", + "api", + "auth", + "key", + "credential", + "creds", + "passwd", + "password", + "secret", + "token", + }, + Entropy: 3.5, + Allowlists: []*config.Allowlist{ + { + // 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 ensure the secret contains both numbers and alphabetical characters, not just alphabetical characters. + Regexes: []*regexp.Regexp{ + regexp.MustCompile(`^[a-zA-Z_.-]+$`), + }, + }, + { + Description: "Allowlist for Generic API Keys", + MatchCondition: config.AllowlistMatchOr, + RegexTarget: "match", + Regexes: []*regexp.Regexp{ + regexp.MustCompile(`(?i)(?:` + + // Access + `access(?:ibility|or)` + + `|access[_.-]?id` + + `|random[_.-]?access` + + // API + `|api[_.-]?(?:id|name|version)` + // id/name/version -> not a secret + `|rapid|capital` + // common words containing "api" + `|[a-z0-9-]*?api[a-z0-9-]*?:jar:` + // Maven META-INF dependencies that contain "api" in the name. + // Auth + `|author` + + `|X-MS-Exchange-Organization-Auth` + // email header + `|Authentication-Results` + // email header + // Credentials + `|(?:credentials?[_.-]?id|withCredentials)` + // Jenkins plugins + // Key + `|(?:bucket|foreign|hot|idx|natural|primary|pub(?:lic)?|schema|sequence)[_.-]?key` + + `|(?:turkey)` + + `|key[_.-]?(?:alias|board|code|frame|id|length|mesh|name|pair|press(?:ed)?|ring|selector|signature|size|stone|storetype|word|up|down|left|right)` + + // Azure KeyVault + `|key[_.-]?vault[_.-]?(?:id|name)|keyVaultToStoreSecrets` + + `|key(?:store|tab)[_.-]?(?:file|path)` + + `|issuerkeyhash` + // part of ssl cert + `|(?-i:[DdMm]onkey|[DM]ONKEY)|keying` + // common words containing "key" + // Secret + `|(?:secret)[_.-]?(?:length|name|size)` + // name of e.g. env variable + `|UserSecretsId` + // https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-8.0&tabs=linux + + // Token + `|(?:csrf)[_.-]?token` + + `|(?:io\.jsonwebtoken[ \t]?:[ \t]?[\w-]+)` + // Maven library coordinates. (e.g., https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt) + + // General + `|(?:api|credentials|token)[_.-]?(?:endpoint|ur[il])` + + `|public[_.-]?token` + + `|(?:key|token)[_.-]?file` + + // Empty variables capturing the next line (e.g., .env files) + `|(?-i:(?:[A-Z_]+=\n[A-Z_]+=|[a-z_]+=\n[a-z_]+=)(?:\n|\z))` + + `|(?-i:(?:[A-Z.]+=\n[A-Z.]+=|[a-z.]+=\n[a-z.]+=)(?:\n|\z))` + + `)`), + }, + StopWords: append(DefaultStopWords, + "6fe4476ee5a1832882e326b506d14126", // https://github.com/yarnpkg/berry/issues/6201 + ), + }, + { + RegexTarget: "line", + Regexes: []*regexp.Regexp{ + // Docker build secrets (https://docs.docker.com/build/building/secrets/#using-build-secrets). + regexp.MustCompile(`--mount=type=secret,`), + // https://github.com/gitleaks/gitleaks/issues/1800 + regexp.MustCompile(`import[ \t]+{[ \t\w,]+}[ \t]+from[ \t]+['"][^'"]+['"]`), + }, + }, + { + MatchCondition: config.AllowlistMatchAnd, + RegexTarget: "line", + Regexes: []*regexp.Regexp{ + regexp.MustCompile(`LICENSE[^=]*=\s*"[^"]+`), + regexp.MustCompile(`LIC_FILES_CHKSUM[^=]*=\s*"[^"]+`), + regexp.MustCompile(`SRC[^=]*=\s*"[a-zA-Z0-9]+`), + }, + Paths: []*regexp.Regexp{ + regexp.MustCompile(`\.bb$`), + regexp.MustCompile(`\.bbappend$`), + regexp.MustCompile(`\.bbclass$`), + regexp.MustCompile(`\.inc$`), + }, + }, + }, + } + + // validate + tps := utils.GenerateSampleSecrets("generic", "CLOJARS_34bf0e88955ff5a1c328d6a7491acc4f48e865a7b8dd4d70a70749037443") //gitleaks:allow + tps = append(tps, utils.GenerateSampleSecrets("generic", "Zf3D0LXCM3EIMbgJpUNnkRtOfOueHznB")...) + tps = append(tps, + // Access + `'access_token': 'eyJ0eXAioiJKV1slS3oASx=='`, + + // API + `some_api_token_123 = "`+newPlausibleSecret(`[a-zA-Z0-9]{60}`)+`"`, + + // Auth + `"user_auth": "am9obmRvZTpkMDY5NGIxYi1jMTcxLTQ4ODYt+TMyYS0wMmUwOWQ1/mIwNjc="`, + + // Credentials + `"credentials" : "0afae57f3ccfd9d7f5767067bc48b30f719e271ba470488056e37ab35d4b6506"`, + `creds = `+newPlausibleSecret(`[a-zA-Z0-9]{30}`), + + // Key + `private-key: `+newPlausibleSecret(`[a-zA-Z0-9\-_.=]{100}`), + + // Password + `passwd = `+newPlausibleSecret(`[a-zA-Z0-9\-_.=]{30}`), + // TODO: `ID=dbuser;password=` + newPlausibleSecret(`[a-zA-Z0-9+/]{30}={0,3}`) + `;"`, + + // Secret + `"client_secret" : "6da89121079f83b2eb6acccf8219ea982c3d79bccc3e9c6a85856480661f8fde",`, + `mySecretString=`+newPlausibleSecret(`[a-zA-Z0-9]{30}`), + `todo_secret_do_not_commit = `+newPlausibleSecret(`[a-zA-Z0-9]{30}`), + + // Token + ` utils.GetEnvOrDefault("api_token", "dafa7817-e246-48f3-91a7-e87653d587b8")`, + // `"env": { + //"API_TOKEN": "Lj2^5O%xi214"`, + ) + fps := []string{ + // Access + `"accessor":"rA1wk0Y45YCufyfq",`, + `report_access_id: e8e4df51-2054-49b0-ab1c-516ac95c691d`, + `accessibilityYesOptionId = "0736f5ef-7e88-499a-80cc-90c85d2a5180"`, + `_RandomAccessIterator> +_LIBCPP_CONSTEXPR_AFTER_CXX11 `, + + // API + `this.ultraPictureBox1.Name = "ultraPictureBox1";`, + `rapidstring:marm64-uwp=fail`, + `event-bus-message-api:rc0.15.0_20231217_1420-SNAPSHOT'`, + `COMMUNICATION_API_VERSION=rc0.13.0_20230412_0712-SNAPSHOT`, + `MantleAPI_version=9a038989604e8da62ecddbe2094b16ce1b778be1`, + `[DEBUG] org.slf4j.slf4j-api:jar:1.7.8.:compile (version managed from default)`, + `[DEBUG] org.neo4j.neo4j-graphdb-api:jar:3.5.12:test`, + `apiUrl=apigee.corpint.com`, + `X-API-Name": "NRG0-Hermes-INTERNAL-API",`, + // TODO: Jetbrains IML files (requires line-level allowlist). + // `` + + // Auth + `author = "james.fake@ymail.com",`, + `X-MS-Exchange-Organization-AuthSource: sm02915.int.contoso.com`, + `Authentication-Results: 5h.ca.iphmx.com`, + + // Credentials + `withCredentials([usernamePassword(credentialsId: '29f63271-dc2f-4734-8221-5b31b5169bac', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {`, + `credentialsId: 'ff083f76-7804-4ef1-80e4-fe975bb9141b'`, + `jobCredentialsId: 'f4aeb6bc-2a25-458a-8111-9be9e502c0e7'`, + ` "credentialId": "B9mTcFSck2LzJO2S3ols63",`, + `environment { + CREDENTIALS_ID = "K8S_CRED" +}`, + `dev.credentials.url=dev-lb1.api.f4ke.com:5215`, + + // Key + `keyword: "Befaehigung_P2"`, + `public_key = "9Cnzj4p4WGeKLs1Pt8QuKUpRKfFLfRYC9AIKjbJTWit"`, + `pub const X509_pubkey_st = struct_X509_pubkey_st;`, + `|| pIdxKey->default_rc==0`, + `monkeys-audio:mx64-uwp=fail`, + `primaryKey=` + newPlausibleSecret(`[a-zA-Z0-9\-_.=]{30}`), + `foreignKey=` + newPlausibleSecret(`[a-zA-Z0-9\-_.=]{30}`), + `key_down_event=` + newPlausibleSecret(`[a-zA-Z0-9\-_.=]{30}`), + `issuerKeyHash=` + newPlausibleSecret(`[a-zA-Z0-9\-_.=]{30}`), + ``, + `minisat-master-keying:x64-uwp=fail`, + `IceSSL.KeyFile=s_rsa1024_priv.pem`, + `"bucket_key": "SalesResults-1.2"`, + ``, + // `packageKey":` + newPlausibleSecret(`[a-zA-Z0-9\-_.=]{30}`), + `schemaKey = 'DOC_Vector_5_32'`, + `sequenceKey = "18"`, + `app.keystore.file=env/cert.p12`, + `-DKEYTAB_FILE=/tmp/app.keytab`, + ` doc.Security.KeySize = PdfEncryptionKeySize.Key128Bit;`, + `o.keySelector=n,o.haKey=!1,`, + // TODO: Requires line-level allowlists. + ` "key_name": "prod5zyxlmy-cmk",`, + ` "kms_key_id": "555ea4a3-d53a-4412-9c66-3a7cb667b0d6",`, + ` "key_vault_name": "web21prqodx24021",`, + ` keyVaultToStoreSecrets: cmp2-qat-1208358310`, // e.g., https://github.com/2uasimojo/community-operators-prod/blob/9e51e4c8e0b5caaa3087e8e18e6fb918b2c36643/operators/azure-service-operator/1.0.59040/manifests/azure.microsoft.com_cosmosdbs.yaml#L50 + `,apiKey:"6fe4476ee5a1832882e326b506d14126",`, + `const validKeyChars = "0123456789abcdefghijklmnopqrstuvwxyz_-."`, + `const keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"`, + `key_length = XSalsa20.key_length`, + `pub const SN_id_Gost28147_89_None_KeyMeshing = "id-Gost28147-89-None-KeyMeshing"`, + `KeyPair = X25519.KeyPair`, + `BlindKeySignatures = Ed25519.BlindKeySignatures`, + `AVEncVideoMaxKeyframeDistance, "2987123a-ba93-4704-b489-ec1e5f25292c"`, + ` keyPressed = kVK_Return.u16`, + `timezone_mapping = { + "Turkey Standard Time": "Europe/Istanbul", +}`, // https://github.com/gitleaks/gitleaks/issues/1799 + // ``, + //` { key: '9df21e95-3848-409d-8f94-c675cdfee839', value: 'Americas' },`, + // ``, + // `secret: + // secretName: app-decryption-secret + // items: + // - key: app-k8s.yml + // path: app-k8s.yml`, + + // TODO: https://learn.microsoft.com/en-us/windows/apps/design/style/xaml-theme-resources + //`#FFBAE4FF`, + + // Password + `password combination. + +R5: Regulatory--21`, + `PuttyPassword=0`, + + // Secret + `LLM_SECRET_NAME = "NEXUS-GPT4-API-KEY"`, + ` 79a3edd0-2092-40a2-a04d-dcb46d5ca9ed`, + `secret_length = X25519.secret_length`, + `secretSize must be >= XXH3_SECRET_SIZE_MIN`, + `# get build time secret for authentication +#RUN --mount=type=secret,id=jfrog_secret \ +# JFROG_SECRET = $(cat /run/secrets/jfrog_secret) && \`, + + // Token + ` access_token_url='https://github.com/login/oauth/access_token',`, + `publicToken = "9Cnzj4p4WGeKLs1Pt8QuKUpRKfFLfRYC9AIKjbJTWit"`, + ``, + `notes = "Maven - io.jsonwebtoken:jjwt-jackson-0.11.2"`, + `csrf-token=Mj2qykJO5rELyHgezQ69nzUX0i3OH67V7+V4eUrLfpuyOuxmiW9rhROG/Whikle15syazJOkrjJa3U2AbhIvUw==`, + // TODO: `TOKEN_AUDIENCE = "25872395-ed3a-4703-b647-22ec53f3683c"`, + + // General + `clientId = "73082700-1f09-405b-80d0-3131bfd6272d"`, + `GITHUB_API_KEY= +DYNATRACE_API_KEY=`, + `snowflake.password= +jdbc.snowflake.url=`, + `import { chain_Anvil1_Key, chain_Anvil2_Key } from '../blockchain-tests/pallets/supported-chains/consts';`, + + // Yocto/BitBake + `SRCREV_moby = "43fc912ef59a83054ea7f6706df4d53a7dea4d80"`, + `LIC_FILES_CHKSUM = "file://${WORKDIR}/license.html;md5=5c94767cedb5d6987c902ac850ded2c6"`, + } + return utils.Validate(r, tps, fps) +} + +func newPlausibleSecret(regex string) string { + allowList := &config.Allowlist{StopWords: DefaultStopWords} + // attempt to generate a random secret, + // retrying until it contains at least one digit and no stop words + // TODO: currently the DefaultStopWords list contains many short words, + // so there is a significant chance of generating a secret that contains a stop word + for { + secret := secrets.NewSecret(regex) + if !regexp.MustCompile(`[1-9]`).MatchString(secret) { + continue + } + if ok, _ := allowList.ContainsStopWord(secret); ok { + continue + } + return secret + } +} diff --git a/cmd/generate/config/rules/github.go b/cmd/generate/config/rules/github.go new file mode 100644 index 0000000..58aaf43 --- /dev/null +++ b/cmd/generate/config/rules/github.go @@ -0,0 +1,111 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +var githubAllowlist = []*config.Allowlist{ + { + Paths: []*regexp.Regexp{ + // https://github.com/octokit/auth-token.js/?tab=readme-ov-file#createtokenauthtoken-options + regexp.MustCompile(`(?:^|/)@octokit/auth-token/README\.md$`), + }, + }, +} + +func GitHubPat() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "github-pat", + Description: "Uncovered a GitHub Personal Access Token, potentially leading to unauthorized repository access and sensitive content exposure.", + Regex: regexp.MustCompile(`ghp_[0-9a-zA-Z]{36}`), + Entropy: 3, + Keywords: []string{"ghp_"}, + Allowlists: githubAllowlist, + } + + // validate + tps := utils.GenerateSampleSecrets("github", "ghp_"+secrets.NewSecret(utils.AlphaNumeric("36"))) + fps := []string{ + "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + } + return utils.Validate(r, tps, fps) +} + +func GitHubFineGrainedPat() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "github-fine-grained-pat", + Description: "Found a GitHub Fine-Grained Personal Access Token, risking unauthorized repository access and code manipulation.", + Regex: regexp.MustCompile(`github_pat_\w{82}`), + Entropy: 3, + Keywords: []string{"github_pat_"}, + } + + // validate + tps := utils.GenerateSampleSecrets("github", "github_pat_"+secrets.NewSecret(utils.AlphaNumeric("82"))) + fps := []string{ + "github_pat_xxxxxxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + } + return utils.Validate(r, tps, fps) +} + +func GitHubOauth() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "github-oauth", + Description: "Discovered a GitHub OAuth Access Token, posing a risk of compromised GitHub account integrations and data leaks.", + Regex: regexp.MustCompile(`gho_[0-9a-zA-Z]{36}`), + Entropy: 3, + Keywords: []string{"gho_"}, + } + + // validate + tps := utils.GenerateSampleSecrets("github", "gho_"+secrets.NewSecret(utils.AlphaNumeric("36"))) + fps := []string{ + "gho_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + } + return utils.Validate(r, tps, fps) +} + +func GitHubApp() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "github-app-token", + Description: "Identified a GitHub App Token, which may compromise GitHub application integrations and source code security.", + Regex: regexp.MustCompile(`(?:ghu|ghs)_[0-9a-zA-Z]{36}`), + Entropy: 3, + Keywords: []string{"ghu_", "ghs_"}, + Allowlists: githubAllowlist, + } + + // validate + tps := utils.GenerateSampleSecrets("github", "ghs_"+secrets.NewSecret(utils.AlphaNumeric("36"))) + tps = append(tps, utils.GenerateSampleSecrets("github", "ghu_"+secrets.NewSecret(utils.AlphaNumeric("36")))...) + fps := []string{ + "ghu_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "ghs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + } + return utils.Validate(r, tps, fps) +} + +func GitHubRefresh() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "github-refresh-token", + Description: "Detected a GitHub Refresh Token, which could allow prolonged unauthorized access to GitHub services.", + Regex: regexp.MustCompile(`ghr_[0-9a-zA-Z]{36}`), + Entropy: 3, + Keywords: []string{"ghr_"}, + } + + // validate + tps := utils.GenerateSampleSecrets("github", "ghr_"+secrets.NewSecret(utils.AlphaNumeric("36"))) + fps := []string{ + "ghr_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/gitlab.go b/cmd/generate/config/rules/gitlab.go new file mode 100644 index 0000000..69babe9 --- /dev/null +++ b/cmd/generate/config/rules/gitlab.go @@ -0,0 +1,222 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +// overview with all GitLab tokens: +// https://docs.gitlab.com/ee/security/tokens/index.html#token-prefixes + +func GitlabCiCdJobToken() *config.Rule { + r := config.Rule{ + RuleID: "gitlab-cicd-job-token", + Description: "Identified a GitLab CI/CD Job Token, potential access to projects and some APIs on behalf of a user while the CI job is running.", + Regex: regexp.MustCompile(`glcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}`), + Entropy: 3, + Keywords: []string{"glcbt-"}, + } + tps := utils.GenerateSampleSecrets("gitlab", "glcbt-"+secrets.NewSecret(utils.AlphaNumeric("5"))+"_"+secrets.NewSecret(utils.AlphaNumeric("20"))) + return utils.Validate(r, tps, nil) +} + +func GitlabDeployToken() *config.Rule { + r := config.Rule{ + Description: "Identified a GitLab Deploy Token, risking access to repositories, packages and containers with write access.", + RuleID: "gitlab-deploy-token", + Regex: regexp.MustCompile(`gldt-[0-9a-zA-Z_\-]{20}`), + Entropy: 3, + Keywords: []string{"gldt-"}, + } + tps := []string{ + utils.GenerateSampleSecret("gitlab", "gldt-"+secrets.NewSecret(utils.AlphaNumeric("20"))), + } + return utils.Validate(r, tps, nil) +} + +func GitlabFeatureFlagClientToken() *config.Rule { + r := config.Rule{ + RuleID: "gitlab-feature-flag-client-token", + Description: "Identified a GitLab feature flag client token, risks exposing user lists and features flags used by an application.", + Regex: regexp.MustCompile(`glffct-[0-9a-zA-Z_\-]{20}`), + Entropy: 3, + Keywords: []string{"glffct-"}, + } + tps := utils.GenerateSampleSecrets("gitlab", "glffct-"+secrets.NewSecret(utils.AlphaNumeric("20"))) + return utils.Validate(r, tps, nil) +} + +func GitlabFeedToken() *config.Rule { + r := config.Rule{ + RuleID: "gitlab-feed-token", + Description: "Identified a GitLab feed token, risking exposure of user data.", + Regex: regexp.MustCompile(`glft-[0-9a-zA-Z_\-]{20}`), + Entropy: 3, + Keywords: []string{"glft-"}, + } + tps := utils.GenerateSampleSecrets("gitlab", "glft-"+secrets.NewSecret(utils.AlphaNumeric("20"))) + return utils.Validate(r, tps, nil) +} + +func GitlabIncomingMailToken() *config.Rule { + r := config.Rule{ + RuleID: "gitlab-incoming-mail-token", + Description: "Identified a GitLab incoming mail token, risking manipulation of data sent by mail.", + Regex: regexp.MustCompile(`glimt-[0-9a-zA-Z_\-]{25}`), + Entropy: 3, + Keywords: []string{"glimt-"}, + } + tps := utils.GenerateSampleSecrets("gitlab", "glimt-"+secrets.NewSecret(utils.AlphaNumeric("25"))) + return utils.Validate(r, tps, nil) +} + +func GitlabKubernetesAgentToken() *config.Rule { + r := config.Rule{ + RuleID: "gitlab-kubernetes-agent-token", + Description: "Identified a GitLab Kubernetes Agent token, risking access to repos and registry of projects connected via agent.", + Regex: regexp.MustCompile(`glagent-[0-9a-zA-Z_\-]{50}`), + Entropy: 3, + Keywords: []string{"glagent-"}, + } + tps := utils.GenerateSampleSecrets("gitlab", "glagent-"+secrets.NewSecret(utils.AlphaNumeric("50"))) + return utils.Validate(r, tps, nil) +} + +func GitlabOauthAppSecret() *config.Rule { + r := config.Rule{ + RuleID: "gitlab-oauth-app-secret", + Description: "Identified a GitLab OIDC Application Secret, risking access to apps using GitLab as authentication provider.", + Regex: regexp.MustCompile(`gloas-[0-9a-zA-Z_\-]{64}`), + Entropy: 3, + Keywords: []string{"gloas-"}, + } + tps := utils.GenerateSampleSecrets("gitlab", "gloas-"+secrets.NewSecret(utils.AlphaNumeric("64"))) + return utils.Validate(r, tps, nil) +} + +func GitlabPat() *config.Rule { + r := config.Rule{ + RuleID: "gitlab-pat", + Description: "Identified a GitLab Personal Access Token, risking unauthorized access to GitLab repositories and codebase exposure.", + Regex: regexp.MustCompile(`glpat-[\w-]{20}`), + Entropy: 3, + Keywords: []string{"glpat-"}, + } + + // validate + tps := utils.GenerateSampleSecrets("gitlab", "glpat-"+secrets.NewSecret(utils.AlphaNumeric("20"))) + fps := []string{ + "glpat-XXXXXXXXXXX-XXXXXXXX", + } + return utils.Validate(r, tps, fps) +} + +func GitlabPatRoutable() *config.Rule { + r := config.Rule{ + RuleID: "gitlab-pat-routable", + Description: "Identified a GitLab Personal Access Token (routable), risking unauthorized access to GitLab repositories and codebase exposure.", + Regex: regexp.MustCompile(`\bglpat-[0-9a-zA-Z_-]{27,300}\.[0-9a-z]{2}[0-9a-z]{7}\b`), + Entropy: 4, + Keywords: []string{"glpat-"}, + } + + // validate + tps := utils.GenerateSampleSecrets("gitlab", "glpat-"+secrets.NewSecret(utils.AlphaNumeric("27"))+"."+secrets.NewSecret(utils.AlphaNumeric("2"))+secrets.NewSecret(utils.AlphaNumeric("7"))) + fps := []string{ + "glpat-xxxxxxxx-xxxxxxxxxxxxxxxxxx.xxxxxxxxx", + } + return utils.Validate(r, tps, fps) +} + +func GitlabPipelineTriggerToken() *config.Rule { + r := config.Rule{ + RuleID: "gitlab-ptt", + Description: "Found a GitLab Pipeline Trigger Token, potentially compromising continuous integration workflows and project security.", + Regex: regexp.MustCompile(`glptt-[0-9a-f]{40}`), + Entropy: 3, + Keywords: []string{"glptt-"}, + } + + // validate + tps := utils.GenerateSampleSecrets("gitlab", "glptt-"+secrets.NewSecret(utils.Hex("40"))) + fps := []string{ + "glptt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + } + return utils.Validate(r, tps, fps) +} + +func GitlabRunnerRegistrationToken() *config.Rule { + r := config.Rule{ + RuleID: "gitlab-rrt", + Description: "Discovered a GitLab Runner Registration Token, posing a risk to CI/CD pipeline integrity and unauthorized access.", + Regex: regexp.MustCompile(`GR1348941[\w-]{20}`), + Entropy: 3, + Keywords: []string{"GR1348941"}, + } + + tps := utils.GenerateSampleSecrets("gitlab", "GR1348941"+secrets.NewSecret(utils.AlphaNumeric("20"))) + fps := []string{ + "GR134894112312312312312312312", + "GR1348941XXXXXXXXXXXXXXXXXXXX", + } + return utils.Validate(r, tps, fps) +} + +func GitlabRunnerAuthenticationToken() *config.Rule { + r := config.Rule{ + RuleID: "gitlab-runner-authentication-token", + Description: "Discovered a GitLab Runner Authentication Token, posing a risk to CI/CD pipeline integrity and unauthorized access.", + Regex: regexp.MustCompile(`glrt-[0-9a-zA-Z_\-]{20}`), + Entropy: 3, + Keywords: []string{"glrt-"}, + } + + tps := utils.GenerateSampleSecrets("gitlab", "glrt-"+secrets.NewSecret(utils.AlphaNumeric("20"))) + return utils.Validate(r, tps, nil) +} + +func GitlabRunnerAuthenticationTokenRoutable() *config.Rule { + r := config.Rule{ + RuleID: "gitlab-runner-authentication-token-routable", + Description: "Discovered a GitLab Runner Authentication Token (Routable), posing a risk to CI/CD pipeline integrity and unauthorized access.", + Regex: regexp.MustCompile(`\bglrt-t\d_[0-9a-zA-Z_\-]{27,300}\.[0-9a-z]{2}[0-9a-z]{7}\b`), + Entropy: 4, + Keywords: []string{"glrt-"}, + } + + tps := utils.GenerateSampleSecrets("gitlab", "glrt-t"+secrets.NewSecret(utils.Numeric("1"))+"_"+secrets.NewSecret(utils.AlphaNumeric("27"))+"."+secrets.NewSecret(utils.AlphaNumeric("2"))+secrets.NewSecret(utils.AlphaNumeric("7"))) + fps := []string{ + "glrt-tx_xxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxx", + } + + return utils.Validate(r, tps, fps) +} + +func GitlabScimToken() *config.Rule { + r := config.Rule{ + RuleID: "gitlab-scim-token", + Description: "Discovered a GitLab SCIM Token, posing a risk to unauthorized access for a organization or instance.", + Regex: regexp.MustCompile(`glsoat-[0-9a-zA-Z_\-]{20}`), + Entropy: 3, + Keywords: []string{"glsoat-"}, + } + + tps := utils.GenerateSampleSecrets("gitlab", "glsoat-"+secrets.NewSecret(utils.AlphaNumeric("20"))) + return utils.Validate(r, tps, nil) +} + +func GitlabSessionCookie() *config.Rule { + r := config.Rule{ + RuleID: "gitlab-session-cookie", + Description: "Discovered a GitLab Session Cookie, posing a risk to unauthorized access to a user account.", + Regex: regexp.MustCompile(`_gitlab_session=[0-9a-z]{32}`), + Entropy: 3, + Keywords: []string{"_gitlab_session="}, + } + + // validate + tps := utils.GenerateSampleSecrets("gitlab", "_gitlab_session="+secrets.NewSecret(utils.AlphaNumeric("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/gitter.go b/cmd/generate/config/rules/gitter.go new file mode 100644 index 0000000..776264e --- /dev/null +++ b/cmd/generate/config/rules/gitter.go @@ -0,0 +1,25 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func GitterAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "gitter-access-token", + Description: "Uncovered a Gitter Access Token, which may lead to unauthorized access to chat and communication services.", + Regex: utils.GenerateSemiGenericRegex([]string{"gitter"}, + utils.AlphaNumericExtendedShort("40"), true), + + Keywords: []string{ + "gitter", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("gitter", secrets.NewSecret(utils.AlphaNumericExtendedShort("40"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/gocardless.go b/cmd/generate/config/rules/gocardless.go new file mode 100644 index 0000000..87e8150 --- /dev/null +++ b/cmd/generate/config/rules/gocardless.go @@ -0,0 +1,25 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func GoCardless() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "gocardless-api-token", + Description: "Detected a GoCardless API token, potentially risking unauthorized direct debit payment operations and financial data exposure.", + Regex: utils.GenerateSemiGenericRegex([]string{"gocardless"}, `live_(?i)[a-z0-9\-_=]{40}`, true), + + Keywords: []string{ + "live_", + "gocardless", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("gocardless", "live_"+secrets.NewSecret(utils.AlphaNumericExtended("40"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/grafana.go b/cmd/generate/config/rules/grafana.go new file mode 100644 index 0000000..27ee4fc --- /dev/null +++ b/cmd/generate/config/rules/grafana.go @@ -0,0 +1,80 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func GrafanaApiKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "grafana-api-key", + Description: "Identified a Grafana API key, which could compromise monitoring dashboards and sensitive data analytics.", + Regex: utils.GenerateUniqueTokenRegex(`eyJrIjoi[A-Za-z0-9]{70,400}={0,3}`, true), + Entropy: 3, + Keywords: []string{"eyJrIjoi"}, + } + + // validate + tps := utils.GenerateSampleSecrets("grafana-api-key", "eyJrIjoi"+secrets.NewSecret(utils.AlphaNumeric("70"))) + return utils.Validate(r, tps, nil) +} + +func GrafanaCloudApiToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "grafana-cloud-api-token", + Description: "Found a Grafana cloud API token, risking unauthorized access to cloud-based monitoring services and data exposure.", + Regex: utils.GenerateUniqueTokenRegex(`glc_[A-Za-z0-9+/]{32,400}={0,3}`, true), + Entropy: 3, + Keywords: []string{"glc_"}, + } + + // validate + tps := utils.GenerateSampleSecrets("grafana-cloud-api-token", "glc_"+secrets.NewSecret(utils.AlphaNumeric("32"))) + tps = append(tps, + utils.GenerateSampleSecret("grafana-cloud-api-token", + "glc_"+ + secrets.NewSecret(utils.AlphaNumeric("32"))), + `loki_key: glc_eyJvIjoiNzQ0NTg3IiwibiI7InN0YWlrLTQ3NTgzMC1obC13cml0ZS1oYW5kc29uJG9raSIsImsiOiI4M2w3cmdYUlBoMTUyMW1lMU023nl5UDUiLCJtIjp7IOIiOiJ1cyJ9fQ==`, + // TODO: + //` loki: + //endpoint: https://322137:glc_eyJvIjoiNzQ0NTg3IiwibiI7InN0YWlrLTQ3NTgzMC1obC13cml0ZS1oYW5kc29uJG9raSIsImsiOiI4M2w3cmdYUlBoMTUyMW1lMU023nl5UDUiLCJtIjp7IOIiOiJ1cyJ9fQ==@logs-prod4.grafana.net/loki/api/v1/push`, + ) + fps := []string{ + // Low entropy. + `glc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`, + ` API_KEY="glc_111111111111111111111111111111111111111111="`, + // Invalid. + `static void GLC_CreateLightmapTextureArray(void); +static void GLC_CreateLightmapTexturesIndividual(void); + +void GLC_UploadLightmap(int textureUnit, int lightmapnum);`, + `// Alias models +void GLC_StateBeginUnderwaterAliasModelCaustics(texture_ref base_texture, texture_ref caustics_texture) +{`, + } + return utils.Validate(r, tps, fps) +} + +func GrafanaServiceAccountToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "grafana-service-account-token", + Description: "Discovered a Grafana service account token, posing a risk of compromised monitoring services and data integrity.", + Regex: utils.GenerateUniqueTokenRegex(`glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8}`, true), + Entropy: 3, + Keywords: []string{"glsa_"}, + } + + // validate + tps := utils.GenerateSampleSecrets("grafana-service-account-token", "glsa_"+secrets.NewSecret(utils.AlphaNumeric("32"))+"_"+secrets.NewSecret(utils.Hex("8"))) + tps = append(tps, + `'Authorization': 'Bearer glsa_pITqMOBIfNH2KL4PkXJqmTyQl0D9QGxF_486f63e1'`, + ) + fps := []string{ + "glsa_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_AAAAAAAA", + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/harness.go b/cmd/generate/config/rules/harness.go new file mode 100644 index 0000000..0f78888 --- /dev/null +++ b/cmd/generate/config/rules/harness.go @@ -0,0 +1,25 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func HarnessApiKey() *config.Rule { + // Define rule for Harness Personal Access Token (PAT) and Service Account Token (SAT) + r := config.Rule{ + Description: "Identified a Harness Access Token (PAT or SAT), risking unauthorized access to a Harness account.", + RuleID: "harness-api-key", + Regex: regexp.MustCompile(`(?:pat|sat)\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9]{24}\.[a-zA-Z0-9]{20}`), + Keywords: []string{"pat.", "sat."}, + } + + // Generate a sample secret for validation + tps := utils.GenerateSampleSecrets("harness", "pat."+secrets.NewSecret(utils.AlphaNumeric("22"))+"."+secrets.NewSecret(utils.AlphaNumeric("24"))+"."+secrets.NewSecret(utils.AlphaNumeric("20"))) + tps = append(tps, utils.GenerateSampleSecrets("harness", "sat."+secrets.NewSecret(utils.AlphaNumeric("22"))+"."+secrets.NewSecret(utils.AlphaNumeric("24"))+"."+secrets.NewSecret(utils.AlphaNumeric("20")))...) + + // validate the rule + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/hashicorp.go b/cmd/generate/config/rules/hashicorp.go new file mode 100644 index 0000000..81895bb --- /dev/null +++ b/cmd/generate/config/rules/hashicorp.go @@ -0,0 +1,58 @@ +package rules + +import ( + "fmt" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func HashiCorpTerraform() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "hashicorp-tf-api-token", + Description: "Uncovered a HashiCorp Terraform user/org API token, which may lead to unauthorized infrastructure management and security breaches.", + Regex: regexp.MustCompile(`(?i)[a-z0-9]{14}\.(?-i:atlasv1)\.[a-z0-9\-_=]{60,70}`), + Entropy: 3.5, + Keywords: []string{"atlasv1"}, + } + + // validate + tps := utils.GenerateSampleSecrets("hashicorpToken", secrets.NewSecret(utils.Hex("14"))+".atlasv1."+secrets.NewSecret(utils.AlphaNumericExtended("60,70"))) + tps = append(tps, + `#token = "hE1hlYILrSqpqh.atlasv1.ARjZuyzl33F71WR55s6ln5GQ1HWIwTDDH3MiRjz7OnpCfaCb1RCF5zGaSncCWmJdcYA"`, + ) + fps := []string{ + `token = "xxxxxxxxxxxxxx.atlasv1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"`, // low entropy + } + return utils.Validate(r, tps, fps) +} + +func HashicorpField() *config.Rule { + keywords := []string{"administrator_login_password", "password"} + // define rule + r := config.Rule{ + RuleID: "hashicorp-tf-password", + Description: "Identified a HashiCorp Terraform password field, risking unauthorized infrastructure configuration and security breaches.", + Regex: utils.GenerateSemiGenericRegex(keywords, fmt.Sprintf(`"%s"`, utils.AlphaNumericExtended("8,20")), true), + Entropy: 2, + Path: regexp.MustCompile(`(?i)\.(?:tf|hcl)$`), + Keywords: keywords, + } + + tps := map[string]string{ + // Example from: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/sql_server.html + "file.tf": "administrator_login_password = " + `"thisIsDog11"`, + // https://registry.terraform.io/providers/petoju/mysql/latest/docs + "file.hcl": "password = " + `"rootpasswd"`, + } + fps := map[string]string{ + "file.tf": "administrator_login_password = var.db_password", + "file.hcl": `password = "${aws_db_instance.default.password}"`, + "unrelated.js": "password = " + `"rootpasswd"`, + } + + return utils.ValidateWithPaths(r, tps, fps) +} diff --git a/cmd/generate/config/rules/hashicorp_vault.go b/cmd/generate/config/rules/hashicorp_vault.go new file mode 100644 index 0000000..c329689 --- /dev/null +++ b/cmd/generate/config/rules/hashicorp_vault.go @@ -0,0 +1,67 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func VaultServiceToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "vault-service-token", + Description: "Identified a Vault Service Token, potentially compromising infrastructure security and access to sensitive credentials.", + Regex: utils.GenerateUniqueTokenRegex(`(?:hvs\.[\w-]{90,120}|s\.(?i:[a-z0-9]{24}))`, false), + Entropy: 3.5, + Keywords: []string{"hvs.", "s."}, + Allowlists: []*config.Allowlist{ + { + Regexes: []*regexp.Regexp{ + // https://github.com/gitleaks/gitleaks/issues/1490#issuecomment-2334166357 + regexp.MustCompile(`s\.[A-Za-z]{24}`), + }, + }, + }, + } + + // validate + tps := []string{ + // Old + utils.GenerateSampleSecret("vault", secrets.NewSecret(`s\.[0-9][a-zA-Z0-9]{23}`)), + `token: s.ZC9Ecf4M5g9o34Q6RkzGsj0z`, + // New + utils.GenerateSampleSecret("vault", secrets.NewSecret(`hvs\.[0-9][\w\-]{89}`)), + `-vaultToken hvs.CAESIP2jTxc9S2K7Z6CtcFWQv7-044m_oSsxnPE1H3nF89l3GiYKHGh2cy5sQmlIZVNyTWJNcDRsYWJpQjlhYjVlb1cQh6PL8wEYAg"`, // longer than 100 chars + } + + fps := []string{ + // Old + ` credentials: new AWS.SharedIniFileCredentials({ profile: '' })`, // word boundary start + `INFO 4 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean`, // word boundary end + `s.xxxxxxxxxxxxxxxxxxxxxxxx`, // low entropy + `s.THISSTRINGISALLUPPERCASE`, // uppercase + `s.thisstringisalllowercase`, // lowercase + `s.AcceptanceTimeoutSeconds `, // pascal-case + `s.makeKubeConfigController = args`, // camel-case + // New + `hvs.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`, // low entropy + } + return utils.Validate(r, tps, fps) +} + +func VaultBatchToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "vault-batch-token", + Description: "Detected a Vault Batch Token, risking unauthorized access to secret management services and sensitive data.", + Regex: utils.GenerateUniqueTokenRegex(`hvb\.[\w-]{138,300}`, false), + Entropy: 4, + Keywords: []string{"hvb."}, + } + + // validate + tps := utils.GenerateSampleSecrets("vault", "hvb."+secrets.NewSecret(utils.AlphaNumericExtendedShort("138"))) + tps = append(tps, `hvb.AAAAAQJgxDgqsGNorpoOR7hPZ5SU-ynBvCl764jyRP_fnX7WvkdkDzGjbLNGdPdtlY33Als2P36yDZueqzfdGw9RsaTeaYXSH7E4RYSWuRoQ9YRKIw8o7mDDY2ZcT3KOB7RwtW1w1FN2eDqcy_sbCjXPaM1iBVH-mqMSYRmRd2nb5D1SJPeBzIYRqSglLc31wUGN7xEzyrKUczqOKsIcybQA`) // gitleaks:allow + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/heroku.go b/cmd/generate/config/rules/heroku.go new file mode 100644 index 0000000..436f067 --- /dev/null +++ b/cmd/generate/config/rules/heroku.go @@ -0,0 +1,45 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func Heroku() *config.Rule { + // define rule + r := config.Rule{ + Description: "Detected a Heroku API Key, potentially compromising cloud application deployments and operational security.", + RuleID: "heroku-api-key", + Regex: utils.GenerateSemiGenericRegex([]string{"heroku"}, utils.Hex8_4_4_4_12(), true), + + Keywords: []string{"heroku"}, + } + + // validate + tps := utils.GenerateSampleSecrets("heroku", secrets.NewSecret(utils.Hex8_4_4_4_12())) + tps = append(tps, + `const HEROKU_KEY = "12345678-ABCD-ABCD-ABCD-1234567890AB"`, // gitleaks:allow + `heroku_api_key = "832d2129-a846-4e27-99f4-7004b6ad53ef"`, // gitleaks:allow + ) + return utils.Validate(r, tps, nil) +} + +func HerokuV2() *config.Rule { + // define rule + r := config.Rule{ + Description: "Detected a Heroku API Key, potentially compromising cloud application deployments and operational security.", + RuleID: "heroku-api-key-v2", + Regex: utils.GenerateUniqueTokenRegex(`(HRKU-AA[0-9a-zA-Z_-]{58})`, false), + Entropy: 4, + Keywords: []string{"HRKU-AA"}, + } + + // validate + tps := utils.GenerateSampleSecrets("heroku", secrets.NewSecret(`\b(HRKU-AA[0-9a-zA-Z_-]{58})\b`)) + tps = append(tps, + `const KEY = "HRKU-AAlQ1aVoHDujJ9QsDHdHlHO0hbzhoERRSO45ZQusSYHg_____w4_hLrAym_u""`, + `API_Key = "HRKU-AAy9Ppr_HD2pPuTyIiTYInO0hbzhoERRSO93ZQusSYHgaD7_WQ07FnF7L9FX"`, + ) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/hubspot.go b/cmd/generate/config/rules/hubspot.go new file mode 100644 index 0000000..1c26f49 --- /dev/null +++ b/cmd/generate/config/rules/hubspot.go @@ -0,0 +1,26 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func HubSpot() *config.Rule { + // define rule + r := config.Rule{ + Description: "Found a HubSpot API Token, posing a risk to CRM data integrity and unauthorized marketing operations.", + RuleID: "hubspot-api-key", + Regex: utils.GenerateSemiGenericRegex([]string{"hubspot"}, + `[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}`, true), + + Keywords: []string{"hubspot"}, + } + + // validate + tps := utils.GenerateSampleSecrets("hubspot", secrets.NewSecret(utils.Hex8_4_4_4_12())) + tps = append(tps, + `const hubspotKey = "12345678-ABCD-ABCD-ABCD-1234567890AB"`, // gitleaks:allow + ) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/huggingface.go b/cmd/generate/config/rules/huggingface.go new file mode 100644 index 0000000..09cbac7 --- /dev/null +++ b/cmd/generate/config/rules/huggingface.go @@ -0,0 +1,116 @@ +package rules + +import ( + "fmt" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +// Reference: https://huggingface.co/docs/hub/security-tokens +// +// Old tokens have the prefix `api_`, however, I am not sure it's worth detecting them as that would be high noise. +// https://huggingface.co/docs/api-inference/quicktour +func HuggingFaceAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "huggingface-access-token", + Description: "Discovered a Hugging Face Access token, which could lead to unauthorized access to AI models and sensitive data.", + Regex: utils.GenerateUniqueTokenRegex("hf_(?i:[a-z]{34})", false), + Entropy: 2, + Keywords: []string{ + "hf_", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("huggingface", "hf_"+secrets.NewSecret("[a-zA-Z]{34}")) + tps = append(tps, + `huggingface-cli login --token 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`, + `huggingface-cli login --token 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`, + `huggingface-cli login --token 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`, + `huggingface-cli login --token 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`, + `huggingface-cli login --token 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`, + `huggingface-cli login --token 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`, + `huggingface-cli login --token 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`, + `huggingface-cli login --token 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`, + `-H "Authorization: Bearer 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"`, + `DEV=1 HF_TOKEN=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 poetry run python app.py`, + `use_auth_token='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')`, + `CI_HUB_USER_TOKEN = "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"`, + `- Change line 5 and add your Hugging Face token, that is, instead of 'hf_token = "ADD_YOUR_HUGGING_FACE_TOKEN_HERE"', you will need to change it to something like'hf_token = "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"'`, + //TODO: ` " hf_token = \"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\"\n",`, + `# Not critical, only usable on the sandboxed CI instance. + TOKEN = "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"`, + ` parser.add_argument("--hf_token", type=str, default='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', help="Hugging Face Access Token to access PyAnnote gated models")`, + ) + fps := []string{ + `- (id)hf_requiredCharacteristicTypesForDisplayMetadata;`, + `amazon.de#@#div[data-cel-widget="desktop-rhf_SponsoredProductsRemoteRHFSearchEXPSubsK2ClickPagination"]`, + ` _kHMSymptomhf_generatedByHomeAppForDebuggingPurposesKey,`, + ` #define OSCHF_DebugGetExpectedAverageCrystalAmplitude NOROM_OSCHF_DebugGetExpectedAverageCrystalAmplitude`, + ` M_UINT (ServingCellPriorityParametersDescription_t, H_PRIO, 2, &hf_servingcellpriorityparametersdescription_h_prio),`, + `+HWI-ST565_0092:4:1101:5508:5860#ACTTGA/1 + bb_eeeeegfgffhiiiiiiiiiiihiiiiicgafhf_eefghihhiiiifhifhhdhifhiiiihifdgdhggf\bbceceedbcd + @HWI-ST565_0092:4:1101:7621:5770#ACTTGA/1`, + `y{}x|~|}{~}}~|~}||�~|�{��|{}{|~z{}{{|{||{|}|{}{~|y}vjoePbUBJ7&;"; <; :;?!!;<7%$IACa_ecghbfbaebejhahfbhf_ddbficghbgfbhhcghdghfhigiifhhehhdggcgfchf_fgcei^[[.40&54"5666 6`, + ` change_dir(cwd) + subdirs = glob.glob('HF_CAASIMULIAComputeServicesBuildTime.HF*.Linux64') + if len(subdirs) == 1:`, + ` os.environ.get("HF_AUTH_TOKEN", + "hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"),`, + `# HuggingFace API Token https://huggingface.co/settings/tokens + HUGGINGFACE_API_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,`, + } + return utils.Validate(r, tps, fps) +} + +// Will be deprecated Aug 1st, 2023. +func HuggingFaceOrganizationApiToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "huggingface-organization-api-token", + Description: "Uncovered a Hugging Face Organization API token, potentially compromising AI organization accounts and associated data.", + Regex: utils.GenerateUniqueTokenRegex("api_org_(?i:[a-z]{34})", false), + + Entropy: 2, + Keywords: []string{ + "api_org_", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("huggingface", "api_org_"+secrets.NewSecret("[a-zA-Z]{34}")) + tps = append(tps, + `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`, + "`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`", + `\'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'\`, + //TODO: `\"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\"`, + //TODO: `,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,`, + //TODO: `(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)`, + //TODO: `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`, + `def test_private_space(self): + hf_token = "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" # Intentionally revealing this key for testing purposes + io = gr.load(`, + `hf_token = "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" # Intentionally revealing this key for testing purposes`, + `"news_train_dataset = datasets.load_dataset('nlpHakdang/aihub-news30k', data_files = \"train_news_text.csv\", use_auth_token='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')\n",`, + `os.environ['HUGGINGFACEHUB_API_TOKEN'] = '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'`, + fmt.Sprintf("api_org_%s", secrets.NewSecret(`[a-zA-Z]{34}`)), + ) + fps := []string{ + `public static final String API_ORG_EXIST = "APIOrganizationExist";`, + `const api_org_controller = require('../../controllers/api/index').organizations;`, + `API_ORG_CREATE("https://qyapi.weixin.qq.com/cgi-bin/department/create?access_token=ACCESS_TOKEN"),`, + `def test_internal_api_org_inclusion_with_href(api_name, href, expected, monkeypatch, called_with): + monkeypatch.setattr("requests.sessions.Session.request", called_with)`, + ` def _api_org_96726c78_4ae3_402f_b08b_7a78c6903d2a(self, method, url, body, headers): + body = self.fixtures.load("api_org_96726c78_4ae3_402f_b08b_7a78c6903d2a.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK]`, + `

You should see a token hf_xxxxx (old tokens are api_XXXXXXXX or api_org_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX).

`, + ` From Hugging Face docs: + You should see a token hf_xxxxx (old tokens are api_XXXXXXXX or api_org_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx). + If you do not submit your API token when sending requests to the API, you will not be able to run inference on your private models.`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/infracost.go b/cmd/generate/config/rules/infracost.go new file mode 100644 index 0000000..fba2355 --- /dev/null +++ b/cmd/generate/config/rules/infracost.go @@ -0,0 +1,42 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func InfracostAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "infracost-api-token", + Description: "Detected an Infracost API Token, risking unauthorized access to cloud cost estimation tools and financial data.", + Regex: utils.GenerateUniqueTokenRegex(`ico-[a-zA-Z0-9]{32}`, false), + Entropy: 3, + Keywords: []string{"ico-"}, + } + + // validate + tps := utils.GenerateSampleSecrets("ico", "ico-"+secrets.NewSecret("[A-Za-z0-9]{32}")) + tps = append(tps, + ` variable { + name = "INFRACOST_API_KEY" + secret_value = "ico-mlCr1Mn3SRcRiZMObUZOTHLcgtH2Lpgt" + is_secret = true + }`, + // TODO: New format with longer keys? + // ` headers = { + //'X-Api-Key': 'ico-EeDdSfctrmjD14f45f45te5gJ7l6lw4o6M36sXT62a6', + //'Content-Type': 'application/json', + //}`, + ) + fps := []string{ + // Low entropy + `ico-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`, + // Invalid + `http://assets.r7.com/assets/media_box_tv_tres_colunas/video_box.ico-7a388b69018576d24b59331fd60aab0c.png`, + `https://explosivelab.notion.site/Pianificazione-Nerdz-Ng-pubblico-1bc826ecc0994dd8915be97fc3489cde?pvs=74`, + `http://ece252-2.uwaterloo.ca:2540/image?q=gAAAAABdHkoqb9ZaJ3q4dlzEvTgG9WYwKcD9Aw7OUXeFicO-5M5IdNDjHBpKw7KBK3nCVqtuga4yzUaFEpJn8BqA1LzZprIJBw==`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/intercom.go b/cmd/generate/config/rules/intercom.go new file mode 100644 index 0000000..9a0eda2 --- /dev/null +++ b/cmd/generate/config/rules/intercom.go @@ -0,0 +1,22 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func Intercom() *config.Rule { + // define rule + r := config.Rule{ + Description: "Identified an Intercom API Token, which could compromise customer communication channels and data privacy.", + RuleID: "intercom-api-key", + Regex: utils.GenerateSemiGenericRegex([]string{"intercom"}, utils.AlphaNumericExtended("60"), true), + + Keywords: []string{"intercom"}, + } + + // validate + tps := utils.GenerateSampleSecrets("intercom", secrets.NewSecret(utils.AlphaNumericExtended("60"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/intra42.go b/cmd/generate/config/rules/intra42.go new file mode 100644 index 0000000..da6c61b --- /dev/null +++ b/cmd/generate/config/rules/intra42.go @@ -0,0 +1,31 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func Intra42ClientSecret() *config.Rule { + // define rule + r := config.Rule{ + Description: "Found a Intra42 client secret, which could lead to unauthorized access to the 42School API and sensitive data.", + RuleID: "intra42-client-secret", + Regex: utils.GenerateUniqueTokenRegex(`s-s4t2(?:ud|af)-(?i)[abcdef0123456789]{64}`, false), + Entropy: 3, + Keywords: []string{ + "intra", + "s-s4t2ud-", + "s-s4t2af-", + }, + } + + // validate + tps := []string{ + "clientSecret := \"s-s4t2ud-" + secrets.NewSecret(utils.Hex("64")) + "\"", + "clientSecret := \"s-s4t2af-" + secrets.NewSecret(utils.Hex("64")) + "\"", + "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", // gitleaks:allow + "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", // gitleaks:allow + } + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/jfrog.go b/cmd/generate/config/rules/jfrog.go new file mode 100644 index 0000000..db76e2e --- /dev/null +++ b/cmd/generate/config/rules/jfrog.go @@ -0,0 +1,64 @@ +package rules + +import ( + "fmt" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func JFrogAPIKey() *config.Rule { + keywords := []string{"jfrog", "artifactory", "bintray", "xray"} + + // Define Rule + r := config.Rule{ + // Human readable description of the rule + Description: "Found a JFrog API Key, posing a risk of unauthorized access to software artifact repositories and build pipelines.", + + // Unique ID for the rule + RuleID: "jfrog-api-key", + + // Regex capture group for the actual secret + + // Regex used for detecting secrets. See regex section below for more details + Regex: utils.GenerateSemiGenericRegex(keywords, utils.AlphaNumeric("73"), true), + + // Keywords used for string matching on fragments (think of this as a prefilter) + Keywords: keywords, + } + // validate + tps := []string{ + fmt.Sprintf("--set imagePullSecretJfrog.password=%s", secrets.NewSecret(utils.AlphaNumeric("73"))), + } + return utils.Validate(r, tps, nil) +} + +func JFrogIdentityToken() *config.Rule { + keywords := []string{"jfrog", "artifactory", "bintray", "xray"} + + // Define Rule + r := config.Rule{ + // Human readable description of the rule + Description: "Discovered a JFrog Identity Token, potentially compromising access to JFrog services and sensitive software artifacts.", + + // Unique ID for the rule + RuleID: "jfrog-identity-token", + + // Regex capture group for the actual secret + + // Regex used for detecting secrets. See regex section below for more details + Regex: utils.GenerateSemiGenericRegex(keywords, utils.AlphaNumeric("64"), true), + + // Keywords used for string matching on fragments (think of this as a prefilter) + Keywords: keywords, + } + + // validate + tps := utils.GenerateSampleSecrets("jfrog", secrets.NewSecret(utils.AlphaNumeric("64"))) + tps = append(tps, utils.GenerateSampleSecrets("artifactory", secrets.NewSecret(utils.AlphaNumeric("64")))...) + tps = append(tps, utils.GenerateSampleSecrets("bintray", secrets.NewSecret(utils.AlphaNumeric("64")))...) + tps = append(tps, utils.GenerateSampleSecrets("xray", secrets.NewSecret(utils.AlphaNumeric("64")))...) + tps = append(tps, fmt.Sprintf("\"artifactory\", \"%s\"", secrets.NewSecret(utils.AlphaNumeric("64")))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/jwt.go b/cmd/generate/config/rules/jwt.go new file mode 100644 index 0000000..0e93702 --- /dev/null +++ b/cmd/generate/config/rules/jwt.go @@ -0,0 +1,194 @@ +package rules + +import ( + b64 "encoding/base64" + "fmt" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func JWT() *config.Rule { + // define rule + r := config.Rule{ + Description: "Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.", + RuleID: "jwt", + Regex: utils.GenerateUniqueTokenRegex(`ey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/\\_-]{17,}\.(?:[a-zA-Z0-9\/\\_-]{10,}={0,2})?`, false), + Entropy: 3, + Keywords: []string{"ey"}, + } + + // validate + tps := utils.GenerateSampleSecrets("jwt", secrets.NewSecret(`ey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/\\_-]{17,}\.(?:[a-zA-Z0-9\/\\_-]{10,}={0,2})?`)) + tps = append(tps, + `eyJhbGciOieeeiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwic3ViZSI6IjEyMzQ1Njc4OTAiLCJuYW1lZWEiOiJKb2huIERvZSIsInN1ZmV3YWZiIjoiMTIzNDU2Nzg5MCIsIm5hbWVmZWF3ZnciOiJKb2huIERvZSIsIm5hbWVhZmV3ZmEiOiJKb2huIERvZSIsInN1ZndhZndlYWIiOiIxMjM0NTY3ODkwIiwibmFtZWZ3YWYiOiJKb2huIERvZSIsInN1YmZ3YWYiOiIxMjM0NTY3ODkwIiwibmFtZndhZSI6IkpvaG4gRG9lIiwiaWZ3YWZhYXQiOjE1MTYyMzkwMjJ9.a_5icKBDo-8EjUlrfvz2k2k-FYaindQ0DEYNrlsnRG0==`, // gitleaks:allow + `JWT := eyJhbGciOieeeiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwic3ViZSI6IjEyMzQ1Njc4OTAiLCJuYW1lZWEiOiJKb2huIERvZSIsInN1ZmV3YWZiIjoiMTIzNDU2Nzg5MCIsIm5hbWVmZWF3ZnciOiJKb2huIERvZSIsIm5hbWVhZmV3ZmEiOiJKb2huIERvZSIsInN1ZndhZndlYWIiOiIxMjM0NTY3ODkwIiwibmFtZWZ3YWYiOiJKb2huIERvZSIsInN1YmZ3YWYiOiIxMjM0NTY3ODkwIiwibmFtZndhZSI6IkpvaG4gRG9lIiwiaWZ3YWZhYXQiOjE1MTYyMzkwMjJ9.a_5icKBDo-8EjUlrfvz2k2k-FYaindQ0DEYNrlsnRG0`, // gitleaks:allow + `"access_token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJRMzFDVlMxUFNDSjRPVEsyWVZFTSIsImF0X2hhc2giOiI4amItZFE2OXRtZEVueUZaMUttNWhnIiwiYXVkIjoiZXhhbXBsZS1hcHAiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6IjE1OTQ2MDAxODIiLCJpYXQiOjE1OTQ1ODkzODQsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NTU1Ni9kZXgiLCJuYW1lIjoiYWRtaW4iLCJzdWIiOiJDaVF3T0dFNE5qZzBZaTFrWWpnNExUUmlOek10T1RCaE9TMHpZMlF4TmpZeFpqVTBOallTQld4dlkyRnMifQ.nrbzIJz99Om7TvJ04jnSTmhvlM7aR9hMM1Aqjp2ONJ1UKYCvegBLrTu6cYR968_OpmnAGJ8vkd7sIjUjtR4zbw"`, // gitleaks:allow + `https://dai2-playlistserver.aws.syncbak.com/cpl/20980038/dai2v5/1.0/7b2264657669636554797065223a387d/master.m3u8?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkdyYXkyMDE2MDgyOSJ9.eyJtaWQiOiIyMDk4MDAzOCIsImNpZCI6MjE5MDMsInNpZCI6MTU4LCJtZDUiOiIwN2QxMmRjNjAwOTM2MGI0MmY3NjNkNTRiMWIwZjI1NCIsImlhdCI6MTY2MDkxMzU2MCwiZXhwIjoxNjkyNDQ5NTYwLCJpc3MiOiJTeW5jYmFrIChURykifQ.JrWVgwzIn_RcNuWhkzIjr1i4qjXU1v4n0KFrSzoTQvQ`, // gitleaks:allow ` + `"SessionToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiI2TjJCQUxYN0VMTzgyN0RYUzNHSyIsImFjciI6IjAiLCJhdWQiOiJhY2NvdW50IiwiYXV0aF90aW1lIjoxNTY5OTEwNTUyLCJhenAiOiJhY2NvdW50IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJleHAiOjE1Njk5MTQ1NTQsImlhdCI6MTU2OTkxMDk1NCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgxL2F1dGgvcmVhbG1zL2RlbW8iLCJqdGkiOiJkOTk4YTBlZS01NDk2LTQ4OWYtYWJlMi00ZWE5MjJiZDlhYWYiLCJuYmYiOjAsInBvbGljeSI6InJlYWR3cml0ZSIsInByZWZlcnJlZF91c2VybmFtZSI6Im5ld3VzZXIxIiwic2Vzc2lvbl9zdGF0ZSI6IjJiYTAyYTI2LWE5MTUtNDUxNC04M2M1LWE0YjgwYjc4ZTgxNyIsInN1YiI6IjY4ZmMzODVhLTA5MjItNGQyMS04N2U5LTZkZTdhYjA3Njc2NSIsInR5cCI6IklEIn0._UG_-ZHgwdRnsp0gFdwChb7VlbPs-Gr_RNUz9EV7TggCD59qjCFAKjNrVHfOSVkKvYEMe0PvwfRKjnJl3A_mBA",`, // gitleaks:allow + `2020/11/04 21:08:40 Access Token: + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYTAwYzI3ZDEtYjVhYS00NjU0LWFmMTYtYjExNzNkZTY1NjI5Iiwicm9sZXMiOlsiYWRtaW4iXSwiaWF0IjoxNjA0NTE2OTIwLCJleHAiOjE2MDQ1MTc4MjAsImp0aSI6IjYzNmVmMDc0LTE2MzktNGJhZi1hNGNiLTQ4ZDM4NGMxMzliYSIsImlzcyI6Im15YXBwIn0.T9B0zG0AHShO5JfQgrMQBlToH33KHgp8nLMPFpN6QmM"`, // gitleaks:allow + `"idToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik56azVNREl5TVRnNFJqWTBORGswT0VJM1JrRXpORGN4UmtVMU1FWXdNemczT1VKQlFqRTBNZyJ9.eyJuaWNrbmFtZSI6InRlc3QtaW50ZXJhY3RpdmUtY2xpIiwibmFtZSI6IlRlc3RpbmcgSW50ZXJhY3RpdmUgQ2xpIiwicGljdHVyZSI6Imh0dHBzOi8vaW50ZXJhdGNpdmUuY2xpL3Rlc3RpbmcucG5nIiwidXBkYXRlZF9hdCI6IjIwMTktMDktMTZUMTU6MTg6NDMuOTk5WiIsImVtYWlsIjoidGVzdGluZ0BpbnRlcmFjdGl2ZS5jbGkiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6Ly9zZXJ2ZXJsZXNzaW5jLmF1dGgwLmNvbS8iLCJzdWIiOiJ0ZXN0LWludGVyYWN0aXZlLWNsaSIsImF1ZCI6IlhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYIiwiaWF0IjoxNTYwMDAwMDAwLCJleHAiOjMwMDAwMDAwMDB9.GcNQtWSxv9CHTABw-HIjYSvRxTEapDUDqIIWRGmz01XmShQxRGOHRuUg1NKU4w9MpOlB6txHKs8UWd2eZkzw_Z4QmIuLyAVhVklpWP2-xeysPLUyqVTgqAg8kgIUAwdKjmrdpQqHhGd-Q1BIX62-E-qKKx8prmADSw_hgmuvlMuSCa1ajCnfyUXycQxDmbFrvjd24lJER0FSpB2nWWW3KxZ_UBX-TuVmiEtRXg9GYeSv6oIU78PrIhYgJ0QjERRF1yAYamIXNRs-KZ7Z4YiFNC4uKzFH1524pZkS4Q0-pweIvBrrsjekz-vEYcbaVG1zAxDu_yNrYPk5phCy8MHTrQ",`, // gitleaks:allow + `TokenIssuer1WithAzp = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRUX3c5TFJOclk3d0phbEdzVFlTdDdydXRaaTg2R3Z5YzBFS1I0Q2FRQXciLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJiYXIiLCJleHAiOjQ3MzQxMjU0NTMsImlhdCI6MTU4MDUyNTQ1MywiaXNzIjoidGVzdC1pc3N1ZXItMUBpc3Rpby5pbyIsInN1YiI6InN1Yi0xIn0.SO4qjRJPYItkpGGpCDfEhaUfdthO8L9b_aawao4dJKyqqXN0uYdsJau_JZzyPQ1emAmJP7VyjwELrlszA6xV65na_O-eny23iwhEoroChQMpcr9DWqSUBUfpbHSPFAjUv38SUbQfLgar0HrMxQlTAzB0vyzn2g6-cukP469ZlOUmzvi9b4UpolTLp_WPgEHKjZw8CL56CcuJqBIKgfn0M7ta2bY_qx-UrsEW0CqnXol7vhXuDAfMeWZYKuDP8qc2VH1T6wpO2JnZ0EaNDuZfQLOWFYKsFGlaYcus9j462AfJQBSFQTbkIjkvKMK6aI_rMEesAnJr2eei1UYi14JYiQ"`, // gitleaks:allow + `eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjM1MzczOTExMDQsImdyb3VwcyI6WyJncm91cDEiLCJncm91cDIiXSwiaWF0IjoxNTM3MzkxMTA0LCJpc3MiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyIsInNjb3BlIjpbInNjb3BlMSIsInNjb3BlMiJdLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9.EdJnEZSH6X8hcyEii7c8H5lnhgjB5dwo07M5oheC8Xz8mOllyg--AHCFWHybM48reunF--oGaG6IXVngCEpVF0_P5DwsUoBgpPmK1JOaKN6_pe9sh0ZwTtdgK_RP01PuI7kUdbOTlkuUi2AO-qUyOm7Art2POzo36DLQlUXv8Ad7NBOqfQaKjE9ndaPWT7aexUsBHxmgiGbz1SyLH879f7uHYPbPKlpHU6P9S-DaKnGLaEchnoKnov7ajhrEhGXAQRukhDPKUHO9L30oPIr5IJllEQfHYtt6IZvlNUGeLUcif3wpry1R5tBXRicx2sXMQ7LyuDremDbcNy_iE76Upg`, // gitleaks:allow + `python examples/cli.py eyJhbGciOiJIUzI1NiJ9.eyJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkJhZFNlY3JldHMiLCJleHAiOjE1OTMxMzM0ODMsImlhdCI6MTQ2NjkwMzA4M30.ovqRikAo_0kKJ0GVrAwQlezymxrLGjcEiW_s3UJMMCo`, // gitleaks:allow + `"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw"`, // gitleaks:allow + `"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",`, // gitleaks:allow + `// authorization: 'eyJhbGciOiJIUzUxMiIsImlhdCI6MTU3Njk5Njc5OSwiZXhwIjoxNTg0ODU5MTk5fQ.eyJ1aWQiOjQ1NzQyN30.0ei5UE6OgLBzN2_IS7xUIbIfW_S1Wzl42q2UeusbboxuzvctO_4Mz6YRr6f0PBLUVZMETxt8F0_4-yqIJ3_kUQ',`, // gitleaks:allow + `eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJpc3Rpby1jbmktdG9rZW4tcGpwYnciLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiaXN0aW8tY25pIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiZmY2MDY0ODAtY2MxMC0xMWU4LTkxYzctMDAwYWY3MGE5YmE4Iiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Omt1YmUtc3lzdGVtOmlzdGlvLWNuaSJ9.0YHfuEwYn6tbtgy1YOhOjEtuQ8TnvmA_1RkuggfVGigMpCMGoOkIWwxpeDVXZm7dNwRmQVwLhchA3MeXz0QGRmStLa-VncedkmPOGSC-FyPvPybhZI53w3nhIVU3Vkh9_s-E2H2zFTwRQthxlDAlldNqEHpM9fINIVs0Z3bAogz2DYHwerSOtfZU-6d8b5nn73gnNhl6zBJ_0qg22SZjc6TrDYk--WwjUbU5_OIW6YxEmFnNVqfSeCrpg18IiJCsB0XRkixgu46Ev63jsrJ1vi41PVvBN79X7F-SiNNwTqwACRZvlX1zRw_GV7o4iPvnKn685WLOyMfoB5K6hSxrpQ`, // gitleaks:allow + `const TestKubernetesJWT_A = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImFkbWluLXRva2VuLXFsejQyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiNzM4YmMyNTEtNjUzMi0xMWU5LWI2N2YtNDhlNmM4YjhlY2I1Iiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6YWRtaW4ifQ.ixMlnWrAG7NVuTTKu8cdcYfM7gweS3jlKaEsIBNGOVEjPE7rtXtgMkAwjQTdYR08_0QBjkgzy5fQC5ZNyglSwONJ-bPaXGvhoH1cTnRi1dz9H_63CfqOCvQP1sbdkMeRxNTGVAyWZT76rXoCUIfHP4LY2I8aab0KN9FTIcgZRF0XPTtT70UwGIrSmRpxW38zjiy2ymWL01cc5VWGhJqVysmWmYk3wNp0h5N57H_MOrz4apQR4pKaamzskzjLxO55gpbmZFC76qWuUdexAR7DT2fpbHLOw90atN_NlLMY-VrXyW3-Ei5EhYaVreMB9PSpKwkrA4jULITohV-sxpa1LA"`, // gitleaks:allow + `string: grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJkZXYtdG8tYW5hbHl0aWMtYXBpLW1hYy10ZXN0QGRldnRvLTE3NTQxOS5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsImF1ZCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92NC90b2tlbiIsImV4cCI6MTUxOTIyOTAxOSwiaWF0IjoxNTE5MjI4ODk5LCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvYW5hbHl0aWNzLnJlYWRvbmx5In0.V8CSfSS7sKfoE5857jE9WDrGFHF1CyRr3cZpdUv9MjaaTcPRSLuNxB8yrxRP_7hNmlRgx_KdUzBgDJp3M_9tU4rZgFaIC7-bctvz_0rqbnMqSTniHYNGo7w__zO0bRaTpR3ILOfoxCQLcVC-tA4eCIMzRCznkY0VAaoLM7K-hnwQz6fCqSF31fmOwzAdVBPi5qnMETogh_7SiHn4WNUYI0FQf5SFLhcCbBZtORcbANe9hXp9po2P-VTBqs6u9dAZw5kZ2c1l5zbzrjYp5VcYl1XQFQTxP2zgMxhpX3k1UH9ObggOMUxvASyLbPZ7viOPKRlFxkAAHPTN2N1FYbpVeA`, // gitleaks:allow + `eyJhbGciOiJSUzI1NiIsImtpZCI6IlM1WGxrRnVIclJRaEVDbmg3cndZZFVTRTFpT0lfQzZsZ2NXbHZoOS1pbVUifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi11c2VyLXRva2VuLWo1a3B2Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluLXVzZXIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJiZTZjZmUwZS0yYzFhLTRkNTYtYmVkMC1jYWRmYjYxNzA1N2YiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQ6YWRtaW4tdXNlciJ9.LaBPEh6Qantd8tAc0X5DY9dDwUqZpxu38FHnp9TSJw-ghs3TsjrscFulUeEAtp2ng3ElLcU4SbNKPGJflF2dyW9Tmfn-Kt_6Jwq8HQ9GOCwAicEz0JVireHA7EWhATzuT56eO6MTe-2j5bpGnPQRJJtQ8AbtAN3nVK7RPjSzmc8Ppqx1z5i4oCGwiyRlGwqT-FkCtQLbQaQ4XmrASQoN4pJ_OBy5slztUhk32HdGP6pQx5c-nfei-of_4ij_fHrP0xEEfmVVvXqi9WKv1PLkQ3qTiSFDzv8M2sE4T6XmCGBbw7gyHzEGSpOAPZr00bX_YMCUvEF0lyP4YK696xWCBA`, // gitleaks:allow + `$ curl "http://admin:password@127.0.0.1:8080/api/v2/token" + {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiQVBJIl0sImV4cCI6MTYxMzMzNTI2MSwianRpIjoiYzBrb2gxZmNkcnBjaHNzMGZwZmciLCJuYmYiOjE2MTMzMzQ2MzEsInBlcm1pc3Npb25zIjpbIioiXSwic3ViIjoiYUJ0SHUwMHNBUmxzZ29yeEtLQ1pZZWVqSTRKVTlXbThHSGNiVWtWVmc1TT0iLCJ1c2VybmFtZSI6ImFkbWluIn0.WiyqvUF-92zCr--y4Q_sxn-tPnISFzGZd_exsG-K7ME","expires_at":"2021-02-14T20:41:01Z"}`, // gitleaks:allow + `curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiQVBJIl0sImV4cCI6MTYxMzMzNTI2MSwianRpIjoiYzBrb2gxZmNkcnBjaHNzMGZwZmciLCJuYmYiOjE2MTMzMzQ2MzEsInBlcm1pc3Npb25zIjpbIioiXSwic3ViIjoiYUJ0SHUwMHNBUmxzZ29yeEtLQ1pZZWVqSTRKVTlXbThHSGNiVWtWVmc1TT0iLCJ1c2VybmFtZSI6ImFkbWluIn0.WiyqvUF-92zCr--y4Q_sxn-tPnISFzGZd_exsG-K7ME" "http://127.0.0.1:8080/api/v2/dumpdata?output-data=1"`, // gitleaks:allow + `"authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZ3Vlc3QiLCJzdWIiOiJZV3hwWTJVPSIsIm5iZiI6MTUxNDg1MTEzOSwiZXhwIjoxNjQxMDgxNTM5fQ.K5DnnbbIOspRbpCr2IKXE9cPVatGOCBrBQobQmBmaeU"`, // gitleaks:allow + `{"signatures": [ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaWxlcyI6W3sibmFtZSI6Ii5tYW5pZmVzdCIsImhhc2giOiJjMjEzMTU0NGM3MTZhMjVhNWUzMWY1MDQzMDBmNTI0MGU4MjM1Y2FkYjlhNTdmMGJkMWI2ZjRiZDc0YjI2NjEyIiwiYWxnb3JpdGhtIjoiU0hBMjU2In0seyJuYW1lIjoicm9sZXMvYmluZGluZ3MvZGF0YS5qc29uIiwiaGFzaCI6IjQyY2ZlNjc2OGI1N2JiNWY3NTAzYzE2NWMyOGRkMDdhYzViODEzNTU0ZWJjODUwZjJjYzM1ODQzZTcxMzdiMWQifV0sImlhdCI6MTU5MjI0ODAyNywiaXNzIjoiSldUU2VydmljZSIsImtleWlkIjoibXlQdWJsaWNLZXkiLCJzY29wZSI6IndyaXRlIn0.ZjtUgXC6USwmhv4XP9gFH6MzZwpZrGpAL_2sTK1P-mg"]}`, // gitleaks:allow + `"id_token": "eyJ4NXQiOiJOVEF4Wm1NeE5ETXlaRGczTVRVMVpHTTBNekV6T0RKaFpXSTRORE5sWkRVMU9HRmtOakZpTVEiLCJraWQiOiJOVEF4Wm1NeE5ETXlaRGczTVRVMVpHTTBNekV6T0RKaFpXSTRORE5sWkRVMU9HRmtOakZpTVEiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJQb0VnWFA2dVZPNDVJc0VOUm5nRFhqNUF1NVlhIiwiYXpwIjoiUG9FZ1hQNnVWTzQ1SXNFTlJuZ0RYajVBdTVZYSIsImlzcyI6Imh0dHBzOlwvXC9sb2NhbGhvc3Q6OTQ0M1wvb2F1dGgyXC90b2tlbiIsImV4cCI6MTUzNDg5MTc3OCwiaWF0IjoxNTM0ODg4MTc4LCJqdGkiOiIxODQ0MzI5Yy1kNjVhLTQ4YTMtODIyOC05ZGY3M2ZlODNkNTYifQ.ELZ8ujk2Xp9xTGgMqnCa5ehuimaAPXWlSCW5QeBbTJIT4M5OB_2XEVIV6p89kftjUdKu50oiYe4SbfrxmLm6NGSGd2qxkjzJK3SRKqsrmVWEn19juj8fz1neKtUdXVHuSZu6ws_bMDy4f_9hN2Jv9dFnkoyeNT54r4jSTJ4A2FzN2rkiURheVVsc8qlm8O7g64Az-5h4UGryyXU4zsnjDCBKYk9jdbEpcUskrFMYhuUlj1RWSASiGhHHHDU5dTRqHkVLIItfG48k_fb-ehU60T7EFWH1JBdNjOxM9oN_yb0hGwOjLUyCUJO_Y7xcd5F4dZzrBg8LffFmvJ09wzHNtQ",`, // gitleaks:allow + ` # The following default key is generated by the local Supabase start and doesn't change + - SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0`, // gitleaks:allow + `Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJiIjoyLCJjIjozfQ.hxhGCCCmGV9nT1slief1WgEsOsfdnlVizNrODxfh1M8`, // gitleaks:allow + `--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJhY3RvclR5cGUiOiJVU0VSIiwiYWN0b3JJZCI6ImRhdGFodWIiLCJ0eXBlIjoiUEVSU09OQUwiLCJ2ZXJzaW9uIjoiMSIsImV4cCI6MTY1MDY2MDY1NSwianRpIjoiM2E4ZDY3ZTItOTM5Yi00NTY3LWE0MjYtZDdlMDA1ZGU3NjJjIiwic3ViIjoiZGF0YWh1YiIsImlzcyI6ImRhdGFodWItbWV0YWRhdGEtc2VydmljZSJ9.pp_vW2u1tiiTT7U0nDF2EQdcayOMB8jatiOA8Je4JJA' \`, // gitleaks:allow + `"Cookie": "auth-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NDIxMTEsImlzQWRtaW4iOmZhbHNlLCJnaXRodWJJZCI6ImR5ZXhwbG9kZSIsImFwcFJvbGVzIjpbInVzZXIiXSwicm9sZXMiOnsiNDciOiJzdHVkZW50IiwiNTYiOiJzdHVkZW50In0sImNvdXJzZXNSb2xlcyI6eyI0NyI6WyJzdHVkZW50Il0sIjU2IjpbInN0dWRlbnQiXX0sImNvdXJzZXMiOnsiNDciOnsibWVudG9ySWQiOm51bGwsInN0dWRlbnRJZCI6NjY3OTMsInJvbGVzIjpbInN0dWRlbnQiXX0sIjU2Ijp7Im1lbnRvcklkIjpudWxsLCJzdHVkZW50SWQiOjYzMzk4LCJyb2xlcyI6WyJzdHVkZW50Il19fSwiaWF0IjoxNjQ1MjA5NzI2LCJleHAiOjE2NDUzODI1MjZ9.btpYeSioEDUNMI6bAPqgu5zndA8XR5DT8P9U9kotOEA",`, // gitleaks:allow + `
  • + 在线演示
  • `, // gitleaks:allow + `eyJhbGciOiJSUzI1NiIsImtpZCI6IlM1WGxrRnVIclJRaEVDbmg3cndZZFVTRTFpT0lfQzZsZ2NXbHZoOS1pbVUifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi11c2VyLXRva2VuLWo1a3B2Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluLXVzZXIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJiZTZjZmUwZS0yYzFhLTRkNTYtYmVkMC1jYWRmYjYxNzA1N2YiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQ6YWRtaW4tdXNlciJ9.LaBPEh6Qantd8tAc0X5DY9dDwUqZpxu38FHnp9TSJw-ghs3TsjrscFulUeEAtp2ng3ElLcU4SbNKPGJflF2dyW9Tmfn-Kt_6Jwq8HQ9GOCwAicEz0JVireHA7EWhATzuT56eO6MTe-2j5bpGnPQRJJtQ8AbtAN3nVK7RPjSzmc8Ppqx1z5i4oCGwiyRlGwqT-FkCtQLbQaQ4XmrASQoN4pJ_OBy5slztUhk32HdGP6pQx5c-nfei-of_4ij_fHrP0xEEfmVVvXqi9WKv1PLkQ3qTiSFDzv8M2sE4T6XmCGBbw7gyHzEGSpOAPZr00bX_YMCUvEF0lyP4YK696xWCBA`, // gitleaks:allow + `eyJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6IlRlc3QiLCJsb2dnZWQtaW4tdXNlciI6eyJzY29wZWRQZXJtaXNzaW9uIjpbXSwicGVybWlzc2lvbnMiOlsiQS5hZG1pbl9jdXN0b21lcl9kZWxldGUiLCJBLm5vcm1hbF91c2VyX2FwcCIsIkEubm9ybWFsX3VzZXJfY29uZmlndXJhdGlvbiIsIkEubm9ybWFsX3VzZXJfd2VsY29tZV9jb250cm9scyIsIkEubm9ybWFsX3VzZXJfb3JkZXIiLCJBLm5vcm1hbF91c2VyX3NlYXJjaCIsIkEubm9ybWFsX3VzZXJfc2VhcmNoX3BjIiwiQS5ub3JtYWxfdXNlcl9zZWFyY2hfcHJpdmF0ZSIsIkEubm9ybWFsX3VzZXJfcHJpY2luZyIsIkEubm9ybWFsX3VzZXJfcHJpdmF0ZSIsIkEubm9ybWFsX3VzZXJfY29tbWVyY2lhbCIsIkEubm9ybWFsX3VzZXJfcGMiXSwibmFtZSI6WyJUZXN0Il0sIm1haWwiOlsiVGVzdEBleGFtcGxlLmNvbSJdLCJvcmdhbml6YXRpb24iOlsiWC1YeHh4eHguMTIzNCJdLCJsb2NhdGlvbiI6WyIxMjMyMSJdLCJ1bml0IjpbIjEwMyJdLCJjb3VudHJ5IjpbIkNOIl0sInVzZXJUeXBlIjoiZW1wbG95ZWUifSwiaWRlbnRpdHktaWQiOiJNb2NrIn0.EK5TbwsIgde3mT3n7NK2W7TCvpgQQLzshvPPANRQeUmKOv2AWbo_7vNEDTSkwUlaHRN3-dknv8F95p5MsGTzH6Uva8aOPJG6JdBIoYX_ud3aBN-hY1i2Xpf8pqjeINfY3_gDNAB9gdMznEej2uqhPwUXmZtcuWPdUCCeNqPJbRUAJeVXxLr_JtQzO2jmuwNY_YYp7KaEIANZwG1spvLuIGZ0HA03u8ye9c2lfqYcjgfIkjMrwgWPamR7joZOZPdQSO2EHrF7bUWMjRNY-FF5V7tOjEijkknE_nDq5THcEvx1seHYFdFNwy9LSSGGPVmZMKTKQ3UUlZZyBMXcOpOA9w`, // gitleaks:allow + // TODO: Detect newlines or escapes (\)? + // https://github.com/mongodb/mongo/blob/1960b792ade4e179ddc6113a3cd400e9492ca11d/src/mongo/crypto/README.JWT.md?plain=1#L115-L117 + // TODO: Detect empty claims section? + // `eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IllUcEY3bGtTc3JvZVVUVFdCb21LNzBTN0FhVTJyc0ptMURpZ1ZzbjRMY2F5eUxFNFBabldkYmFVcE9jQVV5a1ciLCJ5IjoiLU5pS3loUktjSk52Nm02Z0ZJUWc4cy1Xd1VXUW9uT3A5dkQ4cHpoa2tUU3U2RzFlU2FUTVlhZGltQ2Q4V0ExMSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ`, + `String tokenWithNoneAlg = "eyJhbGciOiJub25lIn0.eyJzdWIiOiJ0ZXN0LXVzZXIifQ.";`, // gitleaks:allow + `# Req: Invoke-RestMethod -Uri 'http://localhost:8085/users' -Headers @{ 'X-API-KEY' = 'eyJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6Im1vcnR5Iiwic3ViIjoiMTIzIn0.' }`, // gitleaks:allow + ) + fps := []string{} + return utils.Validate(r, tps, fps) +} + +func JWTBase64() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "jwt-base64", + Description: "Detected a Base64-encoded JSON Web Token, posing a risk of exposing encoded authentication and data exchange information.", + Regex: regexp.MustCompile( + `\bZXlK(?:(?PaGJHY2lPaU)|(?PaGNIVWlPaU)|(?PaGNIWWlPaU)|(?PaGRXUWlPaU)|(?PaU5qUWlP)|(?PamNtbDBJanBi)|(?PamRIa2lPaU)|(?PbGNHc2lPbn)|(?PbGJtTWlPaU)|(?PcWEzVWlPaU)|(?PcWQyc2lPb)|(?PcGMzTWlPaU)|(?PcGRpSTZJ)|(?PcmFXUWlP)|(?PclpYbGZiM0J6SWpwY)|(?PcmRIa2lPaUp)|(?PdWIyNWpaU0k2)|(?Pd01tTWlP)|(?Pd01uTWlPaU)|(?Pd2NIUWlPaU)|(?PemRXSWlPaU)|(?PemRuUWlP)|(?PMFlXY2lPaU)|(?PMGVYQWlPaUp)|(?PMWNtd2l)|(?PMWMyVWlPaUp)|(?PMlpYSWlPaU)|(?PMlpYSnphVzl1SWpv)|(?PNElqb2)|(?PNE5XTWlP)|(?PNE5YUWlPaU)|(?PNE5YUWpVekkxTmlJNkl)|(?PNE5YVWlPaU)|(?PNmFYQWlPaU))[a-zA-Z0-9\/\\_+\-\r\n]{40,}={0,2}`), + Entropy: 2, + Keywords: []string{"zxlk"}, + } + + tps := generateTestValues() + fps := []string{ + `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG/MacGPG2 v2 +Comment: GPGTools - https://gpgtools.org + +mQENBFOrMNcBCAC+gLI4s3bUkobS5NpOQbWfjWXbqC0Ixpc5bZYDOvsmfstmswna +UWUXkRH9RONabzrAu4TGvW0f5DkC2fuWWHJhZWEccn+VE83+avMZN4/mzldSXPNX +A6F7+wHb1DjG+FCDcxMghkwDjGc16LOtZGufUo5iRQaC5pmNBOgDWdiObGPKOTEL +/uU8zLtKi2cibbkhRm22IGOzGyZMZN6zvEtPzlCp3eZEGMW0Ig+kbl6SaSDrSJNK +wElYcr/kJ9QF6CQ2iwZCGeL2jH5QaOi5uj1LXONpCd9nPeyDXc+Z20gXZiqkwRLc +IBPKza6hq/+4nwHBq8DNLv0W4xNC59jLbIhpABEBAAG0LEZyYW5rIE5vdGhhZnQg +PGZub3RoYWZ0QGFsdW1uaS5zdGFuZm9yZC5lZHU+iQE9BBMBCgAnBQJTqzDXAhsD +BQkHhh+ABQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJECqYOYG1w7FJOr4IAK8x +ec4jbjd6jkKe0YJGdPzg6TM73ISV5VrUlJX7O3jgxHB4M8KIHN/8A/+ZxLk7WM96 +iq0C8atWHkCkQBtNduWhzAFccQlpxrrb18T5/oItcrmX9Dx2H5WeIl4WAoqe0MTk +iMPv29RMMH9RJvXL0ihuuH4Z0VxV5nurI9QmGzG69QOzfP9qY1EfQEceO7OqXXvY +vvBEUbmWshLuHJ6tOQY5ib3+aLO7m+yJTgJ7s6DHBDqLhqJPW0g7jiJcrDhYXsoS +JMMhMdUhckeZfTXm1N3Dc+/t9/E8NZjdZ2q/ZZzvygC4mu0uihwBKFqoFvXCHRaW +Rf4uYxE+/Upna/mbXQi5AQ0EU6sw1wEIALSp9Cc4t/F2k+rwEfEMXihXLcLM9Dmh +ukz++kMSCSuq4QHE+I4rLda/lVSNJCXaXrGVkzJuzmpEeQFdhr6nLW9ZYhzK8FIc +YyfsYTQxXUVf5W4e/XfKNoG9lrwQd5XHxJTBJ57XjjoWJYPQ69NWH8622foOBpux +xewgR2LEFgl+ksu7aQL4cQif6D3dko3EiIf1t0LDBXxFEREFCg+vkDFsDIW5bdaI +mDYwewGj7dAPLuo0sx1We+uBxb+j30xw/ASDOBhO3ratQWs+4w2FC7gw7cuf0CJC +/hMEw8nloGsIbqBmAnLdlQBxkfFG9DqRBNdSUM8xI66F0eGaaPHJ/S8AEQEAAYkB +JQQYAQoADwUCU6sw1wIbDAUJB4YfgAAKCRAqmDmBtcOxSZvvB/4w6S5YZQUmDVYK +9HPOm49qWxGPd5dLHr2g388sJ4LDK98Q9oicHgf2R35OXTyqhv4kFJL3eukQ6oLW +QOqQKLRycrQUu7eSESAiVmJ1gbuXLAWJmvUABGSYzj3BjWQRexYW/MZ51XqNftF9 +oOSAoFWrdqeJbHoxPTXIb6P8EWk4Ei2j2bSIfBBSbBMSB6Kfk9IjpISM8+97RS5o +6605rmM5MY1Lz8cq8AZIYJs/MnUCZrpQ0c9QSWrflHAeWAy6xMGfdSynbEExQ4z8 +AIfKWMro74nRFbAnv/BzrF2R8uHmdE7T6q9ZZsAydlaxQbdmyhCeGAdw93CwAypb +vHVobJ8A +=mIC1 +-----END PGP PUBLIC KEY BLOCK-----`, + } + return utils.Validate(r, tps, fps) +} + +func generateTestValues() []string { + // Sample JWT from RFC-7519 + jwtSuffix := ".eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" + // Validate known header parameters + // https://www.iana.org/assignments/jose/jose.xhtml + headers := map[string][]string{ + "alg": { + `"ES256"`, + `"ES384"`, + `"ES512"`, + `"ECDH-ES"`, + `"HS256"`, + `"HS384"`, + `"HS512"`, + `"RS256"`, + `"RS384"`, + `"RS512"`, + `"PS256"`, + `"PS384"`, `"PS512"`, + `"none"`, + // Nonstandard + `"A128KW"`, + `"ECDH-ES+A256KW"`, + `"RSA-OAEP"`, + }, + "apu": {`"Tx9qG69ZfodhRos-8qfhTPc6ZFnNUcgNDVdHqX1UR3s"`, `"RfXdxTfIzilWBzWWX3ovHBDzgDcLNy0BFJWSxa0dqnw"`}, + "apv": {`"ZGlkOmVsZW06cm9wc3RlbjpFa"`, `"ZGlkOmtleTp6Nk1rak1TWWF1dU1neDlhekV5VW5UVzVvNWpGUmtiQnU1VDgzZjM5dU53bnNHbW0jejZMU29HdFpTclVNWnVkQWFnekVmWWY3azhqSFpjR0Q3OVNveDd2NHdDa0RLTlN3"`}, + "aud": {`"https://vault.example.com"`, `"http://example.com"`}, + "b64": {`true`, `false`}, + "crit": {`["exp"]`}, + "cty": {`"example"`, `"json"`}, + "epk": {`{"crv":"X25519","kty":"OKP","x":"Tx9qG69ZfodhRos-8qfhTPc6ZFnNUcgNDVdHqX1UR3s"}`, `{"kty":"OKP","crv":"X25519","x":"RfXdxTfIzilWBzWWX3ovHBDzgDcLNy0BFJWSxa0dqnw"}`}, + "enc": {`"A256GCM"`, `"A256CBC-HS512"`, `"A128CBC-HS256"`}, + "jku": {`"https://c2id.com/jwks.json"`}, + "jwk": {`{"crv":"P-256","kid":"DB2X:GSG2:72H3:AE3R:KCMI:Y77E:W7TF:ERHK:V5HR:JJ2Y:YMS6:HFGJ","kty":"EC","x":"jyr9-xZBorSC9fhqNsmfU_Ud31wbaZ-bVGz0HmySvbQ","y":"vkE6qZCCvYRWjSUwgAOvibQx_s8FipYkAiHS0VnAFNs"}`, `{"kty":"RSA","n":"jyTwiSJACtW_SW-aiihQS5Y5QR704zUwjhlevY0oK-y5wP7SlIc2hq2OPVRarCzjhOxZl2AQFzM5VCR7xRDcnIn9t_pl7Mgsnx9hKDS9yQ24YXzhQ4cMEVVuqwcHvXqPdWDSoCZ1ccMqiiPyBSNGQTXMPY5PBxMOR47XwOb4eNMOPqnzVio3MEtL2wphtEonP3MY6pxJJzzel04wSCRZ4n06reqwER3KwRFPnRpRxAgmSEot5IBLIT3jj-amT5sD7YoUDbPmLk23zgDBIhX88fkClilg1W-fUi1XxYZomEPGvV7OrE1yszt4YDPqKgjJT8t2JPy__1ri-8rZgSxn5Q","e":"AQAB"`}, + "iss": {`"http://localhost:8087/realms/grafana"`, `"kubernetes/serviceaccount"`}, + "iv": {`"zjJPRrj0TGez9JYkChTrB3iqKoDkiBhn"`, `"10PlAIteHLVABtt"`}, + "kid": {`"my_key_id"`, `"did:example:123#zC1Rnuvw9rVa6E5TKF4uQVRuQuaCpVgB81Um2u17Fu7UK"`}, + "key_ops": {`["sign"]`, `["decrypt"]`, `["encrypt"]`}, + "kty": {`"EC"`, `"RSA"`}, + "nonce": {`"Os_sBjfWzVZenwwjvLrwXA"`, `"LDDZAGcBuKYpuNlFTCxPYw"`}, + "p2c": {`4096`, `1000`}, + "p2s": {`"c_ORk4HSsqZD2LvVeCUHqg"`}, + "ppt": {`"foo"`}, + "sub": {`"project_path:my-group/my-project:ref_type:branch:ref:feature-branch-1"`, `"1234567890"`}, + // I cannot find any real-world examples of the svt header + // and the RFC doesn't seem to explicitly state the content. + "svt": {``}, + "tag": {`"h6mJqHn33oCsDd5X57MI-g"`}, + "typ": {`"JWT"`, `"JWK"`, `"JSOE"`, `"JSON"`, `"plain"`}, + "url": {`"https://example.com"`}, + "use": {`"enc"`, `"sig"`}, + "ver": {`"2"`, `"3"`}, + "version": {`"2"`, `"3"`}, + "x5c": {`["MIICmzCCAYMCBgF4HR7HNDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwMzEwMTcwOTE5WhcNMzEwMzEwMTcxMDU5WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCpLzXHp8i09R2HU5YJPyncC4tiAWWmaDrVZenqynWlWKOjIXb0Y5JoP3ET68u16Bf7mHQGc/u9rRvCw4A92HpJ15WyUSJ80YcK0gPTE0Woc1ZxdK3h4t9AoA8VSrROwQ77w/VAdGrrJ4bwkAVrFqSRpqAsW5XxV3/bU8YkVaG8mh0kuFf/5ib0vxdSkg+mz+ZCuIxQ5YN77kNaMecO19XuaBo7FsG9WjfCPxXYuajkuLgdptPgwTN4np70h0WjaSP/jhjL2ixf48w+27wFDP+ic+B/TCOtVa3fj1GPo6RLxHGU0Zh64jFhmvRM6E/kX7IQ+FJcOwp1VPA9/vMABopAgMBAAEwDQYJKoZIhvcNAQELBQADggEBALILq1Z4oQNJZEUt24VZcvknsWtQtvPxl3JNcBQgDR5/IMgl5VndRZ9OT56KUqrR5xRsWiCvh5Lgv4fUEzAAo9ToiPLub1SKP063zWrvfgi3YZ19bty0iXFm7l2cpQ3ejFV7WpcdLJE0lapFdPLo6QaRdgNu/1p4vbYg7zSK1fQ0OY5b3ajhAx/bhWlrN685owRbO5/r4rUOa6oo9l4Qn7jUxKUx4rcoe7zUM7qrpOPqKvn0DBp3n1/+9pOZXCjIfZGvYwP5NhzBDCkRzaXcJHlOqWzMBzyovVrzVmUilBcj+EsTYJs0gVXKzduX5zO6YWhFs23lu7AijdkxTY65YM0="]`}, + "x5t": {`"IYIeevIT57t8ppUejM42Bqx6f3I"`}, + "x5t#S256": {`"TuOrBy2NcTlFSWuZ8Kh8W8AjQagb4fnfP1SlKMO8-So"`}, + "x5u": {`"https://tel.example.org/passport.cer"`}, + "zip": {`"DEF"`}, + } + + var examples []string + for key, values := range headers { + for _, value := range values { + header := fmt.Sprintf(`{"%s":%s}`, key, value) + jwt := []byte(b64.RawURLEncoding.EncodeToString([]byte(header)) + jwtSuffix) + examples = append(examples, b64.StdEncoding.EncodeToString(jwt)) + examples = append(examples, b64.RawStdEncoding.EncodeToString(jwt)) + examples = append(examples, b64.URLEncoding.EncodeToString(jwt)) + examples = append(examples, b64.RawURLEncoding.EncodeToString(jwt)) + } + } + return examples +} diff --git a/cmd/generate/config/rules/kraken.go b/cmd/generate/config/rules/kraken.go new file mode 100644 index 0000000..c5678cf --- /dev/null +++ b/cmd/generate/config/rules/kraken.go @@ -0,0 +1,25 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func KrakenAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "kraken-access-token", + Description: "Identified a Kraken Access Token, potentially compromising cryptocurrency trading accounts and financial security.", + Regex: utils.GenerateSemiGenericRegex([]string{"kraken"}, + utils.AlphaNumericExtendedLong("80,90"), true), + + Keywords: []string{ + "kraken", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("kraken", secrets.NewSecret(utils.AlphaNumericExtendedLong("80,90"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/kubernetes.go b/cmd/generate/config/rules/kubernetes.go new file mode 100644 index 0000000..95e8f42 --- /dev/null +++ b/cmd/generate/config/rules/kubernetes.go @@ -0,0 +1,463 @@ +package rules + +import ( + "fmt" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +// KubernetesSecret validates if we detected a kubernetes secret which contains data! +func KubernetesSecret() *config.Rule { + // Only match basic variations of `kind: secret`, we don't want things like `kind: ExternalSecret`. + //language=regexp + kindPat := `\bkind:[ \t]*["']?\bsecret\b["']?` + // Only matches values (`key: value`) under `data:` that are: + // - valid base64 characters + // - longer than 10 characters (no "YmFyCg==") + //language=regexp + dataPat := `\bdata:(?s:.){0,100}?\s+([\w.-]+:(?:[ \t]*(?:\||>[-+]?)\s+)?[ \t]*(?:["']?[a-z0-9+/]{10,}={0,3}["']?|\{\{[ \t\w"|$:=,.-]+}}|""|''))` + + // define rule + r := config.Rule{ + RuleID: "kubernetes-secret-yaml", + Description: "Possible Kubernetes Secret detected, posing a risk of leaking credentials/tokens from your deployments", + Regex: regexp.MustCompile(fmt.Sprintf( + //language=regexp + `(?i)(?:%s(?s:.){0,200}?%s|%s(?s:.){0,200}?%s)`, kindPat, dataPat, dataPat, kindPat)), + Keywords: []string{ + "secret", + }, + // Kubernetes secrets are usually yaml files. + Path: regexp.MustCompile(`(?i)\.ya?ml$`), + Allowlists: []*config.Allowlist{ + { + Regexes: []*regexp.Regexp{ + // Ignore empty or placeholder values. + // variable: {{ .Values.Example }} (https://helm.sh/docs/chart_template_guide/variables/) + // variable: "" + // variable: '' + regexp.MustCompile(`[\w.-]+:(?:[ \t]*(?:\||>[-+]?)\s+)?[ \t]*(?:\{\{[ \t\w"|$:=,.-]+}}|""|'')`), + }, + }, + { + // Avoid overreach between directives. + RegexTarget: "match", + Regexes: []*regexp.Regexp{ + regexp.MustCompile(`(kind:(?s:.)+\n---\n(?s:.)+\bdata:|data:(?s:.)+\n---\n(?s:.)+\bkind:)`), + }, + }, + }, + } + + // validate + tps := map[string]string{ + "base64-characters.yaml": ` +apiVersion: v1 +kind: Secret +data: + password: AAAAAAAAAAC7hjsA+H3owFygUv4w5B67lcSx14zff9FCPADiNbSwYWgE+O7Dhiy5tkRecs21ljjofvebe6xsYlA4cVmght0=`, + "comment.yaml": ` +apiVersion: v1 +kind: Secret +metadata: + name: heketi-secret + namespace: default +data: + # base64 encoded password. E.g.: echo -n "mypassword" | base64 + key: bXlwYXNzd29yZA==`, + // The "data"-key is before the identifier "kind: Secret" + "before-kubernetes.yaml": `apiVersion: v1 + data: + extra: YWRtaW46cGFzc3dvcmQ= + kind: secret + metadata: + name: secret-sa-sample + annotations: + kubernetes.io/service-account.name: 'sa-name'`, + "before-kubernetes.yml": `apiVersion: v1 +data: + password: UyFCXCpkJHpEc2I9 + username: YWRtaW4= +kind: Secret +metadata: + creationTimestamp: '2022-06-28T17:44:13Z' + name: db-user-pass + namespace: default +type: Opaque`, + "before-comment.yml": `apiVersion: v1 +data: + # the data is abbreviated in this example + password: UyFCXCpkJHpEc2I9 + username: YWRtaW4= +kind: Secret +metadata: + creationTimestamp: '2022-06-28T17:44:13Z' + name: db-user-pass + namespace: default +type: Opaque`, + "before-quoted-1.yaml": `apiVersion: 'v1' +data: + extra: 'YWRtaW46cGFzc3dvcmQ=' +kind: 'Secret' +metadata: + name: 'secret-sa-sample' + annotations: + kubernetes.io/service-account.name: 'sa-name'`, + "before-quoted-2.yaml": `apiVersion: "v1" +data: + extra: "YWRtaW46cGFzc3dvcmQ=" +kind: "secret" +metadata: + name: "secret-sa-sample" + annotations: + kubernetes.io/service-account.name: "sa-name"`, + "before-multiline-literal.yaml": `apiVersion: v1 +data: + .dockercfg: | + eyJhdXRocyI6eyJodHRwczovL2V4YW1wbGUvdjEvIjp7ImF1dGgiOiJvcGVuc2VzYW1lIn19fQo= +metadata: + name: secret-dockercfg +type: kubernetes.io/dockercfg +kind: Secret +`, + "before-multiline-folded.yaml": `apiVersion: v1 +data: + .dockercfg: > + eyJhdXRocyI6eyJodHRwczovL2V4YW1wbGUvdjEvIjp7ImF1dGgiOiJvcGVuc2VzYW1lIn19fQo= +metadata: + name: secret-dockercfg +type: kubernetes.io/dockercfg +kind: Secret`, + // Sample Kubernetes Secret from https://kubernetes.io/docs/concepts/configuration/secret/ + // The "data"-key is after the identifier "kind: Secret" + "after-kubernetes.yaml": `apiVersion: v1 +kind: secret +data: + extra: YWRtaW46cGFzc3dvcmQ= +metadata: + name: secret-sa-sample + annotations: + kubernetes.io/service-account.name: 'sa-name'`, + "after-kubernetes.yml": `apiVersion: v1 +kind: Secret +metadata: + name: ca-secret +type: Opaque +data: + ca.pem: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR4RENDQXF5Z0F3SUJBZ0lVV3pqUDl5RUk0eHlRSnBzVHVERU4yV2ROaUFzd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2FERUxNQWtHQTFVRUJoTUNWVk14RHpBTkJnTlZCQWdUQms5eVpXZHZiakVSTUE4R0ExVUVCeE1JVUc5eQpkR3hoYm1ReEV6QVJCZ05WQkFvVENrdDFZbVZ5Ym1WMFpYTXhDekFKQmdOVkJBc1RBa05CTVJNd0VRWURWUVFECkV3cExkV0psY201bGRHVnpNQjRYRFRFMk1EZ3hNVEUyTkRnd01Gb1hEVEl4TURneE1ERTJORGd3TUZvd2FERUwKTUFrR0ExVUVCaE1DVlZNeER6QU5CZ05WQkFnVEJrOXlaV2R2YmpFUk1BOEdBMVVFQnhNSVVHOXlkR3hoYm1ReApFekFSQmdOVkJBb1RDa3QxWW1WeWJtVjBaWE14Q3pBSkJnTlZCQXNUQWtOQk1STXdFUVlEVlFRREV3cExkV0psCmNtNWxkR1Z6TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF3QkhNOGN6anc0Q1cKK05wbklhV012RzZlcVhtelNZT20vbHdaNUhOMnVLck9xaTNHYUUyTjFKd2tzcGRmMXNOUGFZMHdPR2xkbURIZgoxSnlyTW8rUFdLVUVjWko1WGE4Vm02d2I0MlpjczN3MEp5dlEzWFJjaDQyMFJRWGRKayszcmMybWRvSVRkL0lmCnZjWms0N0RzQTMrQU5QSUlSTzdWRmZpS1JNRFpTUDR1OThnVjI2eW1zbjc0TzFVKzNVUHR1TEFTVTFLck9FTk4KR01FWG0ydTJpdmVvbTJrbjFlZTZuM1hCR1o2bU52cUNPdWUxRXdza0gvWkhoUVh1UDgyV1U5dVk0aGVORnoyQwpBNmR0Q0Q0c3Z6eHc3ZFQ2cVhsV0ZIWUYrc3VLVDhXNkczd3NkOWxzV0ZVY0ZWL0lwaTVobEVaTWprNFNoY3RqCjdpYnlrRURKM1FJREFRQUJvMll3WkRBT0JnTlZIUThCQWY4RUJBTUNBUVl3RWdZRFZSMFRBUUgvQkFnd0JnRUIKL3dJQkFqQWRCZ05WSFE0RUZnUVVOdnhRZ3o5ZTNXS2VscU1KTmZXNE1KUHYzc0V3SHdZRFZSMGpCQmd3Rm9BVQpOdnhRZ3o5ZTNXS2VscU1KTmZXNE1KUHYzc0V3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUp1TUhYUms1TEVyCmxET1p4Mm9aRUNQZ29reXMzSGJsM05oempXd2pncXdxNVN6a011V3QrUnVkdnRTK0FUQjFtTjRjYTN0eSt2bWcKT09heTkvaDZoditmSE5jZHpYdWR5dFZYZW1KN3F4ZFoxd25DUUcwdnpqOWRZY0xFSGpJWi94dU1jNlY3dnJ4YwpSc0preGp5aE01UXBmRHd0eVZKeGpkUmVBZ0huSyswTkNieHdtQ3cyRGIvOXpudm9LWGk4TEQwbkQzOFQxY3R3CmhmdGxwTmRoZXFNRlpEZXBuTUYwY2g2cHo5TFV5Mkh1cnhrV2dkWVNjY2VNU0hPTzBMcG4xeVVBMWczOTJhUjUKWk81Zm5KMW95Vm1LVWFCeDJCMndsSVlUSXlES1ZiMnY1UXNHbnYvRHVTMDZhcmVLTmsvTGpHRTRlMXlHOHJkcwpacnZHMzNvUmtEbz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= +`, + "after-comment.yml": `apiVersion: v1 +kind: Secret +metadata: + creationTimestamp: '2022-06-28T17:44:13Z' + name: db-user-pass + namespace: default +type: Opaque +data: + # the data is abbreviated in this example + password: UyFCXCpkJHpEc2I9 + username: YWRtaW4= +`, + "after-quoted-1.yaml": `apiVersion: 'v1' +kind: 'Secret' +data: + password: 'UyFCXCpkJHpEc2I9' + username: 'YWRtaW4=' +metadata: + name: 'db-user-pass' + namespace: 'default' +type: 'Opaque'`, + "after-quoted-2.yaml": `apiVersion: "v1" +kind: "Secret" +data: + password: "UyFCXCpkJHpEc2I9" + username: "YWRtaW4=" +metadata: + name: "db-user-pass" + namespace: "default" +type: "Opaque"`, + "after-multiline-literal.yaml": `apiVersion: v1 +kind: Secret +metadata: + name: secret-dockercfg +type: kubernetes.io/dockercfg +data: + .dockercfg: | + eyJhdXRocyI6eyJodHRwczovL2V4YW1wbGUvdjEvIjp7ImF1dGgiOiJvcGVuc2VzYW1lIn19fQo= +`, + "after-multiline-folded.yaml": `apiVersion: v1 +kind: Secret +metadata: + name: secret-dockercfg +type: kubernetes.io/dockercfg +data: + .dockercfg: > + eyJhdXRocyI6eyJodHRwczovL2V4YW1wbGUvdjEvIjp7ImF1dGgiOiJvcGVuc2VzYW1lIn19fQo=`, + } + fps := map[string]string{ + "empty-quotes1.yml": `apiVersion: v1 +kind: Secret +metadata: + name: registry-auth-data +type: Opaque +data: + htpasswd: '' +`, + "empty-quotes2.yml": `apiVersion: v1 +kind: Secret +metadata: + name: registry-auth-data +type: Opaque +data: + htpasswd: "" +`, + "overly-permissive1.yaml": `apiVersion: v1 +kind: Secret +metadata: + name: registry-auth-data +type: Opaque +data: + htpasswd: {{ htpasswd }} +--- +apiVersion: v1 + kind: ReplicationController`, + "overly-permissive2.yaml": `apiVersion: v1 +kind: Secret +metadata: + labels: + k8s-app: kubernetes-dashboard + addonmanager.kubernetes.io/mode: EnsureExists + name: kubernetes-dashboard-csrf + namespace: kubernetes-dashboard +type: Opaque +data: + csrf: "" + +--- + +apiVersion: v1 +kind: Secret +metadata: + labels: + k8s-app: kubernetes-dashboard + addonmanager.kubernetes.io/mode: EnsureExists + name: kubernetes-dashboard-key-holder + namespace: kubernetes-dashboard +type: Opaque +`, + "overly-permissive3.yaml": ` kind: Secret + target: + name: mysecret + creationPolicy: Owner + +--- + +kind: ConfigMap + data: + conversionStrategy: Default + decodingStrategy: None + key: secret/mysecret + property: foo + secretKey: foo`, + // https://github.com/gitleaks/gitleaks/issues/1644 + "wrong-kind.yaml": `apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: example + namespace: example-ns +spec: + refreshInterval: 15s + secretStoreRef: + name: example + kind: SecretStore + target: + name: mysecret + creationPolicy: Owner + data: + - remoteRef: + conversionStrategy: Default + decodingStrategy: None + key: secret/mysecret + property: foo + secretKey: foo +`, + "sopssecret.yaml": `apiVersion: isindir.github.com/v1alpha3 +kind: SopsSecret +metadata: + name: app1-sopssecret + namespace: test +spec: + suspend: false + secretTemplates: + - name: ENC[AES256_GCM,data:W3PiZ6lD6bpfAdI=,iv:2qF98ZkchgfWF4tZo8fok6zY0ZLNRV3wFpl8n2iyC7I=,tag:FzoL+CZHkLEqfWKniRApBA==,type:str] + labels: + app: ENC[AES256_GCM,data:t9ujIQ==,iv:slZBpmKF+DOg/wVBWmq5iTqkRBZUMao0a3MdoxzJs3s=,tag:xJyhdJ4rn/cB/4mxHzmGig==,type:str] + stringData: + db-password: ENC[AES256_GCM,data:O+5l4g==,iv:c/dS4BCBMbnKsXYuzBuCuVQt8RV9bOv5HgdpL+iwmns=,tag:KkQfh6OymvCt4uC13p318g==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + lastmodified: '2021-10-21T10:56:37Z' + mac: ENC[AES256_GCM,data:Tl1V1PuI5tZ0Hu3qxxzpDNeKQkuW0g/x/Mlp1yM6HaBqDr+r2FukLdYSYqjjJ3A8g+YkpvMib50M7j0V7zoX9sgCvMKEg86pRsWtThv8n/L+bsjClVTqnhJ9nfYlaPOMlvggbiMOE5hXPIuVz8WXYoVYJ2cVNCd/GfwOraUmj7I=,iv:n7HVI13okfbW3FS/ZsJ2GNmibudxc/TlkLa3umQQ+vc=,tag:R4ikep1wlxlDWlODJqFHHw==,type:str] + pgp: + - created_at: '2021-10-21T10:56:37Z' + enc: | + -----BEGIN PGP MESSAGE----- + + hQGMA3muqimBu2IIAQwAkhR19/6roLq06oaD12vqDMes3/8FweAHxa6TLKg+LRjp + 2/ntiRJHPBP9DYYFZbkTo8lAmIdVF7KfGIqWgPm5JiNhqfVRhyGPCRgBE7+I8qH6 + EML9Vo/76kJLHtIjs5rOg7OXgwwitaibs1q6uyVY8TuaGXYIOO1iwL9xVtbayIry + NMQd1tFcNb6Vb86Plqm+T1VnSOJMUvryxrLelx89UNM0ctepyVu6YY9jpBjV0QLJ + NqNkKAGIMv3RNa9bZHTwveo9T0oXtFnk5H33BxH0ky/DGpD+5Ch1YgbzbqVnr+Bm + RX0R/GRhS9IDInd+eiyVX6y3LR5di0fc8TuK43+96wTG+2+ck+lbMrkHYsL2UJNv + bAjlOWmIcL4UwGlEOj4EzwcEx+xP3dq57pJ+DasfNwVqps2Kk+ofodR7d6gx7ELH + UQmLypCtkRic9v8fVSA2vEL8hAlg9bT8tpHLhHMOwe228cL5dTzFD60RoP+ovRar + jIU59Pnu1bnM4pXWEVA20l4BzJ8Fd6gj3TfAg/7Mat+dnTaUwnPgRSybFn0ZZHMW + RJDBPkMGFfSGRDfLeD37d61mI31360/w/61LaVp1sdDYodBJCRZFA1YzbqZcxnDl + YRjRmpcVRnO+o72CnU/P + =V4l4 + -----END PGP MESSAGE----- + fp: 73019E949C1D3C3D1BE8B718C7CD51A565AB592C + encrypted_suffix: Templates + version: 3.6.1`, // https://github.com/luca-iachini/argocd-test/blob/af0c8eaba270bc918108c8bc3b909f26a4fe995d/kustomize/base/app1/secrets.enc.yaml#L4 + // The "data"-key is before the identifier "kind: Secret" + "before-min-length.yaml": `apiVersion: v1 +data: + extra: YmFyCg== +kind: secret +metadata: + name: secret-sa-sample + annotations: + kubernetes.io/service-account.name: 'sa-name'`, + "before-template.yaml": `apiVersion: v1 + data: + password: {{ .Values.Password }} + kind: secret + metadata: + name: secret-sa-sample + annotations: + kubernetes.io/service-account.name: 'sa-name'`, + "before-externalsecret1.yml": `apiVersion: 'kubernetes-client.io/v1' +metadata: + name: actions-exporter + namespace: github-actions-exporter +spec: + backendType: secretsManager + data: + - key: MySecretManagerKey + name: github_token + property: github_token +kind: ExternalSecret +`, + "before-externalsecret2.yml": `apiVersion: external-secrets.io/v1beta1 +spec: + secretStoreRef: + kind: ClusterSecretStore + name: aws-secretsmanager + refreshInterval: 1h + target: + creationPolicy: Owner + data: + - secretKey: api-key + remoteRef: + key: my-secrets-manager-secret +metadata: + name: api-key + namespace: my-namespace +kind: ExternalSecret +`, + "before-sopssecret.yml": `apiVersion: isindir.github.com/v1alpha3 +spec: + secretTemplates: + - name: my-secret-name-1 + labels: + label1: value1 + annotations: + key1: value1 + data: + data-name1: ZGF0YS12YWx1ZTE= + data-nameM: ZGF0YS12YWx1ZU0= +kind: SopsSecret +metadata: + name: sopssecret-sample +`, // https://github.com/isindir/sops-secrets-operator/blob/8aaf8bb368dc841a2d57f251bd839f08216a9328/config/samples/isindir_v1alpha3_sopssecret.yaml#L4 + // The "data"-key is after the identifier "kind: Secret" + "after-min-length.yaml": `apiVersion: v1 +kind: secret +data: + extra: YmFyCg== +metadata: + name: secret-sa-sample + annotations: + kubernetes.io/service-account.name: 'sa-name'`, + "after-externalsecret1.yml": `apiVersion: 'kubernetes-client.io/v1' +kind: ExternalSecret +metadata: + name: actions-exporter + namespace: github-actions-exporter +spec: + backendType: secretsManager + data: + - key: MySecretManagerKey + name: github_token + property: github_token + - key: MySecretManagerKey`, + "after-externalsecret2.yml": `apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: api-key + namespace: my-namespace +spec: + secretStoreRef: + kind: ClusterSecretStore + name: aws-secretsmanager + refreshInterval: 1h + target: + creationPolicy: Owner + data: + - secretKey: api-key + remoteRef: + key: my-secrets-manager-secret`, + "after-sopssecret.yml": `apiVersion: isindir.github.com/v1alpha3 +kind: SopsSecret +metadata: + name: sopssecret-sample +spec: + secretTemplates: + - name: my-secret-name-0 + labels: + label0: value0 + labelK: valueK + annotations: + key0: value0 + keyN: valueN + stringData: + data-name0: data-value0 + data-nameL: data-valueL + - name: my-secret-name-1 + labels: + label1: value1 + annotations: + key1: value1 + data: + data-name1: ZGF0YS12YWx1ZTE= + data-nameM: ZGF0YS12YWx1ZU0=`, + "after-empty-data.yaml": `apiVersion: v1 +kind: Secret +metadata: + labels: + k8s-app: kubernetes-dashboard + addonmanager.kubernetes.io/mode: EnsureExists + name: kubernetes-dashboard-csrf + namespace: kubernetes-dashboard +type: Opaque +data: + csrf: "" +`, + } + return utils.ValidateWithPaths(r, tps, fps) +} diff --git a/cmd/generate/config/rules/kucoin.go b/cmd/generate/config/rules/kucoin.go new file mode 100644 index 0000000..cdaf4d5 --- /dev/null +++ b/cmd/generate/config/rules/kucoin.go @@ -0,0 +1,41 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func KucoinAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "kucoin-access-token", + Description: "Found a Kucoin Access Token, risking unauthorized access to cryptocurrency exchange services and transactions.", + Regex: utils.GenerateSemiGenericRegex([]string{"kucoin"}, utils.Hex("24"), true), + + Keywords: []string{ + "kucoin", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("kucoin", secrets.NewSecret(utils.Hex("24"))) + return utils.Validate(r, tps, nil) +} + +func KucoinSecretKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "kucoin-secret-key", + Description: "Discovered a Kucoin Secret Key, which could lead to compromised cryptocurrency operations and financial data breaches.", + Regex: utils.GenerateSemiGenericRegex([]string{"kucoin"}, utils.Hex8_4_4_4_12(), true), + + Keywords: []string{ + "kucoin", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("kucoin", secrets.NewSecret(utils.Hex8_4_4_4_12())) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/launchdarkly.go b/cmd/generate/config/rules/launchdarkly.go new file mode 100644 index 0000000..47bfb2f --- /dev/null +++ b/cmd/generate/config/rules/launchdarkly.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func LaunchDarklyAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "launchdarkly-access-token", + Description: "Uncovered a Launchdarkly Access Token, potentially compromising feature flag management and application functionality.", + Regex: utils.GenerateSemiGenericRegex([]string{"launchdarkly"}, utils.AlphaNumericExtended("40"), true), + + Keywords: []string{ + "launchdarkly", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("launchdarkly", secrets.NewSecret(utils.AlphaNumericExtended("40"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/linear.go b/cmd/generate/config/rules/linear.go new file mode 100644 index 0000000..6d74fdf --- /dev/null +++ b/cmd/generate/config/rules/linear.go @@ -0,0 +1,38 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func LinearAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "linear-api-key", + Description: "Detected a Linear API Token, posing a risk to project management tools and sensitive task data.", + Regex: regexp.MustCompile(`lin_api_(?i)[a-z0-9]{40}`), + Entropy: 2, + Keywords: []string{"lin_api_"}, + } + + // validate + tps := utils.GenerateSampleSecrets("linear", "lin_api_"+secrets.NewSecret(utils.AlphaNumeric("40"))) + return utils.Validate(r, tps, nil) +} + +func LinearClientSecret() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "linear-client-secret", + Description: "Identified a Linear Client Secret, which may compromise secure integrations and sensitive project management data.", + Regex: utils.GenerateSemiGenericRegex([]string{"linear"}, utils.Hex("32"), true), + Entropy: 2, + Keywords: []string{"linear"}, + } + + // validate + tps := utils.GenerateSampleSecrets("linear", secrets.NewSecret(utils.Hex("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/linkedin.go b/cmd/generate/config/rules/linkedin.go new file mode 100644 index 0000000..1639646 --- /dev/null +++ b/cmd/generate/config/rules/linkedin.go @@ -0,0 +1,47 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func LinkedinClientID() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "linkedin-client-id", + Description: "Found a LinkedIn Client ID, risking unauthorized access to LinkedIn integrations and professional data exposure.", + Regex: utils.GenerateSemiGenericRegex([]string{"linked[_-]?in"}, utils.AlphaNumeric("14"), true), + Entropy: 2, + Keywords: []string{ + "linkedin", + "linked_in", + "linked-in", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("linkedin", secrets.NewSecret(utils.AlphaNumeric("14"))) + return utils.Validate(r, tps, nil) +} + +func LinkedinClientSecret() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "linkedin-client-secret", + Description: "Discovered a LinkedIn Client secret, potentially compromising LinkedIn application integrations and user data.", + Regex: utils.GenerateSemiGenericRegex([]string{ + "linked[_-]?in", + }, utils.AlphaNumeric("16"), true), + Entropy: 2, + Keywords: []string{ + "linkedin", + "linked_in", + "linked-in", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("linkedin", secrets.NewSecret(utils.AlphaNumeric("16"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/lob.go b/cmd/generate/config/rules/lob.go new file mode 100644 index 0000000..f47978e --- /dev/null +++ b/cmd/generate/config/rules/lob.go @@ -0,0 +1,43 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func LobPubAPIToken() *config.Rule { + // define rule + r := config.Rule{ + Description: "Detected a Lob Publishable API Key, posing a risk of exposing mail and print service integrations.", + RuleID: "lob-pub-api-key", + Regex: utils.GenerateSemiGenericRegex([]string{"lob"}, `(test|live)_pub_[a-f0-9]{31}`, true), + + Keywords: []string{ + "test_pub", + "live_pub", + "_pub", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("lob", "test_pub_"+secrets.NewSecret(utils.Hex("31"))) + return utils.Validate(r, tps, nil) +} + +func LobAPIToken() *config.Rule { + // define rule + r := config.Rule{ + Description: "Uncovered a Lob API Key, which could lead to unauthorized access to mailing and address verification services.", + RuleID: "lob-api-key", + Regex: utils.GenerateSemiGenericRegex([]string{"lob"}, `(live|test)_[a-f0-9]{35}`, true), + Keywords: []string{ + "test_", + "live_", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("lob", "test_"+secrets.NewSecret(utils.Hex("35"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/looker.go b/cmd/generate/config/rules/looker.go new file mode 100644 index 0000000..6adf4f7 --- /dev/null +++ b/cmd/generate/config/rules/looker.go @@ -0,0 +1,35 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func LookerClientID() *config.Rule { + // define rule + r := config.Rule{ + Description: "Found a Looker Client ID, risking unauthorized access to a Looker account and exposing sensitive data.", + RuleID: "looker-client-id", + Regex: utils.GenerateSemiGenericRegex([]string{"looker"}, utils.AlphaNumeric("20"), true), + Keywords: []string{"looker"}, + } + + // validate + tps := utils.GenerateSampleSecrets("looker", secrets.NewSecret(utils.AlphaNumeric("20"))) + return utils.Validate(r, tps, nil) +} + +func LookerClientSecret() *config.Rule { + // define rule + r := config.Rule{ + Description: "Found a Looker Client Secret, risking unauthorized access to a Looker account and exposing sensitive data.", + RuleID: "looker-client-secret", + Regex: utils.GenerateSemiGenericRegex([]string{"looker"}, utils.AlphaNumeric("24"), true), + Keywords: []string{"looker"}, + } + + // validate + tps := utils.GenerateSampleSecrets("looker", secrets.NewSecret(utils.AlphaNumeric("24"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/mailchimp.go b/cmd/generate/config/rules/mailchimp.go new file mode 100644 index 0000000..2e9278b --- /dev/null +++ b/cmd/generate/config/rules/mailchimp.go @@ -0,0 +1,32 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func MailChimp() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "mailchimp-api-key", + Description: "Identified a Mailchimp API key, potentially compromising email marketing campaigns and subscriber data.", + Regex: utils.GenerateSemiGenericRegex([]string{"MailchimpSDK.initialize", "mailchimp"}, utils.Hex("32")+`-us\d\d`, true), + + Keywords: []string{ + "mailchimp", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("mailchimp", secrets.NewSecret(utils.Hex("32"))+"-us20") + tps = append(tps, + `mailchimp_api_key: cefa780880ba5f5696192a34f6292c35-us18`, // gitleaks:allow + `MAILCHIMPE_KEY = "b5b9f8e50c640da28993e8b6a48e3e53-us18"`, // gitleaks:allow + ) + fps := []string{ + // False Negative + `MailchimpSDK.initialize(token: 3012a5754bbd716926f99c028f7ea428-us18)`, // gitleaks:allow + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/mailgun.go b/cmd/generate/config/rules/mailgun.go new file mode 100644 index 0000000..283535e --- /dev/null +++ b/cmd/generate/config/rules/mailgun.go @@ -0,0 +1,58 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func MailGunPrivateAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "mailgun-private-api-token", + Description: "Found a Mailgun private API token, risking unauthorized email service operations and data breaches.", + Regex: utils.GenerateSemiGenericRegex([]string{"mailgun"}, `key-[a-f0-9]{32}`, true), + + Keywords: []string{ + "mailgun", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("mailgun", "key-"+secrets.NewSecret(utils.Hex("32"))) + return utils.Validate(r, tps, nil) +} + +func MailGunPubAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "mailgun-pub-key", + Description: "Discovered a Mailgun public validation key, which could expose email verification processes and associated data.", + Regex: utils.GenerateSemiGenericRegex([]string{"mailgun"}, `pubkey-[a-f0-9]{32}`, true), + + Keywords: []string{ + "mailgun", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("mailgun", "pubkey-"+secrets.NewSecret(utils.Hex("32"))) + return utils.Validate(r, tps, nil) +} + +func MailGunSigningKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "mailgun-signing-key", + Description: "Uncovered a Mailgun webhook signing key, potentially compromising email automation and data integrity.", + Regex: utils.GenerateSemiGenericRegex([]string{"mailgun"}, `[a-h0-9]{32}-[a-h0-9]{8}-[a-h0-9]{8}`, true), + + Keywords: []string{ + "mailgun", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("mailgun", secrets.NewSecret(utils.Hex("32"))+"-00001111-22223333") + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/mapbox.go b/cmd/generate/config/rules/mapbox.go new file mode 100644 index 0000000..2c90d87 --- /dev/null +++ b/cmd/generate/config/rules/mapbox.go @@ -0,0 +1,22 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func MapBox() *config.Rule { + // define rule + r := config.Rule{ + Description: "Detected a MapBox API token, posing a risk to geospatial services and sensitive location data exposure.", + RuleID: "mapbox-api-token", + Regex: utils.GenerateSemiGenericRegex([]string{"mapbox"}, `pk\.[a-z0-9]{60}\.[a-z0-9]{22}`, true), + + Keywords: []string{"mapbox"}, + } + + // validate + tps := utils.GenerateSampleSecrets("mapbox", "pk."+secrets.NewSecret(utils.AlphaNumeric("60"))+"."+secrets.NewSecret(utils.AlphaNumeric("22"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/mattermost.go b/cmd/generate/config/rules/mattermost.go new file mode 100644 index 0000000..c61afec --- /dev/null +++ b/cmd/generate/config/rules/mattermost.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func MattermostAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "mattermost-access-token", + Description: "Identified a Mattermost Access Token, which may compromise team communication channels and data privacy.", + Regex: utils.GenerateSemiGenericRegex([]string{"mattermost"}, utils.AlphaNumeric("26"), true), + + Keywords: []string{ + "mattermost", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("mattermost", secrets.NewSecret(utils.AlphaNumeric("26"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/maxmind.go b/cmd/generate/config/rules/maxmind.go new file mode 100644 index 0000000..663a4b6 --- /dev/null +++ b/cmd/generate/config/rules/maxmind.go @@ -0,0 +1,21 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/config" +) + +func MaxMindLicenseKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "maxmind-license-key", + Description: "Discovered a potential MaxMind license key.", + Regex: utils.GenerateUniqueTokenRegex(`[A-Za-z0-9]{6}_[A-Za-z0-9]{29}_mmk`, false), + Entropy: 4, + Keywords: []string{"_mmk"}, + } + + // validate + tps := utils.GenerateSampleSecrets("maxmind", `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`) // gitleaks:allow + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/meraki.go b/cmd/generate/config/rules/meraki.go new file mode 100644 index 0000000..c4daf04 --- /dev/null +++ b/cmd/generate/config/rules/meraki.go @@ -0,0 +1,29 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func Meraki() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "cisco-meraki-api-key", + Description: "Cisco Meraki is a cloud-managed IT solution that provides networking, security, and device management through an easy-to-use interface.", + Regex: utils.GenerateSemiGenericRegex([]string{`(?-i:[Mm]eraki|MERAKI)`}, `[0-9a-f]{40}`, false), + Entropy: 3, + Keywords: []string{"meraki"}, + } + + // validate + tps := utils.GenerateSampleSecrets("meraki", secrets.NewSecret(utils.Hex("40"))) + fps := []string{ + `meraki: aaaaaaaaaa1111111111bbbbbbbbbb2222222222`, // low entropy + `meraki-api-key: acdeFf05b1a6d4c890237bf08c5e6e8d2b4d0f2e`, // invalid case + `meraki: abdefghjk0123456789mnopqrstuvwx12345678`, // invalid character + `meraki_token = 5cb4a5f04cd412fe946667b17f0129ba17aeb2e0c7b5b7264efcebf7d022bfe2R21`, // invalid length + `ReactNativeCameraKit: f15a5a04b0f6dc6073e6db0296e6ef2d8b8d2522`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/messagebird.go b/cmd/generate/config/rules/messagebird.go new file mode 100644 index 0000000..12f3819 --- /dev/null +++ b/cmd/generate/config/rules/messagebird.go @@ -0,0 +1,50 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func MessageBirdAPIToken() *config.Rule { + // define rule + r := config.Rule{ + Description: "Found a MessageBird API token, risking unauthorized access to communication platforms and message data.", + RuleID: "messagebird-api-token", + Regex: utils.GenerateSemiGenericRegex([]string{"message[_-]?bird"}, utils.AlphaNumeric("25"), true), + + Keywords: []string{ + "messagebird", + "message-bird", + "message_bird", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("messagebird", secrets.NewSecret(utils.AlphaNumeric("25"))) + tps = append(tps, utils.GenerateSampleSecrets("message-bird", secrets.NewSecret(utils.AlphaNumeric("25")))...) + tps = append(tps, utils.GenerateSampleSecrets("message_bird", secrets.NewSecret(utils.AlphaNumeric("25")))...) + return utils.Validate(r, tps, nil) +} + +func MessageBirdClientID() *config.Rule { + // define rule + r := config.Rule{ + Description: "Discovered a MessageBird client ID, potentially compromising API integrations and sensitive communication data.", + RuleID: "messagebird-client-id", + Regex: utils.GenerateSemiGenericRegex([]string{"message[_-]?bird"}, utils.Hex8_4_4_4_12(), true), + + Keywords: []string{ + "messagebird", + "message-bird", + "message_bird", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("MessageBird", "12345678-ABCD-ABCD-ABCD-1234567890AB") // gitleaks:allow + tps = append(tps, + `const MessageBirdClientID = "12345678-ABCD-ABCD-ABCD-1234567890AB"`, // gitleaks:allow + ) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/netlify.go b/cmd/generate/config/rules/netlify.go new file mode 100644 index 0000000..48ae631 --- /dev/null +++ b/cmd/generate/config/rules/netlify.go @@ -0,0 +1,25 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func NetlifyAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "netlify-access-token", + Description: "Detected a Netlify Access Token, potentially compromising web hosting services and site management.", + Regex: utils.GenerateSemiGenericRegex([]string{"netlify"}, + utils.AlphaNumericExtended("40,46"), true), + + Keywords: []string{ + "netlify", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("netlify", secrets.NewSecret(utils.AlphaNumericExtended("40,46"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/newrelic.go b/cmd/generate/config/rules/newrelic.go new file mode 100644 index 0000000..06ff3ed --- /dev/null +++ b/cmd/generate/config/rules/newrelic.go @@ -0,0 +1,93 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func NewRelicUserID() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "new-relic-user-api-key", + Description: "Discovered a New Relic user API Key, which could lead to compromised application insights and performance monitoring.", + Regex: utils.GenerateSemiGenericRegex([]string{ + "new-relic", + "newrelic", + "new_relic", + }, `NRAK-[a-z0-9]{27}`, true), + + Keywords: []string{ + "NRAK", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("new-relic", "NRAK-"+secrets.NewSecret(utils.AlphaNumeric("27"))) + return utils.Validate(r, tps, nil) +} + +func NewRelicUserKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "new-relic-user-api-id", + Description: "Found a New Relic user API ID, posing a risk to application monitoring services and data integrity.", + Regex: utils.GenerateSemiGenericRegex([]string{ + "new-relic", + "newrelic", + "new_relic", + }, utils.AlphaNumeric("64"), true), + + Keywords: []string{ + "new-relic", + "newrelic", + "new_relic", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("new-relic", secrets.NewSecret(utils.AlphaNumeric("64"))) + return utils.Validate(r, tps, nil) +} + +func NewRelicBrowserAPIKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "new-relic-browser-api-token", + Description: "Identified a New Relic ingest browser API token, risking unauthorized access to application performance data and analytics.", + Regex: utils.GenerateSemiGenericRegex([]string{ + "new-relic", + "newrelic", + "new_relic", + }, `NRJS-[a-f0-9]{19}`, true), + + Keywords: []string{ + "NRJS-", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("new-relic", "NRJS-"+secrets.NewSecret(utils.Hex("19"))) + return utils.Validate(r, tps, nil) +} + +func NewRelicInsertKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "new-relic-insert-key", + Description: "Discovered a New Relic insight insert key, compromising data injection into the platform.", + Regex: utils.GenerateSemiGenericRegex([]string{ + "new-relic", + "newrelic", + "new_relic", + }, `NRII-[a-z0-9-]{32}`, true), + + Keywords: []string{ + "NRII-", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("new-relic", "NRII-"+secrets.NewSecret(utils.Hex("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/notion.go b/cmd/generate/config/rules/notion.go new file mode 100644 index 0000000..88cb7d6 --- /dev/null +++ b/cmd/generate/config/rules/notion.go @@ -0,0 +1,39 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/config" +) + +func Notion() *config.Rule { + // Define the identifiers that match the Keywords + identifiers := []string{"ntn_"} + + // Define the regex pattern for Notion API token + secretRegex := `ntn_[0-9]{11}[A-Za-z0-9]{32}[A-Za-z0-9]{3}` + + regex := utils.GenerateUniqueTokenRegex(secretRegex, false) + + r := config.Rule{ + Description: "Notion API token", + RuleID: "notion-api-token", + Regex: regex, + Entropy: 4, + Keywords: identifiers, + } + + // validate + tps := []string{ + "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", + "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", + "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", + } + + fps := []string{ + "ntn_12345678901", + "ntn_123456789012345678901234567890123456789012345678901234567890", + "ntn_12345678901abc", + } + + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/npm.go b/cmd/generate/config/rules/npm.go new file mode 100644 index 0000000..f6ccc14 --- /dev/null +++ b/cmd/generate/config/rules/npm.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func NPM() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "npm-access-token", + Description: "Uncovered an npm access token, potentially compromising package management and code repository access.", + Regex: utils.GenerateUniqueTokenRegex(`npm_[a-z0-9]{36}`, true), + Entropy: 2, + Keywords: []string{ + "npm_", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("npmAccessToken", "npm_"+secrets.NewSecret(utils.AlphaNumeric("36"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/nuget.go b/cmd/generate/config/rules/nuget.go new file mode 100644 index 0000000..6c6ae08 --- /dev/null +++ b/cmd/generate/config/rules/nuget.go @@ -0,0 +1,48 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func NugetConfigPassword() *config.Rule { + r := config.Rule{ + Description: "Identified a password within a Nuget config file, potentially compromising package management access.", + RuleID: "nuget-config-password", + Regex: regexp.MustCompile(`(?i)`), + Path: regexp.MustCompile(`(?i)nuget\.config$`), + Keywords: []string{"`, + "Nuget.config": ``, + "Nuget.Config": ``, + "Nuget.COnfig": ``, + "Nuget.CONfig": ``, + "Nuget.CONFig": ``, + } + + fps := map[string]string{ + "some.xml": ``, // wrong filename + "nuget.config": ``, // low entropy + "Nuget.config": ``, // too short + "Nuget.Config": ``, // environment variable + "NUget.Config": ``, // known sample + "NUGet.Config": ``, // known sample + } + return utils.ValidateWithPaths(r, tps, fps) +} diff --git a/cmd/generate/config/rules/nytimes.go b/cmd/generate/config/rules/nytimes.go new file mode 100644 index 0000000..0c58097 --- /dev/null +++ b/cmd/generate/config/rules/nytimes.go @@ -0,0 +1,28 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func NytimesAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "nytimes-access-token", + Description: "Detected a Nytimes Access Token, risking unauthorized access to New York Times APIs and content services.", + Regex: utils.GenerateSemiGenericRegex([]string{ + "nytimes", "new-york-times,", "newyorktimes"}, + utils.AlphaNumericExtended("32"), true), + + Keywords: []string{ + "nytimes", + "new-york-times", + "newyorktimes", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("nytimes", secrets.NewSecret(utils.AlphaNumeric("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/octopusdeploy.go b/cmd/generate/config/rules/octopusdeploy.go new file mode 100644 index 0000000..d0ad6e6 --- /dev/null +++ b/cmd/generate/config/rules/octopusdeploy.go @@ -0,0 +1,32 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func OctopusDeployApiKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "octopus-deploy-api-key", + Description: "Discovered a potential Octopus Deploy API key, risking application deployments and operational security.", + Regex: utils.GenerateUniqueTokenRegex(`API-[A-Z0-9]{26}`, false), + Entropy: 3, + Keywords: []string{"api-"}, + } + + // validate + tps := []string{ + utils.GenerateSampleSecret("octopus", secrets.NewSecret(`API-[A-Z0-9]{26}`)), + `set apikey="API-ZNRMR7SL6L3ATMOIK7GKJDKLPY"`, // gitleaks:allow + } + fps := []string{ + // Invalid start + `msgstr "GSSAPI-VIRHEKAPSELOINTIMERKKIJONO."`, + `https://sonarcloud.io/api/project_badges/measure?project=Garden-Coin_API-CalculadoraDeInvestimentos&metric=alert_status`, + `https://fog-ringer-f42.notion.site/API-BD80F56CDC1441E6BF6011AB6D852875`, // Invalid end + ``, // Wrong case + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/okta.go b/cmd/generate/config/rules/okta.go new file mode 100644 index 0000000..47c6996 --- /dev/null +++ b/cmd/generate/config/rules/okta.go @@ -0,0 +1,34 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func OktaAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "okta-access-token", + Description: "Identified an Okta Access Token, which may compromise identity management services and user authentication data.", + Regex: utils.GenerateSemiGenericRegex([]string{`(?-i:[Oo]kta|OKTA)`}, `00[\w=\-]{40}`, false), + Entropy: 4, + Keywords: []string{ + "okta", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("okta", secrets.NewSecret(`00[\w=\-]{40}`)) + tps = append(tps, + `"oktaApiToken": "00ebObu4zSNkyc6dimLvUwq4KpTEop-PCEnnfSTpD3",`, // gitleaks:allow + ` var OktaApiToken = "00fWkOjwwL9xiFd-Vfgm_ePATIRxVj852Iblbb1DS_";`, // gitleaks:allow + ) + fps := []string{ + `oktaKey = 00000000000000000000000000000000000TUVWXYZ`, // low entropy + `rookTable = 0023452Lllk2KqjLBvaxANWEgTd7bqjsxjo8aZj0wd`, // wrong case + } + return utils.Validate(r, tps, fps) +} + +// TODO: Okta client secret? diff --git a/cmd/generate/config/rules/openai.go b/cmd/generate/config/rules/openai.go new file mode 100644 index 0000000..71d4a23 --- /dev/null +++ b/cmd/generate/config/rules/openai.go @@ -0,0 +1,37 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func OpenAI() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "openai-api-key", + Description: "Found an OpenAI API Key, posing a risk of unauthorized access to AI services and data manipulation.", + Regex: utils.GenerateUniqueTokenRegex(`sk-(?:proj|svcacct|admin)-(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})T3BlbkFJ(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})\b|sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20}`, false), + Entropy: 3, + Keywords: []string{ + "T3BlbkFJ", + }, + } + + // validate + tps := append(utils.GenerateSampleSecrets("openaiApiKey", "sk-"+secrets.NewSecret(utils.AlphaNumeric("20"))+"T3BlbkFJ"+secrets.NewSecret(utils.AlphaNumeric("20"))), + []string{ + "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", + "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", + "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", + + "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", + "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", + "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", + + "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", + "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", + "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", + }...) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/openshift.go b/cmd/generate/config/rules/openshift.go new file mode 100644 index 0000000..daa671c --- /dev/null +++ b/cmd/generate/config/rules/openshift.go @@ -0,0 +1,37 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +// OpenShift 4 user tokens are prefixed with `sha256~`. +// https://docs.redhat.com/en/documentation/openshift_container_platform/4.10/html-single/authentication_and_authorization/index#oauth-view-details-tokens_managing-oauth-access-tokens +func OpenshiftUserToken() *config.Rule { + r := config.Rule{ + RuleID: "openshift-user-token", + Description: "Found an OpenShift user token, potentially compromising an OpenShift/Kubernetes cluster.", + // TODO: Do tokens vary in length or are they always 43? + Regex: regexp.MustCompile(`\b(sha256~[\w-]{43})(?:[^\w-]|\z)`), + Entropy: 3.5, + Keywords: []string{ + "sha256~", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("oc", secrets.NewSecret("sha256~[\\w-]{43}")) + tps = append(tps, + `Authorization: Bearer sha256~kV46hPnEYhCWFnB85r5NrprAxggzgb6GOeLbgcKNsH0`, // https://github.com/openshift/console/blob/edae2305e01c2e0e8c33727af720ef960088eee3/dynamic-demo-plugin/README.md?plain=1#L114 + `oc login --token=sha256~ZBMKw9VAayhdnyANaHvjJeXDiGwA7Fsr5gtLKj3-eh- `, // https://github.com/IBM/tekton-tutorial-openshift/blob/2a97d22ba282accad50821bca069210ea89de706/docs/lab1/0_setup.md?plain=1#L85 + "sha256~"+secrets.NewSecret(`[\w-]{43}`), + ) + fps := []string{ + `--set kraken.kubeconfig.token.token="sha256~XXXXXXXXXX_PUT_YOUR_TOKEN_HERE_XXXXXXXXXXXX" \`, // https://github.com/krkn-chaos/krkn/blob/f3933f0e6239824eb9b5c46ff0e5d503b8465d6a/docs/index.md?plain=1#L307 + `oc login --token=sha256~_xxxxxx_xxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxx-X \ + --server=https://api.${zone}.appuio.cloud:6443`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/perplexity.go b/cmd/generate/config/rules/perplexity.go new file mode 100644 index 0000000..c35ac5c --- /dev/null +++ b/cmd/generate/config/rules/perplexity.go @@ -0,0 +1,27 @@ +package rules + +import ( + "regexp" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + + "github.com/zricethezav/gitleaks/v8/config" +) + +func PerplexityAPIKey() *config.Rule { + // Define Rule + r := config.Rule{ + RuleID: "perplexity-api-key", + Description: "Detected a Perplexity API key, which could lead to unauthorized access to Perplexity AI services and data exposure.", + Regex: regexp.MustCompile(`\b(pplx-[a-zA-Z0-9]{48})(?:[\x60'"\s;]|\\[nr]|$|\b)`), + Keywords: []string{"pplx-"}, + Entropy: 4.0, + } + + // validate + tps := utils.GenerateSampleSecrets("perplexity", "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'") + fps := []string{ + "PERPLEXITY_API_KEY=pplx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/plaid.go b/cmd/generate/config/rules/plaid.go new file mode 100644 index 0000000..f79a1de --- /dev/null +++ b/cmd/generate/config/rules/plaid.go @@ -0,0 +1,61 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func PlaidAccessID() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "plaid-client-id", + Description: "Uncovered a Plaid Client ID, which could lead to unauthorized financial service integrations and data breaches.", + Regex: utils.GenerateSemiGenericRegex([]string{"plaid"}, utils.AlphaNumeric("24"), true), + + Entropy: 3.5, + Keywords: []string{ + "plaid", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("plaid", secrets.NewSecret(`[a-zA-Z0-9]{24}`)) + return utils.Validate(r, tps, nil) +} + +func PlaidSecretKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "plaid-secret-key", + Description: "Detected a Plaid Secret key, risking unauthorized access to financial accounts and sensitive transaction data.", + Regex: utils.GenerateSemiGenericRegex([]string{"plaid"}, utils.AlphaNumeric("30"), true), + + Entropy: 3.5, + Keywords: []string{ + "plaid", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("plaid", secrets.NewSecret(utils.AlphaNumeric("30"))) + return utils.Validate(r, tps, nil) +} + +func PlaidAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "plaid-api-token", + Description: "Discovered a Plaid API Token, potentially compromising financial data aggregation and banking services.", + Regex: utils.GenerateSemiGenericRegex([]string{"plaid"}, + "access-(?:sandbox|development|production)-"+utils.Hex8_4_4_4_12(), true), + + Keywords: []string{ + "plaid", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("plaid", secrets.NewSecret("access-(?:sandbox|development|production)-"+utils.Hex8_4_4_4_12())) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/planetscale.go b/cmd/generate/config/rules/planetscale.go new file mode 100644 index 0000000..d0cfb0b --- /dev/null +++ b/cmd/generate/config/rules/planetscale.go @@ -0,0 +1,64 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func PlanetScalePassword() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "planetscale-password", + Description: "Discovered a PlanetScale password, which could lead to unauthorized database operations and data breaches.", + Regex: utils.GenerateUniqueTokenRegex(`pscale_pw_(?i)[\w=\.-]{32,64}`, true), + Entropy: 3, + Keywords: []string{ + "pscale_pw_", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("planetScale", "pscale_pw_"+secrets.NewSecret(utils.AlphaNumericExtended("32"))) + tps = append(tps, utils.GenerateSampleSecrets("planetScale", "pscale_pw_"+secrets.NewSecret(utils.AlphaNumericExtended("43")))...) + tps = append(tps, utils.GenerateSampleSecrets("planetScale", "pscale_pw_"+secrets.NewSecret(utils.AlphaNumericExtended("64")))...) + return utils.Validate(r, tps, nil) +} + +func PlanetScaleAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "planetscale-api-token", + Description: "Identified a PlanetScale API token, potentially compromising database management and operations.", + Regex: utils.GenerateUniqueTokenRegex(`pscale_tkn_(?i)[\w=\.-]{32,64}`, false), + Entropy: 3, + Keywords: []string{ + "pscale_tkn_", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("planetScale", "pscale_tkn_"+secrets.NewSecret(utils.AlphaNumericExtended("32"))) + tps = append(tps, utils.GenerateSampleSecrets("planetScale", "pscale_tkn_"+secrets.NewSecret(utils.AlphaNumericExtended("43")))...) + tps = append(tps, utils.GenerateSampleSecrets("planetScale", "pscale_tkn_"+secrets.NewSecret(utils.AlphaNumericExtended("64")))...) + return utils.Validate(r, tps, nil) +} + +func PlanetScaleOAuthToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "planetscale-oauth-token", + Description: "Found a PlanetScale OAuth token, posing a risk to database access control and sensitive data integrity.", + Regex: utils.GenerateUniqueTokenRegex(`pscale_oauth_[\w=\.-]{32,64}`, false), + Entropy: 3, + Keywords: []string{ + "pscale_oauth_", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("planetScale", "pscale_oauth_"+secrets.NewSecret(utils.AlphaNumericExtended("32"))) + tps = append(tps, utils.GenerateSampleSecrets("planetScale", "pscale_oauth_"+secrets.NewSecret(utils.AlphaNumericExtended("43")))...) + tps = append(tps, utils.GenerateSampleSecrets("planetScale", "pscale_oauth_"+secrets.NewSecret(utils.AlphaNumericExtended("64")))...) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/postman.go b/cmd/generate/config/rules/postman.go new file mode 100644 index 0000000..59b096c --- /dev/null +++ b/cmd/generate/config/rules/postman.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func PostManAPI() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "postman-api-token", + Description: "Uncovered a Postman API token, potentially compromising API testing and development workflows.", + Regex: utils.GenerateUniqueTokenRegex(`PMAK-(?i)[a-f0-9]{24}\-[a-f0-9]{34}`, false), + Entropy: 3, + Keywords: []string{ + "PMAK-", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("postmanAPItoken", "PMAK-"+secrets.NewSecret(utils.Hex("24"))+"-"+secrets.NewSecret(utils.Hex("34"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/prefect.go b/cmd/generate/config/rules/prefect.go new file mode 100644 index 0000000..b29a1c4 --- /dev/null +++ b/cmd/generate/config/rules/prefect.go @@ -0,0 +1,27 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func Prefect() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "prefect-api-token", + Description: "Detected a Prefect API token, risking unauthorized access to workflow management and automation services.", + Regex: utils.GenerateUniqueTokenRegex(`pnu_[a-zA-Z0-9]{36}`, false), + Entropy: 2, + Keywords: []string{ + "pnu_", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("api-token", "pnu_"+secrets.NewSecret(utils.AlphaNumeric("36"))) + fps := []string{ + `PREFECT_API_KEY = "pnu_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/privateai.go b/cmd/generate/config/rules/privateai.go new file mode 100644 index 0000000..6c5fea6 --- /dev/null +++ b/cmd/generate/config/rules/privateai.go @@ -0,0 +1,31 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func PrivateAIToken() *config.Rule { + // https://docs.private-ai.com/reference/latest/operation/metrics_metrics_get/ + r := config.Rule{ + RuleID: "privateai-api-token", + Description: "Identified a PrivateAI Token, posing a risk of unauthorized access to AI services and data manipulation.", + Regex: utils.GenerateSemiGenericRegex([]string{"private[_-]?ai"}, `[a-z0-9]{32}`, false), + Entropy: 3, + Keywords: []string{ + "privateai", + "private_ai", + "private-ai", + }, + } + + // validate + tps := []string{ + utils.GenerateSampleSecret("privateai", secrets.NewSecret(utils.AlphaNumeric("32"))), + } + fps := []string{ + `const privateaiToken = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/privatekey.go b/cmd/generate/config/rules/privatekey.go new file mode 100644 index 0000000..36ff0cc --- /dev/null +++ b/cmd/generate/config/rules/privatekey.go @@ -0,0 +1,61 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func PrivateKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "private-key", + Description: "Identified a Private Key, which may compromise cryptographic security and sensitive data encryption.", + Regex: regexp.MustCompile(`(?i)-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----[\s\S-]{64,}?KEY(?: BLOCK)?-----`), + Keywords: []string{"-----BEGIN"}, + } + + // validate + tps := []string{`-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDAC4AWkdwKYSd8 +Ks14IReLcYgADhoXk56ZzXI= +-----END PRIVATE KEY-----`, + `-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAn6/O8li+SX4m98LLYt/PKSzEmQ++ZBD7Loh9P13f4yQ92EF3 +yxR5MsXFu9PRsrYQA7/4UTPHiC4y2sAVCBg4C2yyBpUEtMQjyCESi6Y= +-----END RSA PRIVATE KEY----- +`, + `-----BEGIN PGP PRIVATE KEY BLOCK----- +lQWGBGSVV4YBDAClvRnxezIRy2Yv7SFlzC0iFiRF/O/jePSw+XYhvcrTaqSYTGic +=8xQN +-----END PGP PRIVATE KEY BLOCK-----`, + } // gitleaks:allow + fps := []string{ + `-----BEGIN PRIVATE KEY----- +anything +-----END PRIVATE KEY-----`, + `-----BEGIN OPENSSH PRIVATE KEY----------END OPENSSH PRIVATE KEY-----`, + } + return utils.Validate(r, tps, fps) +} + +func PrivateKeyPKCS12File() *config.Rule { + // https://en.wikipedia.org/wiki/PKCS_12 + r := config.Rule{ + RuleID: "pkcs12-file", + Description: "Found a PKCS #12 file, which commonly contain bundled private keys.", + Path: regexp.MustCompile(`(?i)(?:^|\/)[^\/]+\.p(?:12|fx)$`), + } + + // validate + tps := map[string]string{ + "security/es_certificates/opensearch/es_kibana_client.p12": "", + "cagw_key.P12": "", + "ToDo/ToDo.UWP/ToDo.UWP_TemporaryKey.pfx": "", + } + fps := map[string]string{ + "doc/typenum/type.P126.html": "", + "scripts/keeneland/syntest.p1200.sh": "", + } + return utils.ValidateWithPaths(r, tps, fps) +} diff --git a/cmd/generate/config/rules/pulumi.go b/cmd/generate/config/rules/pulumi.go new file mode 100644 index 0000000..0c74800 --- /dev/null +++ b/cmd/generate/config/rules/pulumi.go @@ -0,0 +1,27 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func PulumiAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "pulumi-api-token", + Description: "Found a Pulumi API token, posing a risk to infrastructure as code services and cloud resource management.", + Regex: utils.GenerateUniqueTokenRegex(`pul-[a-f0-9]{40}`, false), + Entropy: 2, + Keywords: []string{ + "pul-", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("pulumi-api-token", "pul-"+secrets.NewSecret(utils.Hex("40"))) + fps := []string{ + ` `, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/pypi.go b/cmd/generate/config/rules/pypi.go new file mode 100644 index 0000000..0643899 --- /dev/null +++ b/cmd/generate/config/rules/pypi.go @@ -0,0 +1,25 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func PyPiUploadToken() *config.Rule { + // define rule + r := config.Rule{ + Description: "Discovered a PyPI upload token, potentially compromising Python package distribution and repository integrity.", + RuleID: "pypi-upload-token", + Regex: regexp.MustCompile(`pypi-AgEIcHlwaS5vcmc[\w-]{50,1000}`), + Entropy: 3, + Keywords: []string{ + "pypi-AgEIcHlwaS5vcmc", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("pypi", "pypi-AgEIcHlwaS5vcmc"+secrets.NewSecret(utils.Hex("32"))+secrets.NewSecret(utils.Hex("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/rapidapi.go b/cmd/generate/config/rules/rapidapi.go new file mode 100644 index 0000000..ee0febf --- /dev/null +++ b/cmd/generate/config/rules/rapidapi.go @@ -0,0 +1,25 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func RapidAPIAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "rapidapi-access-token", + Description: "Uncovered a RapidAPI Access Token, which could lead to unauthorized access to various APIs and data services.", + Regex: utils.GenerateSemiGenericRegex([]string{"rapidapi"}, + utils.AlphaNumericExtendedShort("50"), true), + + Keywords: []string{ + "rapidapi", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("rapidapi", secrets.NewSecret(utils.AlphaNumericExtendedShort("50"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/readme.go b/cmd/generate/config/rules/readme.go new file mode 100644 index 0000000..ddfba19 --- /dev/null +++ b/cmd/generate/config/rules/readme.go @@ -0,0 +1,28 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func ReadMe() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "readme-api-token", + Description: "Detected a Readme API token, risking unauthorized documentation management and content exposure.", + Regex: utils.GenerateUniqueTokenRegex(`rdme_[a-z0-9]{70}`, false), + Entropy: 2, + Keywords: []string{ + "rdme_", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("api-token", "rdme_"+secrets.NewSecret(utils.AlphaNumeric("70"))) + + fps := []string{ + `const API_KEY = 'rdme_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/rubygems.go b/cmd/generate/config/rules/rubygems.go new file mode 100644 index 0000000..d29f0b1 --- /dev/null +++ b/cmd/generate/config/rules/rubygems.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func RubyGemsAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "rubygems-api-token", + Description: "Identified a Rubygem API token, potentially compromising Ruby library distribution and package management.", + Regex: utils.GenerateUniqueTokenRegex(`rubygems_[a-f0-9]{48}`, false), + Entropy: 2, + Keywords: []string{ + "rubygems_", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("rubygemsAPIToken", "rubygems_"+secrets.NewSecret(utils.Hex("48"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/scalingo.go b/cmd/generate/config/rules/scalingo.go new file mode 100644 index 0000000..f96d87e --- /dev/null +++ b/cmd/generate/config/rules/scalingo.go @@ -0,0 +1,25 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func ScalingoAPIToken() *config.Rule { + // define rule + r := config.Rule{ + Description: "Found a Scalingo API token, posing a risk to cloud platform services and application deployment security.", + RuleID: "scalingo-api-token", + Regex: utils.GenerateUniqueTokenRegex(`tk-us-[\w-]{48}`, false), + Entropy: 2, + Keywords: []string{"tk-us-"}, + } + + // validate + tps := utils.GenerateSampleSecrets("scalingo", "tk-us-"+secrets.NewSecret(utils.AlphaNumericExtendedShort("48"))) + tps = append(tps, + `scalingo_api_token = "tk-us-loys7ib9yrxcys_ta2sq85mjar6lgcsspkd9x61s7h5epf_-"`, // gitleaks:allow + ) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/sendbird.go b/cmd/generate/config/rules/sendbird.go new file mode 100644 index 0000000..6cecb42 --- /dev/null +++ b/cmd/generate/config/rules/sendbird.go @@ -0,0 +1,41 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func SendbirdAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "sendbird-access-token", + Description: "Uncovered a Sendbird Access Token, potentially risking unauthorized access to communication services and user data.", + Regex: utils.GenerateSemiGenericRegex([]string{"sendbird"}, utils.Hex("40"), true), + + Keywords: []string{ + "sendbird", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("sendbird", secrets.NewSecret(utils.Hex("40"))) + return utils.Validate(r, tps, nil) +} + +func SendbirdAccessID() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "sendbird-access-id", + Description: "Discovered a Sendbird Access ID, which could compromise chat and messaging platform integrations.", + Regex: utils.GenerateSemiGenericRegex([]string{"sendbird"}, utils.Hex8_4_4_4_12(), true), + + Keywords: []string{ + "sendbird", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("sendbird", secrets.NewSecret(utils.Hex8_4_4_4_12())) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/sendgrid.go b/cmd/generate/config/rules/sendgrid.go new file mode 100644 index 0000000..a0e588e --- /dev/null +++ b/cmd/generate/config/rules/sendgrid.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func SendGridAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "sendgrid-api-token", + Description: "Detected a SendGrid API token, posing a risk of unauthorized email service operations and data exposure.", + Regex: utils.GenerateUniqueTokenRegex(`SG\.(?i)[a-z0-9=_\-\.]{66}`, false), + Entropy: 2, + Keywords: []string{ + "SG.", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("sengridAPIToken", "SG."+secrets.NewSecret(utils.AlphaNumericExtended("66"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/sendinblue.go b/cmd/generate/config/rules/sendinblue.go new file mode 100644 index 0000000..17cb9c9 --- /dev/null +++ b/cmd/generate/config/rules/sendinblue.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func SendInBlueAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "sendinblue-api-token", + Description: "Identified a Sendinblue API token, which may compromise email marketing services and subscriber data privacy.", + Regex: utils.GenerateUniqueTokenRegex(`xkeysib-[a-f0-9]{64}\-(?i)[a-z0-9]{16}`, false), + Entropy: 2, + Keywords: []string{ + "xkeysib-", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("sendinblue", "xkeysib-"+secrets.NewSecret(utils.Hex("64"))+"-"+secrets.NewSecret(utils.AlphaNumeric("16"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/sentry.go b/cmd/generate/config/rules/sentry.go new file mode 100644 index 0000000..a78617e --- /dev/null +++ b/cmd/generate/config/rules/sentry.go @@ -0,0 +1,98 @@ +package rules + +import ( + "encoding/base64" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func SentryAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "sentry-access-token", + Description: "Found a Sentry.io Access Token (old format), risking unauthorized access to error tracking services and sensitive application data.", + Regex: utils.GenerateSemiGenericRegex([]string{"sentry"}, utils.Hex("64"), true), + Entropy: 3, + Keywords: []string{ + "sentry", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("sentry", secrets.NewSecret(utils.Hex("64"))) + return utils.Validate(r, tps, nil) +} + +func SentryOrgToken() *config.Rule { + + // format: sntrys_[base64_json]_[base64_secret] + // the json contains the following fields : {"iat": ,"url": ,"region_url": ,"org": } + // Specification: https://github.com/getsentry/rfcs/blob/main/text/0091-ci-upload-tokens.md + // Some test cases from official parser: + // https://github.com/getsentry/sentry-cli/blob/693d62167041846e2da823b7f3b0f21b673b5b1f/src/utils/auth_token/test.rs + // To detect the token, this rule checks for the following base64-encoded json fragments : + // eyJpYXQiO = `{"iat":`, + // LCJyZWdpb25fdXJs = `,"region_url` + // InJlZ2lvbl91cmwi = `"region_url"` + // cmVnaW9uX3VybCI6 = `region_url":` + + // define rule + r := config.Rule{ + RuleID: "sentry-org-token", + Description: "Found a Sentry.io Organization Token, risking unauthorized access to error tracking services and sensitive application data.", + Regex: regexp.MustCompile(`\bsntrys_eyJpYXQiO[a-zA-Z0-9+/]{10,200}(?:LCJyZWdpb25fdXJs|InJlZ2lvbl91cmwi|cmVnaW9uX3VybCI6)[a-zA-Z0-9+/]{10,200}={0,2}_[a-zA-Z0-9+/]{43}(?:[^a-zA-Z0-9+/]|\z)`), + Entropy: 4.5, + Keywords: []string{"sntrys_eyJpYXQiO"}, + } + + // validate + tps := utils.GenerateSampleSecrets("sentry", + `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`, // gitleaks:allow + ) + tps = append(tps, utils.GenerateSampleSecrets("sentry", + `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`, // gitleaks:allow + )...) + tps = append(tps, + ` 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`, // gitleaks:allow + ) + + encodedJson := base64.StdEncoding.EncodeToString([]byte(secrets.NewSecret( + `\{"iat":[\d\.]{3,6},"url":"https://\w{10,20}","region_url":"https://\w{20,30}","org":"\w{5,10}"\}`))) + generatedToken := `sntrys_` + encodedJson + `_` + secrets.NewSecret(`[a-zA-Z0-9+/]{43}`) + tps = append(tps, + generatedToken, + ""+generatedToken+"", + "https://example.com?token="+generatedToken+"&other=stuff", + ) + + fps := []string{ + secrets.NewSecret(`sntrys_[a-zA-Z0-9]{90}_[a-zA-Z0-9]{43}`), // does not contain encoded json + `sntrys_` + encodedJson + `_` + secrets.NewSecret(`[a-zA-Z0-9]{42}`), // too short + `sntrys_` + encodedJson + `_` + secrets.NewSecret(`[a-zA-Z0-9]{44}`), // too long + } + + return utils.Validate(r, tps, fps) +} + +func SentryUserToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "sentry-user-token", + Description: "Found a Sentry.io User Token, risking unauthorized access to error tracking services and sensitive application data.", + Regex: utils.GenerateUniqueTokenRegex(`sntryu_[a-f0-9]{64}`, false), + Entropy: 3.5, + Keywords: []string{"sntryu_"}, + } + + // validate + tps := utils.GenerateSampleSecrets("sentry", secrets.NewSecret(`sntryu_[a-f0-9]{64}`)) + fps := []string{ + secrets.NewSecret(`sntryu_[a-f0-9]{63}`), // too short + secrets.NewSecret(`sntryu_[a-f0-9]{65}`), // too long + secrets.NewSecret(`sntryu_[a]{64}`), // low entropy + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/settlemint.go b/cmd/generate/config/rules/settlemint.go new file mode 100644 index 0000000..3f4bd5f --- /dev/null +++ b/cmd/generate/config/rules/settlemint.go @@ -0,0 +1,70 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func SettlemintPersonalAccessToken() *config.Rule { + // define rule + r := config.Rule{ + Description: "Found a Settlemint Personal Access Token.", + RuleID: "settlemint-personal-access-token", + Regex: utils.GenerateUniqueTokenRegex(`sm_pat_[a-zA-Z0-9]{16}`, false), + Entropy: 3, + Keywords: []string{ + "sm_pat", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("settlemintToken", "sm_pat_"+secrets.NewSecret(utils.AlphaNumeric("16"))) + fps := []string{ + "nonMatchingToken := \"" + secrets.NewSecret(utils.AlphaNumeric("16")) + "\"", + "nonMatchingToken := \"sm_pat_" + secrets.NewSecret(utils.AlphaNumeric("10")) + "\"", + } + return utils.Validate(r, tps, fps) +} + +func SettlemintApplicationAccessToken() *config.Rule { + // define rule + r := config.Rule{ + Description: "Found a Settlemint Application Access Token.", + RuleID: "settlemint-application-access-token", + Regex: utils.GenerateUniqueTokenRegex(`sm_aat_[a-zA-Z0-9]{16}`, false), + Entropy: 3, + Keywords: []string{ + "sm_aat", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("settlemintToken", "sm_aat_"+secrets.NewSecret(utils.AlphaNumeric("16"))) + fps := []string{ + "nonMatchingToken := \"" + secrets.NewSecret(utils.AlphaNumeric("16")) + "\"", + "nonMatchingToken := \"sm_aat_" + secrets.NewSecret(utils.AlphaNumeric("10")) + "\"", + } + return utils.Validate(r, tps, fps) +} + +func SettlemintServiceAccessToken() *config.Rule { + // define rule + r := config.Rule{ + Description: "Found a Settlemint Service Access Token.", + RuleID: "settlemint-service-access-token", + Regex: utils.GenerateUniqueTokenRegex(`sm_sat_[a-zA-Z0-9]{16}`, false), + Entropy: 3, + Keywords: []string{ + "sm_sat", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("settlemintToken", "sm_sat_"+secrets.NewSecret(utils.AlphaNumeric("16"))) + fps := []string{ + "nonMatchingToken := \"" + secrets.NewSecret(utils.AlphaNumeric("16")) + "\"", + "nonMatchingToken := \"sm_sat_" + secrets.NewSecret(utils.AlphaNumeric("10")) + "\"", + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/shippo.go b/cmd/generate/config/rules/shippo.go new file mode 100644 index 0000000..d5cbc6f --- /dev/null +++ b/cmd/generate/config/rules/shippo.go @@ -0,0 +1,25 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func ShippoAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "shippo-api-token", + Description: "Discovered a Shippo API token, potentially compromising shipping services and customer order data.", + Regex: utils.GenerateUniqueTokenRegex(`shippo_(?:live|test)_[a-fA-F0-9]{40}`, false), + Entropy: 2, + Keywords: []string{ + "shippo_", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("shippo", "shippo_live_"+secrets.NewSecret(utils.Hex("40"))) + tps = append(tps, utils.GenerateSampleSecrets("shippo", "shippo_test_"+secrets.NewSecret(utils.Hex("40")))...) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/shopify.go b/cmd/generate/config/rules/shopify.go new file mode 100644 index 0000000..f3e6927 --- /dev/null +++ b/cmd/generate/config/rules/shopify.go @@ -0,0 +1,68 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func ShopifySharedSecret() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "shopify-shared-secret", + Description: "Found a Shopify shared secret, posing a risk to application authentication and e-commerce platform security.", + Regex: regexp.MustCompile(`shpss_[a-fA-F0-9]{32}`), + Entropy: 2, + Keywords: []string{"shpss_"}, + } + + // validate + tps := utils.GenerateSampleSecrets("shopify", "shpss_"+secrets.NewSecret(utils.Hex("32"))) + return utils.Validate(r, tps, nil) +} + +func ShopifyAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "shopify-access-token", + Description: "Uncovered a Shopify access token, which could lead to unauthorized e-commerce platform access and data breaches.", + Regex: regexp.MustCompile(`shpat_[a-fA-F0-9]{32}`), + Entropy: 2, + Keywords: []string{"shpat_"}, + } + + // validate + tps := utils.GenerateSampleSecrets("shopify", "shpat_"+secrets.NewSecret(utils.Hex("32"))) + return utils.Validate(r, tps, nil) +} + +func ShopifyCustomAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "shopify-custom-access-token", + Description: "Detected a Shopify custom access token, potentially compromising custom app integrations and e-commerce data security.", + Regex: regexp.MustCompile(`shpca_[a-fA-F0-9]{32}`), + Entropy: 2, + Keywords: []string{"shpca_"}, + } + + // validate + tps := utils.GenerateSampleSecrets("shopify", "shpca_"+secrets.NewSecret(utils.Hex("32"))) + return utils.Validate(r, tps, nil) +} + +func ShopifyPrivateAppAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "shopify-private-app-access-token", + Description: "Identified a Shopify private app access token, risking unauthorized access to private app data and store operations.", + Regex: regexp.MustCompile(`shppa_[a-fA-F0-9]{32}`), + Entropy: 2, + Keywords: []string{"shppa_"}, + } + + // validate + tps := utils.GenerateSampleSecrets("shopify", "shppa_"+secrets.NewSecret(utils.Hex("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/sidekiq.go b/cmd/generate/config/rules/sidekiq.go new file mode 100644 index 0000000..eaccbe1 --- /dev/null +++ b/cmd/generate/config/rules/sidekiq.go @@ -0,0 +1,62 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func SidekiqSecret() *config.Rule { + // define rule + r := config.Rule{ + Description: "Discovered a Sidekiq Secret, which could lead to compromised background job processing and application data breaches.", + RuleID: "sidekiq-secret", + + Regex: utils.GenerateSemiGenericRegex([]string{"BUNDLE_ENTERPRISE__CONTRIBSYS__COM", "BUNDLE_GEMS__CONTRIBSYS__COM"}, + `[a-f0-9]{8}:[a-f0-9]{8}`, true), + Keywords: []string{"BUNDLE_ENTERPRISE__CONTRIBSYS__COM", "BUNDLE_GEMS__CONTRIBSYS__COM"}, + } + + // validate + tps := utils.GenerateSampleSecrets("BUNDLE_ENTERPRISE__CONTRIBSYS__COM", secrets.NewSecret("[a-f0-9]{8}:[a-f0-9]{8}")) + tps = append(tps, utils.GenerateSampleSecrets("BUNDLE_GEMS__CONTRIBSYS__COM", secrets.NewSecret("[a-f0-9]{8}:[a-f0-9]{8}"))...) + tps = append(tps, + "BUNDLE_ENTERPRISE__CONTRIBSYS__COM: cafebabe:deadbeef", + "export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef", + "export BUNDLE_ENTERPRISE__CONTRIBSYS__COM = cafebabe:deadbeef", + "BUNDLE_GEMS__CONTRIBSYS__COM: \"cafebabe:deadbeef\"", + "export BUNDLE_GEMS__CONTRIBSYS__COM=\"cafebabe:deadbeef\"", + "export BUNDLE_GEMS__CONTRIBSYS__COM = \"cafebabe:deadbeef\"", + "export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;", + "export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef && echo 'hello world'", + ) + return utils.Validate(r, tps, nil) +} + +func SidekiqSensitiveUrl() *config.Rule { + // define rule + r := config.Rule{ + Description: "Uncovered a Sidekiq Sensitive URL, potentially exposing internal job queues and sensitive operation details.", + RuleID: "sidekiq-sensitive-url", + Regex: regexp.MustCompile(`(?i)\bhttps?://([a-f0-9]{8}:[a-f0-9]{8})@(?:gems.contribsys.com|enterprise.contribsys.com)(?:[\/|\#|\?|:]|$)`), + Keywords: []string{"gems.contribsys.com", "enterprise.contribsys.com"}, + } + + // validate + tps := []string{ + "https://cafebabe:deadbeef@gems.contribsys.com/", + "https://cafebabe:deadbeef@gems.contribsys.com", + "https://cafeb4b3:d3adb33f@enterprise.contribsys.com/", + "https://cafeb4b3:d3adb33f@enterprise.contribsys.com", + "http://cafebabe:deadbeef@gems.contribsys.com/", + "http://cafebabe:deadbeef@gems.contribsys.com", + "http://cafeb4b3:d3adb33f@enterprise.contribsys.com/", + "http://cafeb4b3:d3adb33f@enterprise.contribsys.com", + "http://cafeb4b3:d3adb33f@enterprise.contribsys.com#heading1", + "http://cafeb4b3:d3adb33f@enterprise.contribsys.com?param1=true¶m2=false", + "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80", + "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true¶m2=false#heading1", + } + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/slack.go b/cmd/generate/config/rules/slack.go new file mode 100644 index 0000000..31fe185 --- /dev/null +++ b/cmd/generate/config/rules/slack.go @@ -0,0 +1,296 @@ +package rules + +import ( + "fmt" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +// https://api.slack.com/authentication/token-types#bot +func SlackBotToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "slack-bot-token", + Description: "Identified a Slack Bot token, which may compromise bot integrations and communication channel security.", + Regex: regexp.MustCompile(`xoxb-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*`), + Entropy: 3, + Keywords: []string{ + "xoxb", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("bot", "xoxb-781236542736-2364535789652-GkwFDQoHqzXDVsC6GzqYUypD") + tps = append(tps, + // https://github.com/metabase/metabase/blob/74cfb332140680425c7d37d347854160cc997ea8/frontend/src/metabase/admin/settings/slack/components/SlackForm/SlackForm.tsx#L47 + `"bot_token1": "xoxb-781236542736-2364535789652-GkwFDQoHqzXDVsC6GzqYUypD"`, // gitleaks:allow + // https://github.com/jonz-secops/TokenTester/blob/978e9f3eabc7e9978769cfbba10735afa3bf627e/slack#LL44C27-L44C86 + `"bot_token2": "xoxb-263594206564-2343594206574-FGqddMF8t08v8N7Oq4i57vs1MBS"`, // gitleaks:allow + `"bot_token3": "xoxb-4614724432022-5152386766518-O5WzjWGLG0wcCm2WPrjEmnys"`, // gitleaks:allow + `"bot_token4": `+fmt.Sprintf(`"xoxb-%s-%s-%s"`, secrets.NewSecret(utils.Numeric("13")), secrets.NewSecret(utils.Numeric("12")), secrets.NewSecret(utils.AlphaNumeric("24"))), + ) + fps := []string{ + "xoxb-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx", + "xoxb-xxx", + "xoxb-12345-abcd234", + "xoxb-xoxb-my-bot-token", + } + return utils.Validate(r, tps, fps) +} + +// https://api.slack.com/authentication/token-types#user +func SlackUserToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "slack-user-token", + Description: "Found a Slack User token, posing a risk of unauthorized user impersonation and data access within Slack workspaces.", + // The last segment seems to be consistently 32 characters. I've made it 28-34 just in case. + Regex: regexp.MustCompile(`xox[pe](?:-[0-9]{10,13}){3}-[a-zA-Z0-9-]{28,34}`), + Entropy: 2, + Keywords: []string{"xoxp-", "xoxe-"}, + } + + // validate + tps := utils.GenerateSampleSecrets("user", "xoxp-41684372915-1320496754-45609968301-e708ba56e1517a99f6b5fb07349476ef") + tps = append(tps, + // https://github.com/jonz-secops/TokenTester/blob/978e9f3eabc7e9978769cfbba10735afa3bf627e/slack#L25 + `"user_token1": "xoxp-41684372915-1320496754-45609968301-e708ba56e1517a99f6b5fb07349476ef"`, // gitleaks:allow + // https://github.com/praetorian-inc/noseyparker/blob/16e0e5768fd14ea54f6c9a058566184d88343bb4/crates/noseyparker/data/default/rules/slack.yml#L29 + `"user_token2": "xoxp-283316862324-298911817009-298923149681-44f585044dace54f5701618e97cd1c0b"`, // gitleaks:allow + // https://github.com/CloudBoost/cloudboost/blob/7ba2ed17099fa85e6fc652302822601283c6fa13/user-service/services/mailService.js#LL248C17-L248C92 + `"user_token3": "xoxp-11873098179-111402824422-234336993777-b96c9fb3b69f82ebb79d12f280779de1"`, // gitleaks:allow + // https://github.com/evanyeung/terminal-slack/blob/b068f77808de72424d08b525d6cbf814849acd08/readme.md?plain=1#L66 + `"user_token4": "xoxp-254112160503-252950188691-252375361712-6cbf56aada30951a9d310a5f23d032a0"`, // gitleaks:allow + `"user_token5": "xoxp-4614724432022-4621207627011-5182682871568-1ddad9823e8528ad0f4944dfa3c6fc6c"`, // gitleaks:allow + `"user_token6": `+fmt.Sprintf(`"xoxp-%s-%s-%s-%s"`, secrets.NewSecret(utils.Numeric("12")), secrets.NewSecret(utils.Numeric("13")), secrets.NewSecret(utils.Numeric("13")), secrets.NewSecret(utils.AlphaNumeric("32"))), + // It's unclear what the `xoxe-` token means in this context, however, the format is similar to a user token. + `"url_private": "https:\/\/files.slack.com\/files-pri\/T04MCQMEXQ9-F04MAA1PKE3\/image.png?t=xoxe-4726837507825-4848681849303-4856614048758-e0b1f3d4cb371f92260edb0d9444d206"`, + ) + fps := []string{ + `https://docs.google.com/document/d/1W7KCxOxP-1Fy5EyF2lbJGE2WuKmu5v0suYqoHas1jRM`, + `"token1": "xoxp-1234567890"`, // gitleaks:allow + `"token2": "xoxp-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"`, // gitleaks:allow + `"token3": "xoxp-1234-1234-1234-4ddbc191d40ee098cbaae6f3523ada2d"`, // gitleaks:allow + `"token4": "xoxp-572370529330-573807301142-572331691188-####################"`, // gitleaks:allow + // This technically matches the pattern but is an obvious false positive. + // `"token5": "xoxp-000000000000-000000000000-000000000000-00000000000000000000000000000000"`, // gitleaks:allow + } + return utils.Validate(r, tps, fps) +} + +// Reference: https://api.slack.com/authentication/token-types#app +func SlackAppLevelToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "slack-app-token", + Description: "Detected a Slack App-level token, risking unauthorized access to Slack applications and workspace data.", + // This regex is based on a limited number of examples and may not be 100% accurate. + Regex: regexp.MustCompile(`(?i)xapp-\d-[A-Z0-9]+-\d+-[a-z0-9]+`), + Entropy: 2, + Keywords: []string{"xapp"}, + } + + tps := utils.GenerateSampleSecrets("slack", "xapp-1-A052FGTS2DL-5171572773297-610b6a11f4b7eb819e87b767d80e6575a3634791acb9a9ead051da879eb5b55e") + tps = append(tps, + // https://github.com/jonz-secops/TokenTester/blob/978e9f3eabc7e9978769cfbba10735afa3bf627e/slack#L17 + `"token1": "xapp-1-A052FGTS2DL-5171572773297-610b6a11f4b7eb819e87b767d80e6575a3634791acb9a9ead051da879eb5b55e"`, // gitleaks:allow + `"token2": "xapp-1-IEMF8IMY1OQ-4037076220459-85c370b433e366de369c4ef5abdf41253519266982439a75af74a3d68d543fb6"`, // gitleaks:allow + `"token3": "xapp-1-BM3V7LC51DA-1441525068281-86641a2582cd0903402ab523e5bcc53b8253098c31591e529b55b41974d2e82f"`, // gitleaks:allow + `"token4": `+fmt.Sprintf(`"xapp-1-A%s-%s-%s"`, secrets.NewSecret(utils.Numeric("10")), secrets.NewSecret(utils.Numeric("13")), secrets.NewSecret(utils.AlphaNumeric("64"))), + ) + return utils.Validate(r, tps, nil) +} + +// Reference: https://api.slack.com/authentication/config-tokens +func SlackConfigurationToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "slack-config-access-token", + Description: "Found a Slack Configuration access token, posing a risk to workspace configuration and sensitive data access.", + Regex: regexp.MustCompile(`(?i)xoxe.xox[bp]-\d-[A-Z0-9]{163,166}`), + Entropy: 2, + Keywords: []string{"xoxe.xoxb-", "xoxe.xoxp-"}, + } + + tps := utils.GenerateSampleSecrets("access", "xoxe.xoxp-1-Mi0yLTM0MTQwNDE0MDE3Ni0zNjU5NDY0Njg4MTctNTE4MjA3NTQ5NjA4MC01NDEyOTYyODY5NzUxLThhMTBjZmI1ZWIzMGIwNTg0ZDdmMDI5Y2UxNzVlZWVhYzU2ZWQyZTZiODNjNDZiMGUxMzRlNmNjNDEwYmQxMjQ") + tps = append(tps, + `"access_token1": "xoxe.xoxp-1-Mi0yLTM0MTQwNDE0MDE3Ni0zNjU5NDY0Njg4MTctNTE4MjA3NTQ5NjA4MC01NDEyOTYyODY5NzUxLThhMTBjZmI1ZWIzMGIwNTg0ZDdmMDI5Y2UxNzVlZWVhYzU2ZWQyZTZiODNjNDZiMGUxMzRlNmNjNDEwYmQxMjQ"`, // gitleaks:allow + `"access_token2": "xoxe.xoxp-1-Mi0yLTMxNzcwMjQ0MTcxMy0zNjU5NDY0Njg4MTctNTE1ODE1MjY5MTcxNC01MTU4MDI0MTgyOTc5LWRmY2YwY2U4ODhhNzY5ZGU5MTAyNDU4MDJjMGQ0ZDliMTZhMjNkMmEyYzliNjkzMDRlN2VjZTI4MWNiMzRkNGQ"`, // gitleaks:allow + `"access_token3": "xoxe.xoxp-1-`+secrets.NewSecret(utils.AlphaNumeric("163"))+`"`, + `"access_token4": "xoxe.xoxb-1-Mi0yLTMxNzcwMjQ0MTcxMy0zNjU5NDY0Njg4MTctNTE1ODE1MjY5MTcxNC01MTU4MDI0MTgyOTc5LWRmY2YwY2U4ODhhNzY5ZGU5MTAyNDU4MDJjMGQ0ZDliMTZhMjNkMmEyYzliNjkzMDRlN2VjZTI4MWNiMzRkNGQ"`, + `"access_token5": "xoxe.xoxb-1-`+secrets.NewSecret(utils.AlphaNumeric("165"))+`"`, + ) + fps := []string{ + "xoxe.xoxp-1-SlackAppConfigurationAccessTokenHere", + "xoxe.xoxp-1-RANDOMSTRINGHERE", + "xoxe.xoxp-1-initial", + } + return utils.Validate(r, tps, fps) +} + +// Reference: https://api.slack.com/authentication/config-tokens +func SlackConfigurationRefreshToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "slack-config-refresh-token", + Description: "Discovered a Slack Configuration refresh token, potentially allowing prolonged unauthorized access to configuration settings.", + Regex: regexp.MustCompile(`(?i)xoxe-\d-[A-Z0-9]{146}`), + Entropy: 2, + Keywords: []string{"xoxe-"}, + } + + tps := utils.GenerateSampleSecrets("refresh", "xoxe-1-My0xLTMxNzcwMjQ0MTcxMy01MTU4MTUyNjkxNzE0LTUxODE4NDI0MDY3MzYtMjA5MGFkOTFlZThkZWE2OGFlZDYwYWJjODNhYzAxYjA5ZjVmODBhYjgzN2QyNDdjOTNlOGY5NTg2YWM1OGM4Mg") + tps = append(tps, + `"refresh_token1": "xoxe-1-My0xLTMxNzcwMjQ0MTcxMy01MTU4MTUyNjkxNzE0LTUxODE4NDI0MDY3MzYtMjA5MGFkOTFlZThkZWE2OGFlZDYwYWJjODNhYzAxYjA5ZjVmODBhYjgzN2QyNDdjOTNlOGY5NTg2YWM1OGM4Mg"`, // gitleaks:allow + `"refresh_token2": "xoxe-1-My0xLTM0MTQwNDE0MDE3Ni01MTgyMDc1NDk2MDgwLTU0MjQ1NjIwNzgxODEtNGJkYTZhYTUxY2M1ODk3ZTNkN2YzMTgxMDI1ZDQzNzgwNWY4NWQ0ODdhZGIzM2ViOGI0MTM0MjdlNGVmYzQ4Ng"`, // gitleaks:allow + `"refresh_token3": "xoxe-1-`+secrets.NewSecret(utils.AlphaNumeric("146"))+`"`, + ) + fps := []string{"xoxe-1-xxx", "XOxE-RROAmw, Home and Garden, 5:24, 20120323"} + return utils.Validate(r, tps, fps) +} + +// Reference: https://api.slack.com/authentication/token-types#legacy_bot +func SlackLegacyBotToken() *config.Rule { + r := config.Rule{ + RuleID: "slack-legacy-bot-token", + Description: "Uncovered a Slack Legacy bot token, which could lead to compromised legacy bot operations and data exposure.", + // This rule is based off the limited information I could find and may not be 100% accurate. + Regex: regexp.MustCompile(`xoxb-[0-9]{8,14}-[a-zA-Z0-9]{18,26}`), + Entropy: 2, + Keywords: []string{ + "xoxb", + }, + } + + tps := utils.GenerateSampleSecrets("slack", "xoxb-263594206564-FGqddMF8t08v8N7Oq4i57vs1") + tps = append(tps, + // https://github.com/jonz-secops/TokenTester/blob/978e9f3eabc7e9978769cfbba10735afa3bf627e/slack#LL42C38-L42C80 + `"bot_token1": "xoxb-263594206564-FGqddMF8t08v8N7Oq4i57vs1"`, // gitleaks:allow + // https://heejune.me/2018/08/01/crashdump-analysis-automation-using-slackbot-python-cdb-from-windows/ + `"bot_token2": "xoxb-282029623751-BVtmnS3BQitmjZvjpQL7PSGP"`, // gitleaks:allow + // https://github.com/praetorian-inc/noseyparker/blob/16e0e5768fd14ea54f6c9a058566184d88343bb4/crates/noseyparker/data/default/rules/slack.yml#L15 + `"bot_token3": "xoxb-47834520726-N3otsrwj8Cf99cs8GhiRZsX1"`, // gitleaks:allow + // https://github.com/pulumi/examples/blob/32d9047c19c2a9380c04e57a764321c25eef45b0/aws-js-sqs-slack/README.md?plain=1#L39 + `"bot_token4": "xoxb-123456789012-Xw937qtWSXJss1lFaKe"`, // gitleaks:allow + // https://github.com/ilyasProgrammer/Odoo-eBay-Amazon/blob/a9c4a8a7548b19027bc0fd904f8ae9249248a293/custom_logging/models.py#LL9C24-L9C66 + `"bot_token5": "xoxb-312554961652-uSmliU84rFhnUSBq9YdKh6lS"`, // gitleaks:allow + // https://github.com/jay-johnson/sci-pype/blob/6bff42ea4eb32d35b9f223db312e4cd0d3911100/src/pycore.py#L37 + `"bot_token6": "xoxb-51351043345-Lzwmto5IMVb8UK36MghZYMEi"`, // gitleaks:allow + // https://github.com/logicmoo/logicmoo_workspace/blob/2e1794f596121c9949deb3bfbd30d5b027a51d3d/packs_sys/slack_prolog/prolog/slack_client_old.pl#L28 + `"bot_token7": "xoxb-130154379991-ogFL0OFP3w6AwdJuK7wLojpK"`, // gitleaks:allow + // https://github.com/sbarski/serverless-chatbot/blob/7d556897486f3fd53795907b7e33252e5cc6b3a3/Lesson%203/serverless.yml#L38 + `"bot_token8": "xoxb-159279836768-FOst5DLfEzmQgkz7cte5qiI"`, // gitleaks:allow + `"bot_token9": "xoxb-50014434-slacktokenx29U9X1bQ"`, // gitleaks:allow + `"bot_token10": `+fmt.Sprintf(`"xoxb-%s-%s`, secrets.NewSecret(utils.Numeric("10")), secrets.NewSecret(utils.AlphaNumeric("24"))), // gitleaks:allow + `"bot_token11": `+fmt.Sprintf(`"xoxb-%s-%s`, secrets.NewSecret(utils.Numeric("12")), secrets.NewSecret(utils.AlphaNumeric("23"))), // gitleaks:allow + ) + fps := []string{ + "xoxb-xxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx", // gitleaks:allow + "xoxb-Slack_BOT_TOKEN", + "xoxb-abcdef-abcdef", + // "xoxb-0000000000-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // gitleaks:allow + } + return utils.Validate(r, tps, fps) +} + +// Reference: https://api.slack.com/authentication/token-types#workspace +func SlackLegacyWorkspaceToken() *config.Rule { + r := config.Rule{ + RuleID: "slack-legacy-workspace-token", + Description: "Identified a Slack Legacy Workspace token, potentially compromising access to workspace data and legacy features.", + // This is by far the least confident pattern. + Regex: regexp.MustCompile(`xox[ar]-(?:\d-)?[0-9a-zA-Z]{8,48}`), + Entropy: 2, + Keywords: []string{ + "xoxa", + "xoxr", + }, + } + + tps := utils.GenerateSampleSecrets("slack", "xoxa-2-511111111-31111111111-3111111111111-e039d02840a0b9379c") + tps = append(tps, + `"access_token": "xoxa-2-511111111-31111111111-3111111111111-e039d02840a0b9379c"`, // gitleaks:allow + `"access_token1": `+fmt.Sprintf(`"xoxa-%s-%s`, secrets.NewSecret(utils.Numeric("1")), secrets.NewSecret(utils.AlphaNumeric("12"))), + `"access_token2": `+fmt.Sprintf(`"xoxa-%s`, secrets.NewSecret(utils.AlphaNumeric("12"))), + `"refresh_token1": `+fmt.Sprintf(`"xoxr-%s-%s`, secrets.NewSecret(utils.Numeric("1")), secrets.NewSecret(utils.AlphaNumeric("12"))), + `"refresh_token2": `+fmt.Sprintf(`"xoxr-%s`, secrets.NewSecret(utils.AlphaNumeric("12"))), + ) + fps := []string{ + // "xoxa-faketoken", + // "xoxa-access-token-string", + // "XOXa-nx991k", + "https://github.com/xoxa-nyc/xoxa-nyc.github.io/blob/master/README.md", + } + return utils.Validate(r, tps, fps) +} + +// References: +// - https://api.slack.com/authentication/token-types#legacy +// - https://api.slack.com/changelog/2016-05-19-authorship-changing-for-older-tokens +// - https://github.com/jonz-secops/TokenTester/blob/978e9f3eabc7e9978769cfbba10735afa3bf627e/slack#L29 +// - https://gist.github.com/thesubtlety/a1c460d53df0837c5817c478b9f10588#file-local-slack-jack-py-L32 +func SlackLegacyToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "slack-legacy-token", + Description: "Detected a Slack Legacy token, risking unauthorized access to older Slack integrations and user data.", + Regex: regexp.MustCompile(`xox[os]-\d+-\d+-\d+-[a-fA-F\d]+`), + Entropy: 2, + Keywords: []string{"xoxo", "xoxs"}, + } + + // validate + tps := utils.GenerateSampleSecrets("slack", "xoxs-416843729158-132049654-5609968301-e708ba56e1") + tps = append(tps, + // https://github.com/GGStudy-DDUp/https-github.com-aldaor-HackerOneReports/blob/637e9261b63a7292a3a7ddf4bf13729c224d84df/PrivilegeEscalation/47940.txt#L23 + `"access_token1": "xoxs-3206092076-3204538285-3743137121-836b042620"`, // gitleaks:allow + // https://github.com/jonz-secops/TokenTester/blob/978e9f3eabc7e9978769cfbba10735afa3bf627e/slack#L28 + `"access_token2": "xoxs-416843729158-132049654-5609968301-e708ba56e1"`, // gitleaks:allow + // https://github.com/clr2of8/SlackExtract/blob/18d151152ff5a45b293d4b7193aa6d08f9ab1bfd/README.md?plain=1#L32 + `"access_token3": "xoxs-420083410720-421837374423-440811613314-977844f625b707d5b0b268206dbc92cbc85feef3e71b08e44815a8e6e7657190"`, // gitleaks:allow + // https://github.com/zeroc00I/AllVideoPocsFromHackerOne/blob/95ae92f65ccef11c2c6acdaabfb7cc9b2b0eb4c6/jsonReports/61312.json#LL1C17-L1C17 + `"access_token4": "xoxs-4829527689-4829527691-4814341714-d0346ec616"`, // gitleaks:allow + // https://github.com/ericvanderwal/general-playmaker/blob/34bd8e82e2d7b16ca9cc825d0c9d383b8378b550/Logic/setrandomseedtype.cs#LL783C15-L783C69 + `"access_token5": "xoxs-155191149137-155868813314-338998331396-9f6d235915"`, // gitleaks:allow + `"access_token6": "xoxs-`+fmt.Sprintf("%s-%s-%s-%s", secrets.NewSecret(utils.Numeric("10")), secrets.NewSecret(utils.Numeric("10")), secrets.NewSecret(utils.Numeric("10")), secrets.NewSecret(utils.Hex("10")))+`"`, + `"access_token7": "xoxo-523423-234243-234233-e039d02840a0b9379c"`, // gitleaks:allow + ) + fps := []string{ + "https://indieweb.org/images/3/35/2018-250-xoxo-indieweb-1.jpg", + "https://lh3.googleusercontent.com/-tWXjX3LUD6w/Ua4La_N5E2I/AAAAAAAAACg/qcm19xbEYa4/s640/EXO-XOXO-teaser-exo-k-34521098-720-516.jpg", + } + return utils.Validate(r, tps, fps) +} + +func SlackWebHookUrl() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "slack-webhook-url", + Description: "Discovered a Slack Webhook, which could lead to unauthorized message posting and data leakage in Slack channels.", + // If this generates too many false-positives we should define an allowlist (e.g., "xxxx", "00000"). + Regex: regexp.MustCompile( + `(?:https?://)?hooks.slack.com/(?:services|workflows|triggers)/[A-Za-z0-9+/]{43,56}`), + Keywords: []string{ + "hooks.slack.com", + }, + } + + // validate + tps := []string{ + "hooks.slack.com/services/" + secrets.NewSecret(utils.AlphaNumeric("44")), + "http://hooks.slack.com/services/" + secrets.NewSecret(utils.AlphaNumeric("45")), + "https://hooks.slack.com/services/" + secrets.NewSecret(utils.AlphaNumeric("46")), + "http://hooks.slack.com/services/T024TTTTT/BBB72BBL/AZAAA9u0pA4ad666eMgbi555", // gitleaks:allow + "https://hooks.slack.com/services/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", // gitleaks:allow + "hooks.slack.com/workflows/" + secrets.NewSecret(utils.AlphaNumeric("44")), + "http://hooks.slack.com/workflows/" + secrets.NewSecret(utils.AlphaNumeric("45")), + "https://hooks.slack.com/workflows/" + secrets.NewSecret(utils.AlphaNumeric("46")), + "https://hooks.slack.com/workflows/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", // gitleaks:allow + "http://hooks.slack.com/workflows/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", // gitleaks:allow + "https://hooks.slack.com/triggers/" + secrets.NewSecret(utils.AlphaNumeric("56")), + } + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/snyk.go b/cmd/generate/config/rules/snyk.go new file mode 100644 index 0000000..3165f2b --- /dev/null +++ b/cmd/generate/config/rules/snyk.go @@ -0,0 +1,32 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/config" +) + +func Snyk() *config.Rule { + // define rule + r := config.Rule{ + Description: "Uncovered a Snyk API token, potentially compromising software vulnerability scanning and code security.", + RuleID: "snyk-api-token", + + Regex: utils.GenerateSemiGenericRegex([]string{"snyk[_.-]?(?:(?:api|oauth)[_.-]?)?(?:key|token)"}, utils.Hex8_4_4_4_12(), true), + Keywords: []string{"snyk"}, + } + + // validate + tps := utils.GenerateSampleSecrets("snyk", "12345678-ABCD-ABCD-ABCD-1234567890AB") + tps = append(tps, + `const SNYK_TOKEN = "12345678-ABCD-ABCD-ABCD-1234567890AB"`, // gitleaks:allow + `const SNYK_KEY = "12345678-ABCD-ABCD-ABCD-1234567890AB"`, // gitleaks:allow + `SNYK_TOKEN := "12345678-ABCD-ABCD-ABCD-1234567890AB"`, // gitleaks:allow + `SNYK_TOKEN ::= "12345678-ABCD-ABCD-ABCD-1234567890AB"`, // gitleaks:allow + `SNYK_TOKEN :::= "12345678-ABCD-ABCD-ABCD-1234567890AB"`, // gitleaks:allow + `SNYK_TOKEN ?= "12345678-ABCD-ABCD-ABCD-1234567890AB"`, // gitleaks:allow + `SNYK_API_KEY ?= "12345678-ABCD-ABCD-ABCD-1234567890AB"`, // gitleaks:allow + `SNYK_API_TOKEN = "12345678-ABCD-ABCD-ABCD-1234567890AB"`, // gitleaks:allow + `SNYK_OAUTH_TOKEN = "12345678-ABCD-ABCD-ABCD-1234567890AB"`, // gitleaks:allow + ) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/sonar.go b/cmd/generate/config/rules/sonar.go new file mode 100644 index 0000000..feb690d --- /dev/null +++ b/cmd/generate/config/rules/sonar.go @@ -0,0 +1,31 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/config" +) + +func Sonar() *config.Rule { + // define rule + r := config.Rule{ + Description: "Uncovered a Sonar API token, potentially compromising software vulnerability scanning and code security.", + RuleID: "sonar-api-token", + Regex: utils.GenerateSemiGenericRegex([]string{"sonar[_.-]?(login|token)"}, "(?:squ_|sqp_|sqa_)?"+utils.AlphaNumericExtended("40"), true), + Keywords: []string{"sonar"}, + SecretGroup: 2, + } + + // validate + tps := utils.GenerateSampleSecrets("sonar", "12345678ABCDEFH1234567890ABCDEFH12345678") + tps = append(tps, + `const SONAR_LOGIN = "12345678ABCDEFH1234567890ABCDEFH12345678"`, // gitleaks:allow + `SONAR_LOGIN := "12345678ABCDEFH1234567890ABCDEFH12345678"`, // gitleaks:allow + `SONAR.LOGIN ::= "12345678ABCDEFH1234567890ABCDEFH12345678"`, // gitleaks:allow + `SONAR.LOGIN :::= "12345678ABCDEFH1234567890ABCDEFH12345678"`, // gitleaks:allow + `SONAR.LOGIN ?= "12345678ABCDEFH1234567890ABCDEFH12345678"`, // gitleaks:allow + `const SONAR_TOKEN = "squ_12345678ABCDEFH1234567890ABCDEFH12345678"`, // gitleaks:allow + `SONAR_LOGIN := "sqp_12345678ABCDEFH1234567890ABCDEFH12345678"`, // gitleaks:allow + `SONAR.TOKEN = "sqa_12345678ABCDEFH1234567890ABCDEFH12345678"`, // gitleaks:allow + ) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/sourcegraph.go b/cmd/generate/config/rules/sourcegraph.go new file mode 100644 index 0000000..070a4da --- /dev/null +++ b/cmd/generate/config/rules/sourcegraph.go @@ -0,0 +1,34 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/config" +) + +func SourceGraph() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "sourcegraph-access-token", + Description: "Sourcegraph is a code search and navigation engine.", + Regex: utils.GenerateUniqueTokenRegex(`\b(sgp_(?:[a-fA-F0-9]{16}|local)_[a-fA-F0-9]{40}|sgp_[a-fA-F0-9]{40}|[a-fA-F0-9]{40})\b`, true), + Entropy: 3, + Keywords: []string{"sgp_", "sourcegraph"}, + } + + // validate + tps := []string{ + `sgp_AaD80dc6E02eCAE1_d3cba16CC0F18fA14A2EFB61CbDFceEBf9fAD16b`, + `sourcegraph: 6d2FabeB6ADd229Bc199FabA28fD3efb57dF0bD3`, + `sgp_0D697F54cb24238EefB29af05Abf1b505E90950F`, + `sgp_local_d7dfFD43cF2503B1da673EB560aAa3e80f16FA42`, + `sgp_local_bcD1DA18de0d6476Be0f3BD7Ef9Da4f09b479aE5`, + } + fps := []string{ + `sgp_5555555dAAAAA7777777CcccCFaaaaaaaaaaaaaa`, // low entropy + `sgp_local_d45b6G86aBb0F2Cee943902dbaDBCFCFDD1dA089`, // invalid case + `sgp_652d9a2e48FC7E!FcDbEA1BC2E2A6CE23cFe7F7D`, // invalid character + `sgp_78Ad84a5B6e8A2fE5B_4085FB0ccaDDd29DB66Fd7FE9bA2C1cdCE8400CD`, // invalid length + `BcAeb6640ad7DAD46AD73687946Ce85047d5C9Bb`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/square.go b/cmd/generate/config/rules/square.go new file mode 100644 index 0000000..aa46fba --- /dev/null +++ b/cmd/generate/config/rules/square.go @@ -0,0 +1,47 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func SquareAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "square-access-token", + Description: "Detected a Square Access Token, risking unauthorized payment processing and financial transaction exposure.", + Regex: utils.GenerateUniqueTokenRegex(`(?:EAAA|sq0atp-)[\w-]{22,60}`, false), + Entropy: 2, + Keywords: []string{"sq0atp-", "EAAA"}, + } + + // validate + tps := utils.GenerateSampleSecrets("square", secrets.NewSecret(`(?:EAAA|sq0atp-)[\w-]{22,60}`)) + tps = append(tps, + "ARG token=sq0atp-812erere3wewew45678901", // gitleaks:allow + "ARG token=EAAAlsBxkkVgvmr7FasTFbM6VUGZ31EJ4jZKTJZySgElBDJ_wyafHuBFquFexY7E", // gitleaks:allow", + ) + fps := []string{ + `aws-cli@sha256:eaaa7b11777babe28e6133a8b19ff71cea687e0d7f05158dee95a71f76ce3d00`, + } + return utils.Validate(r, tps, fps) +} + +func SquareSecret() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "square-secret", + Description: "Square Secret", + Regex: utils.GenerateUniqueTokenRegex(`sq0csp-[\w-]{43}`, false), + Entropy: 2, + Keywords: []string{"sq0csp-"}, + } + + // validate + tps := utils.GenerateSampleSecrets("square", secrets.NewSecret(`sq0csp-[0-9A-Za-z\\-_]{43}`)) + tps = append(tps, + `value: "sq0csp-0p9h7g6f4s3s3s3-4a3ardgwa6ADRDJDDKUFYDYDYDY"`, // gitleaks:allow + ) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/squarespace.go b/cmd/generate/config/rules/squarespace.go new file mode 100644 index 0000000..eed6061 --- /dev/null +++ b/cmd/generate/config/rules/squarespace.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func SquareSpaceAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "squarespace-access-token", + Description: "Identified a Squarespace Access Token, which may compromise website management and content control on Squarespace.", + Regex: utils.GenerateSemiGenericRegex([]string{"squarespace"}, utils.Hex8_4_4_4_12(), true), + + Keywords: []string{ + "squarespace", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("squarespace", secrets.NewSecret(utils.Hex8_4_4_4_12())) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/stopwords.go b/cmd/generate/config/rules/stopwords.go new file mode 100644 index 0000000..66a3de1 --- /dev/null +++ b/cmd/generate/config/rules/stopwords.go @@ -0,0 +1,1491 @@ +package rules + +// TODO introduce skiplists: +// https://github.com/danielmiessler/SecLists/blob/master/Miscellaneous/wordlist-skipfish.fuzz.txt +// https://github.com/e3b0c442/keywords +// https://gist.github.com/maxtruxa/b2ca551e42d3aead2b3d +// https://github.com/HChakraborty/projects/commit/e860cb863ee9585c38db8360814b04ef9fa1bdce +// https://github.com/UraniumX92/Discord-Bot-using-py/tree/224b2b71a58c25f420ce980f2ea49627b4b646f1/Data%20Files +// https://github.com/Meen11/BSBI-Indexing/blob/63032017aa24f3111f18468607cd0db5997bb891/datasets/citeseer/11/10.1.1.27.6385.txt + +var DefaultStopWords = []string{ + "000000", + "aaaaaa", + "about", + "abstract", + "academy", + "acces", + "account", + "act-", + "act.", + "act_", + "action", + "active", + "actively", + "activity", + "adapter", + "add-", + "add.", + "add_", + "add-on", + "addon", + "addres", + "admin", + "adobe", + "advanced", + "adventure", + "agent", + "agile", + "air-", + "air.", + "air_", + "ajax", + "akka", + "alert", + "alfred", + "algorithm", + "all-", + "all.", + "all_", + "alloy", + "alpha", + "amazon", + "amqp", + "analysi", + "analytic", + "analyzer", + "android", + "angular", + "angularj", + "animate", + "animation", + "another", + "ansible", + "answer", + "ant-", + "ant.", + "ant_", + "any-", + "any.", + "any_", + "apache", + // "api-", + // "api.", + // "api_", lin_api_ is used for linear + "app-", + "app-", + "app.", + "app.", + "app_", + "app_", + "apple", + "arch", + "archive", + "archived", + "arduino", + "array", + "art-", + "art.", + "art_", + "article", + "asp-", + "asp.", + "asp_", + "asset", + "async", + "atom", + "attention", + "audio", + "audit", + "aura", + "auth", + "author", + "author", + "authorize", + "auto", + "automated", + "automatic", + "awesome", + "aws_", + "azure", + "back", + "backbone", + "backend", + "backup", + "bar-", + "bar.", + "bar_", + "base", + "based", + "bash", + "basic", + "batch", + "been", + "beer", + "behavior", + "being", + "benchmark", + "best", + "beta", + "better", + "big-", + "big.", + "big_", + "binary", + "binding", + "bit-", + "bit.", + "bit_", + "bitcoin", + "block", + "blog", + "board", + "book", + "bookmark", + "boost", + "boot", + "bootstrap", + "bosh", + "bot-", + "bot.", + "bot_", + "bower", + "box-", + "box.", + "box_", + "boxen", + "bracket", + "branch", + "bridge", + "browser", + "brunch", + "buffer", + "bug-", + "bug.", + "bug_", + "build", + "builder", + "building", + "buildout", + "buildpack", + "built", + "bundle", + "busines", + "but-", + "but.", + "but_", + "button", + "cache", + "caching", + "cakephp", + "calendar", + "call", + "camera", + "campfire", + "can-", + "can.", + "can_", + "canva", + "captcha", + "capture", + "card", + "carousel", + "case", + "cassandra", + "cat-", + "cat.", + "cat_", + "category", + "center", + "cento", + "challenge", + "change", + "changelog", + "channel", + "chart", + "chat", + "cheat", + "check", + "checker", + "chef", + "ches", + "chinese", + "chosen", + "chrome", + "ckeditor", + "clas", + "classe", + "classic", + "clean", + "cli-", + "cli.", + "cli_", + "client", + "client", + "clojure", + "clone", + "closure", + "cloud", + "club", + "cluster", + "cms-", + "cms_", + "coco", + "code", + "coding", + "coffee", + "color", + "combination", + "combo", + "command", + "commander", + "comment", + "commit", + "common", + "community", + "compas", + "compiler", + "complete", + "component", + "composer", + "computer", + "computing", + "con-", + "con.", + "con_", + "concept", + "conf", + "config", + "config", + "connect", + "connector", + "console", + "contact", + "container", + "contao", + "content", + "contest", + "context", + "control", + "convert", + "converter", + "conway'", + "cookbook", + "cookie", + "cool", + "copy", + "cordova", + "core", + "couchbase", + "couchdb", + "countdown", + "counter", + "course", + "craft", + "crawler", + "create", + "creating", + "creator", + "credential", + "crm-", + "crm.", + "crm_", + "cros", + "crud", + "csv-", + "csv.", + "csv_", + "cube", + "cucumber", + "cuda", + "current", + "currently", + "custom", + "daemon", + "dark", + "dart", + "dash", + "dashboard", + "data", + "database", + "date", + "day-", + "day.", + "day_", + "dead", + "debian", + "debug", + "debug", + "debugger", + "deck", + "define", + "del-", + "del.", + "del_", + "delete", + "demo", + "deploy", + "design", + "designer", + "desktop", + "detection", + "detector", + "dev-", + "dev.", + "dev_", + "develop", + "developer", + "device", + "devise", + "diff", + "digital", + "directive", + "directory", + "discovery", + "display", + "django", + "dns-", + "dns_", + "doc-", + "doc-", + "doc.", + "doc.", + "doc_", + "doc_", + "docker", + "docpad", + "doctrine", + "document", + "doe-", + "doe.", + "doe_", + "dojo", + "dom-", + "dom.", + "dom_", + "domain", + "done", + "don't", + "dot-", + "dot.", + "dot_", + "dotfile", + "download", + "draft", + "drag", + "drill", + "drive", + "driven", + "driver", + "drop", + "dropbox", + "drupal", + "dsl-", + "dsl.", + "dsl_", + "dynamic", + "easy", + "_ec2_", + "ecdsa", + "eclipse", + "edit", + "editing", + "edition", + "editor", + "element", + "emac", + "email", + "embed", + "embedded", + "ember", + "emitter", + "emulator", + "encoding", + "endpoint", + "engine", + "english", + "enhanced", + "entity", + "entry", + "env_", + "episode", + "erlang", + "error", + "espresso", + "event", + "evented", + "example", + "example", + "exchange", + "exercise", + "experiment", + "expire", + "exploit", + "explorer", + "export", + "exporter", + "expres", + "ext-", + "ext.", + "ext_", + "extended", + "extension", + "external", + "extra", + "extractor", + "fabric", + "facebook", + "factory", + "fake", + "fast", + "feature", + "feed", + "fewfwef", + "ffmpeg", + "field", + "file", + "filter", + "find", + "finder", + "firefox", + "firmware", + "first", + "fish", + "fix-", + "fix_", + "flash", + "flask", + "flat", + "flex", + "flexible", + "flickr", + "flow", + "fluent", + "fluentd", + "fluid", + "folder", + "font", + "force", + "foreman", + "fork", + "form", + "format", + "formatter", + "forum", + "foundry", + "framework", + "free", + "friend", + "friendly", + "front-end", + "frontend", + "ftp-", + "ftp.", + "ftp_", + "fuel", + "full", + "fun-", + "fun.", + "fun_", + "func", + "future", + "gaia", + "gallery", + "game", + "gateway", + "gem-", + "gem.", + "gem_", + "gen-", + "gen.", + "gen_", + "general", + "generator", + "generic", + "genetic", + "get-", + "get.", + "get_", + "getenv", + "getting", + "ghost", + "gist", + "git-", + "git.", + "git_", + "github", + "gitignore", + "gitlab", + "glas", + "gmail", + "gnome", + "gnu-", + "gnu.", + "gnu_", + "goal", + "golang", + "gollum", + "good", + "google", + "gpu-", + "gpu.", + "gpu_", + "gradle", + "grail", + "graph", + "graphic", + "great", + "grid", + "groovy", + "group", + "grunt", + "guard", + "gui-", + "gui.", + "gui_", + "guide", + "guideline", + "gulp", + "gwt-", + "gwt.", + "gwt_", + "hack", + "hackathon", + "hacker", + "hacking", + "hadoop", + "haml", + "handler", + "hardware", + "has-", + "has_", + "hash", + "haskell", + "have", + "haxe", + "hello", + "help", + "helper", + "here", + "hero", + "heroku", + "high", + "hipchat", + "history", + "home", + "homebrew", + "homepage", + "hook", + "host", + "hosting", + "hot-", + "hot.", + "hot_", + "house", + "how-", + "how.", + "how_", + "html", + "http", + "hub-", + "hub.", + "hub_", + "hubot", + "human", + "icon", + "ide-", + "ide.", + "ide_", + "idea", + "identity", + "idiomatic", + "image", + "impact", + "import", + "important", + "importer", + "impres", + "index", + "infinite", + "info", + "injection", + "inline", + "input", + "inside", + "inspector", + "instagram", + "install", + "installer", + "instant", + "intellij", + "interface", + "internet", + "interview", + "into", + "intro", + "ionic", + "iphone", + "ipython", + "irc-", + "irc_", + "iso-", + "iso.", + "iso_", + "issue", + "jade", + "jasmine", + "java", + "jbos", + "jekyll", + "jenkin", + "jetbrains", + "job-", + "job.", + "job_", + "joomla", + "jpa-", + "jpa.", + "jpa_", + "jquery", + "json", + "just", + "kafka", + "karma", + "kata", + "kernel", + "keyboard", + "kindle", + "kit-", + "kit.", + "kit_", + "kitchen", + "knife", + "koan", + "kohana", + "lab-", + "lab-", + "lab.", + "lab.", + "lab_", + "lab_", + "lambda", + "lamp", + "language", + "laravel", + "last", + "latest", + "latex", + "launcher", + "layer", + "layout", + "lazy", + "ldap", + "leaflet", + "league", + "learn", + "learning", + "led-", + "led.", + "led_", + "leetcode", + "les-", + "les.", + "les_", + "level", + "leveldb", + "lib-", + "lib.", + "lib_", + "librarie", + "library", + "license", + "life", + "liferay", + "light", + "lightbox", + "like", + "line", + "link", + "linked", + "linkedin", + "linux", + "lisp", + "list", + "lite", + "little", + "load", + "loader", + "local", + "location", + "lock", + "log-", + "log.", + "log_", + "logger", + "logging", + "logic", + "login", + "logstash", + "longer", + "look", + "love", + "lua-", + "lua.", + "lua_", + "mac-", + "mac.", + "mac_", + "machine", + "made", + "magento", + "magic", + "mail", + "make", + "maker", + "making", + "man-", + "man.", + "man_", + "manage", + "manager", + "manifest", + "manual", + "map-", + "map-", + "map.", + "map.", + "map_", + "map_", + "mapper", + "mapping", + "markdown", + "markup", + "master", + "math", + "matrix", + "maven", + "md5", + "mean", + "media", + "mediawiki", + "meetup", + "memcached", + "memory", + "menu", + "merchant", + "message", + "messaging", + "meta", + "metadata", + "meteor", + "method", + "metric", + "micro", + "middleman", + "migration", + "minecraft", + "miner", + "mini", + "minimal", + "mirror", + "mit-", + "mit.", + "mit_", + "mobile", + "mocha", + "mock", + "mod-", + "mod.", + "mod_", + "mode", + "model", + "modern", + "modular", + "module", + "modx", + "money", + "mongo", + "mongodb", + "mongoid", + "mongoose", + "monitor", + "monkey", + "more", + "motion", + "moved", + "movie", + "mozilla", + "mqtt", + "mule", + "multi", + "multiple", + "music", + "mustache", + "mvc-", + "mvc.", + "mvc_", + "mysql", + "nagio", + "name", + "native", + "need", + "neo-", + "neo.", + "neo_", + "nest", + "nested", + "net-", + "net.", + "net_", + "nette", + "network", + "new-", + "new-", + "new.", + "new.", + "new_", + "new_", + "next", + "nginx", + "ninja", + "nlp-", + "nlp.", + "nlp_", + "node", + "nodej", + "nosql", + "not-", + "not.", + "not_", + "note", + "notebook", + "notepad", + "notice", + "notifier", + "now-", + "now.", + "now_", + "number", + "oauth", + "object", + "objective", + "obsolete", + "ocaml", + "octopres", + "official", + "old-", + "old.", + "old_", + "onboard", + "online", + "only", + "open", + "opencv", + "opengl", + "openshift", + "openwrt", + "option", + "oracle", + "org-", + "org.", + "org_", + "origin", + "original", + "orm-", + "orm.", + "orm_", + "osx-", + "osx_", + "our-", + "our.", + "our_", + "out-", + "out.", + "out_", + "output", + "over", + "overview", + "own-", + "own.", + "own_", + "pack", + "package", + "packet", + "page", + "page", + "panel", + "paper", + "paperclip", + "para", + "parallax", + "parallel", + "parse", + "parser", + "parsing", + "particle", + "party", + "password", + "patch", + "path", + "pattern", + "payment", + "paypal", + "pdf-", + "pdf.", + "pdf_", + "pebble", + "people", + "perl", + "personal", + "phalcon", + "phoenix", + "phone", + "phonegap", + "photo", + "php-", + "php.", + "php_", + "physic", + "picker", + "pipeline", + "platform", + "play", + "player", + "please", + "plu-", + "plu.", + "plu_", + "plug-in", + "plugin", + "plupload", + "png-", + "png.", + "png_", + "poker", + "polyfill", + "polymer", + "pool", + "pop-", + "pop.", + "pop_", + "popcorn", + "popup", + "port", + "portable", + "portal", + "portfolio", + "post", + "power", + "powered", + "powerful", + "prelude", + "pretty", + "preview", + "principle", + "print", + "pro-", + "pro.", + "pro_", + "problem", + "proc", + "product", + "profile", + "profiler", + "program", + "progres", + "project", + "protocol", + "prototype", + "provider", + "proxy", + "public", + "pull", + "puppet", + "pure", + "purpose", + "push", + "pusher", + "pyramid", + "python", + "quality", + "query", + "queue", + "quick", + "rabbitmq", + "rack", + "radio", + "rail", + "railscast", + "random", + "range", + "raspberry", + "rdf-", + "rdf.", + "rdf_", + "react", + "reactive", + "read", + "reader", + "readme", + "ready", + "real", + "reality", + "real-time", + "realtime", + "recipe", + "recorder", + "red-", + "red.", + "red_", + "reddit", + "redi", + "redmine", + "reference", + "refinery", + "refresh", + "registry", + "related", + "release", + "remote", + "rendering", + "repo", + "report", + "request", + "require", + "required", + "requirej", + "research", + "resource", + "response", + "resque", + "rest", + "restful", + "resume", + "reveal", + "reverse", + "review", + "riak", + "rich", + "right", + "ring", + "robot", + "role", + "room", + "router", + "routing", + "rpc-", + "rpc.", + "rpc_", + "rpg-", + "rpg.", + "rpg_", + "rspec", + "ruby-", + "ruby.", + "ruby_", + "rule", + "run-", + "run.", + "run_", + "runner", + "running", + "runtime", + "rust", + "rvm-", + "rvm.", + "rvm_", + "salt", + "sample", + "sample", + "sandbox", + "sas-", + "sas.", + "sas_", + "sbt-", + "sbt.", + "sbt_", + "scala", + "scalable", + "scanner", + "schema", + "scheme", + "school", + "science", + "scraper", + "scratch", + "screen", + "script", + "scroll", + "scs-", + "scs.", + "scs_", + "sdk-", + "sdk.", + "sdk_", + "sdl-", + "sdl.", + "sdl_", + "search", + "secure", + "security", + "see-", + "see.", + "see_", + "seed", + "select", + "selector", + "selenium", + "semantic", + "sencha", + "send", + "sentiment", + "serie", + "server", + "service", + "session", + "set-", + "set.", + "set_", + "setting", + "setting", + "setup", + "sha1", + "sha2", + "sha256", + "share", + "shared", + "sharing", + "sheet", + "shell", + "shield", + "shipping", + "shop", + "shopify", + "shortener", + "should", + "show", + "showcase", + "side", + "silex", + "simple", + "simulator", + "single", + "site", + "skeleton", + "sketch", + "skin", + "slack", + "slide", + "slider", + "slim", + "small", + "smart", + "smtp", + "snake", + "snapshot", + "snippet", + "soap", + "social", + "socket", + "software", + "solarized", + "solr", + "solution", + "solver", + "some", + "soon", + "source", + "space", + "spark", + "spatial", + "spec", + "sphinx", + "spine", + "spotify", + "spree", + "spring", + "sprite", + "sql-", + "sql.", + "sql_", + "sqlite", + "ssh-", + "ssh.", + "ssh_", + "stack", + "staging", + "standard", + "stanford", + "start", + "started", + "starter", + "startup", + "stat", + "statamic", + "state", + "static", + "statistic", + "statsd", + "statu", + "steam", + "step", + "still", + "stm-", + "stm.", + "stm_", + "storage", + "store", + "storm", + "story", + "strategy", + "stream", + "streaming", + "string", + "stripe", + "structure", + "studio", + "study", + "stuff", + "style", + "sublime", + "sugar", + "suite", + "summary", + "super", + "support", + "supported", + "svg-", + "svg.", + "svg_", + "svn-", + "svn.", + "svn_", + "swagger", + "swift", + "switch", + "switcher", + "symfony", + "symphony", + "sync", + "synopsi", + "syntax", + "system", + "system", + "tab-", + "tab-", + "tab.", + "tab.", + "tab_", + "tab_", + "table", + "tag-", + "tag-", + "tag.", + "tag.", + "tag_", + "tag_", + "talk", + "target", + "task", + "tcp-", + "tcp.", + "tcp_", + "tdd-", + "tdd.", + "tdd_", + "team", + "tech", + "template", + "term", + "terminal", + "testing", + "tetri", + "text", + "textmate", + "theme", + "theory", + "three", + "thrift", + "time", + "timeline", + "timer", + "tiny", + "tinymce", + "tip-", + "tip.", + "tip_", + "title", + "todo", + "todomvc", + "token", + "tool", + "toolbox", + "toolkit", + "top-", + "top.", + "top_", + "tornado", + "touch", + "tower", + "tracker", + "tracking", + "traffic", + "training", + "transfer", + "translate", + "transport", + "tree", + "trello", + "try-", + "try.", + "try_", + "tumblr", + "tut-", + "tut.", + "tut_", + "tutorial", + "tweet", + "twig", + "twitter", + "type", + "typo", + "ubuntu", + "uiview", + "ultimate", + "under", + "unit", + "unity", + "universal", + "unix", + "update", + "updated", + "upgrade", + "upload", + "uploader", + "uri-", + "uri.", + "uri_", + "url-", + "url.", + "url_", + "usage", + "usb-", + "usb.", + "usb_", + "use-", + "use.", + "use_", + "used", + "useful", + "user", + "using", + "util", + "utilitie", + "utility", + "vagrant", + "validator", + "value", + "variou", + "varnish", + "version", + "via-", + "via.", + "via_", + "video", + "view", + "viewer", + "vim-", + "vim.", + "vim_", + "vimrc", + "virtual", + "vision", + "visual", + "vpn", + "want", + "warning", + "watch", + "watcher", + "wave", + "way-", + "way.", + "way_", + "weather", + "web-", + "web_", + "webapp", + "webgl", + "webhook", + "webkit", + "webrtc", + "website", + "websocket", + "welcome", + "welcome", + "what", + "what'", + "when", + "where", + "which", + "why-", + "why.", + "why_", + "widget", + "wifi", + "wiki", + "win-", + "win.", + "win_", + "window", + "wip-", + "wip.", + "wip_", + "within", + "without", + "wizard", + "word", + "wordpres", + "work", + "worker", + "workflow", + "working", + "workshop", + "world", + "wrapper", + "write", + "writer", + "writing", + "written", + "www-", + "www.", + "www_", + "xamarin", + "xcode", + "xml-", + "xml.", + "xml_", + "xmpp", + "xxxxxx", + "yahoo", + "yaml", + "yandex", + "yeoman", + "yet-", + "yet.", + "yet_", + "yii-", + "yii.", + "yii_", + "youtube", + "yui-", + "yui.", + "yui_", + "zend", + "zero", + "zip-", + "zip.", + "zip_", + "zsh-", + "zsh.", + "zsh_", +} diff --git a/cmd/generate/config/rules/stripe.go b/cmd/generate/config/rules/stripe.go new file mode 100644 index 0000000..a42936b --- /dev/null +++ b/cmd/generate/config/rules/stripe.go @@ -0,0 +1,35 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func StripeAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "stripe-access-token", + Description: "Found a Stripe Access Token, posing a risk to payment processing services and sensitive financial data.", + Regex: utils.GenerateUniqueTokenRegex(`(?:sk|rk)_(?:test|live|prod)_[a-zA-Z0-9]{10,99}`, false), + Entropy: 2, + Keywords: []string{ + "sk_test", + "sk_live", + "sk_prod", + "rk_test", + "rk_live", + "rk_prod", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("stripe", "sk_test_"+secrets.NewSecret(utils.AlphaNumeric("30"))) + tps = append(tps, utils.GenerateSampleSecrets("stripe", "sk_prod_"+secrets.NewSecret(utils.AlphaNumeric("99")))...) + tps = append(tps, + "sk_test_51OuEMLAlTWGaDypq4P5cuDHbuKeG4tAGPYHJpEXQ7zE8mKK3jkhTFPvCxnSSK5zB5EQZrJsYdsatNmAHGgb0vSKD00GTMSWRHs", // gitleaks:allow + "rk_prod_51OuEMLAlTWGaDypquDn9aZigaJOsa9NR1w1BxZXs9JlYsVVkv5XDu6aLmAxwt5Tgun5WcSwQMKzQyqV16c9iD4sx00BRijuoon", // gitleaks:allow + ) + fps := []string{"nonMatchingToken := \"task_test_" + secrets.NewSecret(utils.AlphaNumeric("30")) + "\""} + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/sumologic.go b/cmd/generate/config/rules/sumologic.go new file mode 100644 index 0000000..bb32acc --- /dev/null +++ b/cmd/generate/config/rules/sumologic.go @@ -0,0 +1,75 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func SumoLogicAccessID() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "sumologic-access-id", + Description: "Discovered a SumoLogic Access ID, potentially compromising log management services and data analytics integrity.", + // TODO: Make 'su' case-sensitive. + Regex: utils.GenerateSemiGenericRegex([]string{"(?-i:[Ss]umo|SUMO)"}, "su[a-zA-Z0-9]{12}", false), + Entropy: 3, + Keywords: []string{ + "sumo", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("sumo", secrets.NewSecret(`su[a-zA-Z0-9]{12}`)) + tps = append(tps, + `sumologic.accessId = "su9OL59biWiJu7"`, // gitleaks:allow + `sumologic_access_id = "sug5XpdpaoxtOH"`, // gitleaks:allow + `export SUMOLOGIC_ACCESSID="suDbJw97o9WVo0"`, // gitleaks:allow + `SUMO_ACCESS_ID = "suGyI5imvADdvU"`, // gitleaks:allow + ) + fps := []string{ + `- (NSNumber *)sumOfProperty:(NSString *)property;`, + `- (NSInteger)sumOfValuesInRange:(NSRange)range;`, + `+ (unsigned char)byteChecksumOfData:(id)arg1;`, + `sumOfExposures = sumOfExposures;`, // gitleaks:allow + `.si-sumologic.si--color::before { color: #000099; }`, + `/// Based on the SumoLogic keyword syntax:`, + `sumologic_access_id = ""`, + `SUMOLOGIC_ACCESSID: ${SUMOLOGIC_ACCESSID}`, + `export SUMOLOGIC_ACCESSID=XXXXXXXXXXXXXX`, // gitleaks:allow + `sumObj = suGyI5imvADdvU`, + } + return utils.Validate(r, tps, fps) +} + +func SumoLogicAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "sumologic-access-token", + Description: "Uncovered a SumoLogic Access Token, which could lead to unauthorized access to log data and analytics insights.", + Regex: utils.GenerateSemiGenericRegex([]string{"(?-i:[Ss]umo|SUMO)"}, utils.AlphaNumeric("64"), true), + Entropy: 3, + Keywords: []string{ + "sumo", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("sumo", secrets.NewSecret(utils.AlphaNumeric("64"))) + tps = append(tps, + `export SUMOLOGIC_ACCESSKEY="3HSa1hQfz6BYzlxf7Yb1WKG3Hyovm56LMFChV2y9LgkRipsXCujcLb5ej3oQUJlx"`, // gitleaks:allow + `SUMO_ACCESS_KEY: gxq3rJQkS6qovOg9UY2Q70iH1jFZx0WBrrsiAYv4XHodogAwTKyLzvFK4neRN8Dk`, // gitleaks:allow + `SUMOLOGIC_ACCESSKEY: 9RITWb3I3kAnSyUolcVJq4gwM17JRnQK8ugRaixFfxkdSl8ys17ZtEL3LotESKB7`, // gitleaks:allow + `sumo_access_key = "3Kof2VffNQ0QgYIhXUPJosVlCaQKm2hfpWE6F1fT9YGY74blQBIPsrkCcf1TwKE5"`, // gitleaks:allow + ) + fps := []string{ + `# SUMO_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`, // gitleaks:allow + "-e SUMO_ACCESS_KEY=`etcdctl get /sumologic_secret`", + `SUMO_ACCESS_KEY={SumoAccessKey}`, + `SUMO_ACCESS_KEY=${SUMO_ACCESS_KEY:=$2}`, + `sumo_access_key = ""`, + `SUMO_ACCESS_KEY: AbCeFG123`, + `sumOfExposures = 3Kof2VffNQ0QgYIhXUPJosVlCaQKm2hfpWE6F1fT9YGY74blQBIPsrkCcf1TwKE5;`, + } + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/teams.go b/cmd/generate/config/rules/teams.go new file mode 100644 index 0000000..871774a --- /dev/null +++ b/cmd/generate/config/rules/teams.go @@ -0,0 +1,29 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func TeamsWebhook() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "microsoft-teams-webhook", + Description: "Uncovered a Microsoft Teams Webhook, which could lead to unauthorized access to team collaboration tools and data leaks.", + Regex: regexp.MustCompile( + `https://[a-z0-9]+\.webhook\.office\.com/webhookb2/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}@[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}/IncomingWebhook/[a-z0-9]{32}/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}`), + Keywords: []string{ + "webhook.office.com", + "webhookb2", + "IncomingWebhook", + }, + } + + // validate + tps := []string{ + "https://mycompany.webhook.office.com/webhookb2/" + secrets.NewSecret(`[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}@[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}\/IncomingWebhook\/[a-z0-9]{32}\/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}`), // gitleaks:allow + } + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/telegram.go b/cmd/generate/config/rules/telegram.go new file mode 100644 index 0000000..3b89baf --- /dev/null +++ b/cmd/generate/config/rules/telegram.go @@ -0,0 +1,79 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func TelegramBotToken() *config.Rule { + // define rule + r := config.Rule{ + Description: "Detected a Telegram Bot API Token, risking unauthorized bot operations and message interception on Telegram.", + RuleID: "telegram-bot-api-token", + + Regex: utils.GenerateSemiGenericRegex([]string{"telegr"}, "[0-9]{5,16}:(?-i:A)[a-z0-9_\\-]{34}", true), + Keywords: []string{ + "telegr", + }, + } + + // validate + var ( + validToken = secrets.NewSecret(utils.Numeric("8") + ":A" + utils.AlphaNumericExtendedShort("34")) + minToken = secrets.NewSecret(utils.Numeric("5") + ":A" + utils.AlphaNumericExtendedShort("34")) + maxToken = secrets.NewSecret(utils.Numeric("16") + ":A" + utils.AlphaNumericExtendedShort("34")) + // xsdWithToken = secrets.NewSecret(``) + ) + // variable assignment + tps := utils.GenerateSampleSecrets("telegram", validToken) + // Token with min bot_id + tps = append(tps, utils.GenerateSampleSecrets("telegram", minToken)...) + // Token with max bot_id + tps = append(tps, utils.GenerateSampleSecrets("telegram", maxToken)...) + tps = append(tps, + // URL containing token TODO add another url based rule + // GenerateSampleSecret("url", "https://api.telegram.org/bot"+validToken+"/sendMessage"), + // object constructor + //TODO: `const bot = new Telegraf("`+validToken+`")`, + // .env + `TELEGRAM_API_TOKEN = `+validToken, + // YAML + `telegram bot: `+validToken, + // Valid token in XSD document TODO separate rule for this + // generateSampleSecret("telegram", xsdWithToken), + ) + + var ( + tooSmallToken = secrets.NewSecret(utils.Numeric("4") + ":A" + utils.AlphaNumericExtendedShort("34")) + tooBigToken = secrets.NewSecret(utils.Numeric("17") + ":A" + utils.AlphaNumericExtendedShort("34")) + xsdAgencyIdentificationCode1 = secrets.NewSecret(`` + xsdAgencyIdentificationCode2 = secrets.NewSecret(`token:"clm` + utils.Numeric("5") + `:AgencyIdentificationCodeContentType"`) + xsdAgencyIdentificationCode3 = secrets.NewSecret(``) + prefixedToken1 = secrets.NewSecret(`telegram_api_token = \"` + utils.Numeric("8") + `:Ahello` + utils.AlphaNumericExtendedShort("34") + `\"`) + prefixedToken2 = secrets.NewSecret(`telegram_api_token = \"` + utils.Numeric("8") + `:A-some-other-thing-` + utils.AlphaNumericExtendedShort("34") + `\"`) + prefixedToken3 = secrets.NewSecret(`telegram_api_token = \"` + utils.Numeric("8") + `:A_` + utils.AlphaNumericExtendedShort("34") + `\"`) + suffixedToken1 = secrets.NewSecret(`telegram_api_token = \"` + utils.Numeric("8") + `:A` + utils.AlphaNumericExtendedShort("34") + `hello\"`) + suffixedToken2 = secrets.NewSecret(`telegram_api_token = \"` + utils.Numeric("8") + `:A` + utils.AlphaNumericExtendedShort("34") + `-some-other-thing\"`) + suffixedToken3 = secrets.NewSecret(`telegram_api_token = \"` + utils.Numeric("8") + `:A_` + utils.AlphaNumericExtendedShort("34") + `_\"`) + ) + fps := []string{ + // Token with too small bot_id + utils.GenerateSampleSecret("telegram", tooSmallToken), + // Token with too big bot_id + utils.GenerateSampleSecret("telegram", tooBigToken), + // XSD file containing the string AgencyIdentificationCodeContentType + utils.GenerateSampleSecret("telegram", xsdAgencyIdentificationCode1), + utils.GenerateSampleSecret("telegram", xsdAgencyIdentificationCode2), + utils.GenerateSampleSecret("telegram", xsdAgencyIdentificationCode3), + // Prefix and suffix variations that shouldn't match + utils.GenerateSampleSecret("telegram", prefixedToken1), + utils.GenerateSampleSecret("telegram", prefixedToken2), + utils.GenerateSampleSecret("telegram", prefixedToken3), + utils.GenerateSampleSecret("telegram", suffixedToken1), + utils.GenerateSampleSecret("telegram", suffixedToken2), + utils.GenerateSampleSecret("telegram", suffixedToken3), + } + + return utils.Validate(r, tps, fps) +} diff --git a/cmd/generate/config/rules/travisci.go b/cmd/generate/config/rules/travisci.go new file mode 100644 index 0000000..8bbe935 --- /dev/null +++ b/cmd/generate/config/rules/travisci.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func TravisCIAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "travisci-access-token", + Description: "Identified a Travis CI Access Token, potentially compromising continuous integration services and codebase security.", + Regex: utils.GenerateSemiGenericRegex([]string{"travis"}, utils.AlphaNumeric("22"), true), + + Keywords: []string{ + "travis", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("travis", secrets.NewSecret(utils.AlphaNumeric("22"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/trello.go b/cmd/generate/config/rules/trello.go new file mode 100644 index 0000000..83fc97a --- /dev/null +++ b/cmd/generate/config/rules/trello.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func TrelloAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "trello-access-token", + Description: "Trello Access Token", + Regex: utils.GenerateSemiGenericRegex([]string{"trello"}, `[a-zA-Z-0-9]{32}`, true), + + Keywords: []string{ + "trello", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("trello", secrets.NewSecret(`[a-zA-Z-0-9]{32}`)) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/twilio.go b/cmd/generate/config/rules/twilio.go new file mode 100644 index 0000000..dc63c93 --- /dev/null +++ b/cmd/generate/config/rules/twilio.go @@ -0,0 +1,23 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/regexp" +) + +func Twilio() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "twilio-api-key", + Description: "Found a Twilio API Key, posing a risk to communication services and sensitive customer interaction data.", + Regex: regexp.MustCompile(`SK[0-9a-fA-F]{32}`), + Entropy: 3, + Keywords: []string{"SK"}, + } + + // validate + tps := utils.GenerateSampleSecrets("twilio", "SK"+secrets.NewSecret(utils.Hex("32"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/twitch.go b/cmd/generate/config/rules/twitch.go new file mode 100644 index 0000000..4747396 --- /dev/null +++ b/cmd/generate/config/rules/twitch.go @@ -0,0 +1,23 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func TwitchAPIToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "twitch-api-token", + Description: "Discovered a Twitch API token, which could compromise streaming services and account integrations.", + Regex: utils.GenerateSemiGenericRegex([]string{"twitch"}, utils.AlphaNumeric("30"), true), + Keywords: []string{ + "twitch", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("twitch", secrets.NewSecret(utils.AlphaNumeric("30"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/twitter.go b/cmd/generate/config/rules/twitter.go new file mode 100644 index 0000000..c8261b2 --- /dev/null +++ b/cmd/generate/config/rules/twitter.go @@ -0,0 +1,78 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func TwitterAPIKey() *config.Rule { + // define rule + r := config.Rule{ + Description: "Identified a Twitter API Key, which may compromise Twitter application integrations and user data security.", + RuleID: "twitter-api-key", + Regex: utils.GenerateSemiGenericRegex([]string{"twitter"}, utils.AlphaNumeric("25"), true), + Keywords: []string{"twitter"}, + } + + // validate + tps := utils.GenerateSampleSecrets("twitter", secrets.NewSecret(utils.AlphaNumeric("25"))) + return utils.Validate(r, tps, nil) +} + +func TwitterAPISecret() *config.Rule { + // define rule + r := config.Rule{ + Description: "Found a Twitter API Secret, risking the security of Twitter app integrations and sensitive data access.", + RuleID: "twitter-api-secret", + Regex: utils.GenerateSemiGenericRegex([]string{"twitter"}, utils.AlphaNumeric("50"), true), + Keywords: []string{"twitter"}, + } + + // validate + tps := utils.GenerateSampleSecrets("twitter", secrets.NewSecret(utils.AlphaNumeric("50"))) + return utils.Validate(r, tps, nil) +} + +func TwitterBearerToken() *config.Rule { + // define rule + r := config.Rule{ + Description: "Discovered a Twitter Bearer Token, potentially compromising API access and data retrieval from Twitter.", + RuleID: "twitter-bearer-token", + Regex: utils.GenerateSemiGenericRegex([]string{"twitter"}, "A{22}[a-zA-Z0-9%]{80,100}", true), + + Keywords: []string{"twitter"}, + } + + // validate + tps := utils.GenerateSampleSecrets("twitter", secrets.NewSecret("A{22}[a-zA-Z0-9%]{80,100}")) + return utils.Validate(r, tps, nil) +} + +func TwitterAccessToken() *config.Rule { + // define rule + r := config.Rule{ + Description: "Detected a Twitter Access Token, posing a risk of unauthorized account operations and social media data exposure.", + RuleID: "twitter-access-token", + Regex: utils.GenerateSemiGenericRegex([]string{"twitter"}, "[0-9]{15,25}-[a-zA-Z0-9]{20,40}", true), + Keywords: []string{"twitter"}, + } + + // validate + tps := utils.GenerateSampleSecrets("twitter", secrets.NewSecret("[0-9]{15,25}-[a-zA-Z0-9]{20,40}")) + return utils.Validate(r, tps, nil) +} + +func TwitterAccessSecret() *config.Rule { + // define rule + r := config.Rule{ + Description: "Uncovered a Twitter Access Secret, potentially risking unauthorized Twitter integrations and data breaches.", + RuleID: "twitter-access-secret", + Regex: utils.GenerateSemiGenericRegex([]string{"twitter"}, utils.AlphaNumeric("45"), true), + Keywords: []string{"twitter"}, + } + + // validate + tps := utils.GenerateSampleSecrets("twitter", secrets.NewSecret(utils.AlphaNumeric("45"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/typeform.go b/cmd/generate/config/rules/typeform.go new file mode 100644 index 0000000..032b997 --- /dev/null +++ b/cmd/generate/config/rules/typeform.go @@ -0,0 +1,24 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func Typeform() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "typeform-api-token", + Description: "Uncovered a Typeform API token, which could lead to unauthorized survey management and data collection.", + Regex: utils.GenerateSemiGenericRegex([]string{"typeform"}, + `tfp_[a-z0-9\-_\.=]{59}`, true), + Keywords: []string{ + "tfp_", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("typeformAPIToken", "tfp_"+secrets.NewSecret(utils.AlphaNumericExtended("59"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/yandex.go b/cmd/generate/config/rules/yandex.go new file mode 100644 index 0000000..a6800b4 --- /dev/null +++ b/cmd/generate/config/rules/yandex.go @@ -0,0 +1,60 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func YandexAWSAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "yandex-aws-access-token", + Description: "Uncovered a Yandex AWS Access Token, potentially compromising cloud resource access and data security on Yandex Cloud.", + Regex: utils.GenerateSemiGenericRegex([]string{"yandex"}, + `YC[a-zA-Z0-9_\-]{38}`, true), + Keywords: []string{ + "yandex", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("yandex", secrets.NewSecret(`YC[a-zA-Z0-9_\-]{38}`)) + return utils.Validate(r, tps, nil) +} + +func YandexAPIKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "yandex-api-key", + Description: "Discovered a Yandex API Key, which could lead to unauthorized access to Yandex services and data manipulation.", + Regex: utils.GenerateSemiGenericRegex([]string{"yandex"}, + `AQVN[A-Za-z0-9_\-]{35,38}`, true), + + Keywords: []string{ + "yandex", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("yandex", secrets.NewSecret(`AQVN[A-Za-z0-9_\-]{35,38}`)) + return utils.Validate(r, tps, nil) +} + +func YandexAccessToken() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "yandex-access-token", + Description: "Found a Yandex Access Token, posing a risk to Yandex service integrations and user data privacy.", + Regex: utils.GenerateSemiGenericRegex([]string{"yandex"}, + `t1\.[A-Z0-9a-z_-]+[=]{0,2}\.[A-Z0-9a-z_-]{86}[=]{0,2}`, true), + + Keywords: []string{ + "yandex", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("yandex", secrets.NewSecret(`t1\.[A-Z0-9a-z_-]+[=]{0,2}\.[A-Z0-9a-z_-]{86}[=]{0,2}`)) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/rules/zendesk.go b/cmd/generate/config/rules/zendesk.go new file mode 100644 index 0000000..c1fd6d3 --- /dev/null +++ b/cmd/generate/config/rules/zendesk.go @@ -0,0 +1,23 @@ +package rules + +import ( + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/utils" + "github.com/zricethezav/gitleaks/v8/cmd/generate/secrets" + "github.com/zricethezav/gitleaks/v8/config" +) + +func ZendeskSecretKey() *config.Rule { + // define rule + r := config.Rule{ + RuleID: "zendesk-secret-key", + Description: "Detected a Zendesk Secret Key, risking unauthorized access to customer support services and sensitive ticketing data.", + Regex: utils.GenerateSemiGenericRegex([]string{"zendesk"}, utils.AlphaNumeric("40"), true), + Keywords: []string{ + "zendesk", + }, + } + + // validate + tps := utils.GenerateSampleSecrets("zendesk", secrets.NewSecret(utils.AlphaNumeric("40"))) + return utils.Validate(r, tps, nil) +} diff --git a/cmd/generate/config/utils/generate.go b/cmd/generate/config/utils/generate.go new file mode 100644 index 0000000..f4f3ced --- /dev/null +++ b/cmd/generate/config/utils/generate.go @@ -0,0 +1,162 @@ +// == WARNING == +// These functions are used to generate GitLeak's default config. +// You are free to use these in your own project, HOWEVER, no API stability is guaranteed. + +package utils + +import ( + "fmt" + "strings" + + "github.com/zricethezav/gitleaks/v8/regexp" +) + +const ( + // case insensitive prefix + caseInsensitive = `(?i)` + + // identifier prefix (just an ignore group) + identifierCaseInsensitivePrefix = `[\w.-]{0,50}?(?i:` + identifierCaseInsensitiveSuffix = `)` + identifierPrefix = `[\w.-]{0,50}?(?:` + identifierSuffix = `)(?:[ \t\w.-]{0,20})[\s'"]{0,3}` + + // commonly used assignment operators or function call + //language=regexp + operator = `(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)` + + // boundaries for the secret + secretPrefixUnique = `\b(` + secretPrefix = `[\x60'"\s=]{0,5}(` + secretSuffix = `)(?:[\x60'"\s;]|\\[nr]|$)` +) + +func GenerateSemiGenericRegex(identifiers []string, secretRegex string, isCaseInsensitive bool) *regexp.Regexp { + var sb strings.Builder + // The identifiers should always be case-insensitive. + // This is inelegant but prevents an extraneous `(?i:)` from being added to the pattern; it could be removed. + if isCaseInsensitive { + sb.WriteString(caseInsensitive) + writeIdentifiers(&sb, identifiers) + } else { + sb.WriteString(identifierCaseInsensitivePrefix) + writeIdentifiers(&sb, identifiers) + sb.WriteString(identifierCaseInsensitiveSuffix) + } + sb.WriteString(operator) + sb.WriteString(secretPrefix) + sb.WriteString(secretRegex) + sb.WriteString(secretSuffix) + return regexp.MustCompile(sb.String()) +} + +func MergeRegexps(regexps ...*regexp.Regexp) *regexp.Regexp { + patterns := make([]string, len(regexps)) + + for i, r := range regexps { + patterns[i] = r.String() + } + + return regexp.MustCompile(strings.Join(patterns, "|")) +} + +func writeIdentifiers(sb *strings.Builder, identifiers []string) { + sb.WriteString(identifierPrefix) + sb.WriteString(strings.Join(identifiers, "|")) + sb.WriteString(identifierSuffix) +} + +func GenerateUniqueTokenRegex(secretRegex string, isCaseInsensitive bool) *regexp.Regexp { + var sb strings.Builder + if isCaseInsensitive { + sb.WriteString(caseInsensitive) + } + sb.WriteString(secretPrefixUnique) + sb.WriteString(secretRegex) + sb.WriteString(secretSuffix) + return regexp.MustCompile(sb.String()) +} + +func GenerateSampleSecret(identifier string, secret string) string { + return fmt.Sprintf("%s_api_token = \"%s\"", identifier, secret) +} + +// See: https://github.com/gitleaks/gitleaks/issues/1222 +func GenerateSampleSecrets(identifier string, secret string) []string { + samples := map[string]string{ + // Configuration + // INI + "ini - quoted1": "{i}Token=\"{s}\"", + "ini - quoted2": "{i}Token = \"{s}\"", + "ini - unquoted1": "{i}Token={s}", + "ini - unquoted2": "{i}Token = {s}", + // JSON + "json - string": "{\n \"{i}_token\": \"{s}\"\n}", + // TODO: "json - escaped string": "\\{\n \\\"{i}_token\\\": \\\"{s}\\\"\n\\}", + // TODO: "json - string key/value": "{\n \"name\": \"{i}_token\",\n \"value\": \"{s}\"\n}", + "json - escaped newline in string": `{"config.ini": "{I}_TOKEN={s}\nBACKUP_ENABLED=true"}`, + // XML + // TODO: "xml - element": "<{i}Token>{s}", + "xml - element multiline": "<{i}Token>\n {s}\n", + // TODO: "xml - attribute": "", + // TODO: "xml - key/value elements": "\n \n \n", + // YAML + "yaml - singleline - unquoted": "{i}_token: {s}", + "yaml - singleline - single quote": "{i}_token: '{s}'", + "yaml - singleline - double quote": "{i}_token: \"{s}\"", + // TODO: "yaml - multiline - literal": "{i}_token: |\n {s}", + // TODO: "yaml - multiline - folding": "{i}_token: >\n {s}", + // "": "", + + // Programming Languages + "C#": `string {i}Token = "{s}";`, + "go - normal": `var {i}Token string = "{s}"`, + "go - short": `{i}Token := "{s}"`, + "go - backticks": "{i}Token := `{s}`", + "java": "String {i}Token = \"{s}\";", + // TODO: "java - escaped quotes": `config.put("sasl.jaas.config", "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"JDOE35\" {i}Token=\"{s}\""`, + // TODO:"kotlin - type": "var {i}Token: string = \"{s}\"", + "kotlin - notype": "var {i}Token = \"{s}\"", + "php - string concat": `${i}Token .= "{s}"`, + // TODO: "php - null coalesce": `${i}Token ??= "{s}"`, + "python - single quote": "{i}Token = '{s}'", + "python - double quote": `{i}Token = "{s}"`, + // "": "", + + // Miscellaneous + // TODO: "url - basic auth": `https://{i}:{s}@example.com/`, + // TODO: "url - query parameter": "https://example.com?{i}Token={s}&fooBar=baz", + // TODO: "comment - slash": "//{s} is the password", + // TODO: "comment - slash multiline": "/*{s} is the password", + // TODO: "comment - hashtag": "#{s} is the password", + // TODO: "comment - semicolon": ";{s} is the password", + // TODO: "csv - unquoted": `{i}Token,{s},`, + "misc - comma operator": `System.setProperty("{I}_TOKEN", "{s}")`, + // TODO: "misc - comma suffix": `environmentVariables = {PATH=/usr/local/bin, ENV=/etc/bashrc, {I}_PASSWORD={s}, LC_CTYPE=en_CA.UTF-8},`, // Spotted in `./Library/Logs/gradle-kotlin-dsl-resolver-xxxx.log` + "logstash": " \"{i}Token\" => \"{s}\"", + // TODO: "sql - tabular": "|{s}|", + // TODO: "sql": "", + + // Makefile + // See: https://github.com/gitleaks/gitleaks/pull/1191 + "make - recursive assignment": "{i}_TOKEN = \"{s}\"", + "make - simple assignment": "{i}_TOKEN := \"{s}\"", + "make - shell assignment": "{i}_TOKEN ::= \"{s}\"", + "make - evaluated shell assignment": "{i}_TOKEN :::= \"{s}\"", + "make - conditional assignment": "{i}_TOKEN ?= \"{s}\"", + // TODO: "make - append": "{i}_TOKEN += \"{s}\"", + + // "": "", + } + + replacer := strings.NewReplacer( + "{i}", identifier, + "{I}", strings.ToUpper(identifier), + "{s}", secret, + ) + cases := make([]string, 0, len(samples)) + for _, v := range samples { + cases = append(cases, replacer.Replace(v)) + } + return cases +} diff --git a/cmd/generate/config/utils/generate_test.go b/cmd/generate/config/utils/generate_test.go new file mode 100644 index 0000000..6c9a31d --- /dev/null +++ b/cmd/generate/config/utils/generate_test.go @@ -0,0 +1,235 @@ +package utils + +import ( + "testing" +) + +func TestGenerateSemiGenericRegex(t *testing.T) { + tests := []struct { + name string + identifiers []string + secretRegex string + isCaseInsensitive []bool + validStrings []string + invalidStrings []string + }{ + { + name: "secret is case sensitive, if isCaseInsensitive is false", + identifiers: []string{"api_key"}, + secretRegex: `[a-z]{3}`, + isCaseInsensitive: []bool{false}, + validStrings: []string{"api_key=xxx"}, + invalidStrings: []string{"api_key=XXX", "api_key=xXx"}, + }, + { + name: "secret is case insensitive, if isCaseInsensitive is true", + identifiers: []string{"api_key"}, + secretRegex: `[a-z]{3}`, + isCaseInsensitive: []bool{true}, + validStrings: []string{"api_key=xxx", "api_key=XXX", "api_key=xXx"}, + invalidStrings: []string{"api_key=x!x"}, + }, + { + name: "identifier is case insensitive, regardless of isCaseInsensitive", + identifiers: []string{"api_key"}, + secretRegex: `[a-z]{3}`, + isCaseInsensitive: []bool{true, false}, + validStrings: []string{"api_key=xxx", "ApI_KeY=xxx", "aPi_kEy=xxx", "API_KEY=xxx"}, + invalidStrings: []string{"api!key=xxx"}, + }, + { + name: "identifier can be case sensitive", + identifiers: []string{"(?-i:[Aa]pi_?[Kk]ey|API_?KEY)"}, + secretRegex: `[a-z]{3}`, + isCaseInsensitive: []bool{true, false}, + validStrings: []string{"apikey=xxx", "ApiKey=xxx", "Apikey=xxx", "APIKEY=xxx", "api_key=xxx"}, + invalidStrings: []string{"ApIKeY=xxx", "aPikEy=xxx"}, + }, + { + name: "identifier can be part of a longer word", + identifiers: []string{"key"}, + secretRegex: `[a-z]{3}`, + isCaseInsensitive: []bool{true, false}, + validStrings: []string{"mykey=xxx", "keys=xxx", "key1=xxx", "keystore=xxx", "monkey=xxx"}, + invalidStrings: []string{}, + }, + { + name: "identifier may be followed by specific characters", + identifiers: []string{"api_key"}, + secretRegex: `[a-z]{3}`, + isCaseInsensitive: []bool{true, false}, + validStrings: []string{ + "api_key-----=xxx", + "api_key.....=xxx", + "api_key_____=xxx", + "'''api_key'''=xxx", + `"""api_key"""=xxx`, + "api_key =xxx", + "api_key\t\t\t\t\t=xxx", + "api_key\n\n\n=xxx", // potentially invalid?, + "api_key\r\n=xxx", + // "api_key|||=xxx", + }, + invalidStrings: []string{ + "api_key&=xxx", + "$api_key$=xxx", + "%api_key%=xxx", + "api_key[0]=xxx", + "api_key/*REMOVE*/=xxx", + }, + }, + { + name: "identifier and secret must be separated by specific operators", + identifiers: []string{"api_key"}, + secretRegex: `[a-z]{3}`, + isCaseInsensitive: []bool{true, false}, + validStrings: []string{ + "api_key=xxx", + "api_key: xxx", + "xxx", + "api_key:=xxx", + "api_key:::=xxx", + // "api_key||:=xxx", // this isn't anything + // "api_key <= xxx", + "api_key => xxx", + "api_key ?= xxx", + "api_key, xxx", + }, + invalidStrings: []string{ + "api_keyxxx", + "api_key\txxx", // potentially valid in a tab-separated file + "api_key; xxx", + "api_key", + "api_key&xxx", + "api_key = true ? 'xxx' : 'yyy'", + }, + }, + { + name: "secret is limited by specific boundaries", + identifiers: []string{"api_key"}, + secretRegex: `[a-z]{3}`, + isCaseInsensitive: []bool{true, false}, + validStrings: []string{ + "api_key= xxx ", + "api_key=xxx\n", + "api_key=xxx\r\n", + "api_key=\n\n\n\n\nxxx", // potentially invalid (e.g. .env.example) + "api_key=\r\n\r\nxxx", + "api_key=\t\t\t\txxx\t", + "api_key======xxx;", + "api_key='''xxx'''", + `api_key="""xxx"""`, + "api_key=```xxx```", + `api_key="xxx'`, // could try to match only same opening and closing quotes, might not be worth the complexity + `api_key="don't do it!"`, + `api_key="xxx;notpartofthematch"`, + }, + invalidStrings: []string{ + "api_key=_xxx", + "api_key=xxx_", + "api_key=$xxx", + "api_key=%xxx%", + "api_key=[xxx]", + "api_key=(xxx)", + "api_key=", + "api_key={xxx}", + "xxx", // potentially valid + "example.com?api_key=xxx&other=yyy", // potentially valid + }, + }, + // Note: these test cases do not necessarily prescribe the expected behavior of the function, + // but rather document the current behavior, to ensure that future changes are intentional. + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, isCaseInsensitive := range tt.isCaseInsensitive { + regex := GenerateSemiGenericRegex(tt.identifiers, tt.secretRegex, isCaseInsensitive) + for _, validString := range tt.validStrings { + if !regex.MatchString(validString) { + t.Errorf("Expected match, but got none, \nfor GenerateSemiGenericRegex(%v, /%v/, caseInsensitive=%v).MatchString(`%v`)\n%v", + tt.identifiers, tt.secretRegex, isCaseInsensitive, validString, regex) + } + } + for _, invalidString := range tt.invalidStrings { + if regex.MatchString(invalidString) { + t.Errorf("Expected no match, but got one, \nfor GenerateSemiGenericRegex(%v, /%v/, caseInsensitive=%v).MatchString(`%v`)\n%v", + tt.identifiers, tt.secretRegex, isCaseInsensitive, invalidString, regex) + } + } + } + }) + } +} + +func TestGenerateUniqueTokenRegex(t *testing.T) { + tests := []struct { + name string + secretRegex string + isCaseInsensitive bool + validStrings []string + invalidStrings []string + }{ + { + name: "case sensitive secret", + secretRegex: `[a-c]{3}`, + isCaseInsensitive: false, + validStrings: []string{"abc"}, + invalidStrings: []string{"ABC", "Abc"}, + }, + { + name: "case insensitive secret", + secretRegex: `[a-c]{3}`, + isCaseInsensitive: true, + validStrings: []string{"abc", "ABC", "Abc"}, + invalidStrings: []string{"123"}, + }, + { + name: "allowed boundaries", + secretRegex: `[a-c]{3}`, + isCaseInsensitive: false, + validStrings: []string{ + "abc", + " abc ", + "\nabc\n", + "\r\nabc\r\n", + "\tabc\t", + "'abc'", + `"abc"`, + "```abc```", + "my abc's", + ".com?abc", + }, + invalidStrings: []string{ + "abcabc", + "_abc_", + ".com?abc&def", // potentially valid + "/*abc*/", + "", + "abc", // potentially valid + "{{{abc}}}", + "abc, d", + }, + }, + // Note: these test cases do not necessarily prescribe the expected behavior of the function, + // but rather document the current behavior, to ensure that future changes are intentional. + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + regex := GenerateUniqueTokenRegex(tt.secretRegex, tt.isCaseInsensitive) + for _, validString := range tt.validStrings { + if !regex.MatchString(validString) { + t.Errorf("Expected match, but got none, \nfor GenerateUniqueTokenRegex(/%v/, caseInsensitive=%v).MatchString(`%v`)\n%v", + tt.secretRegex, tt.isCaseInsensitive, validString, regex) + } + } + for _, invalidString := range tt.invalidStrings { + if regex.MatchString(invalidString) { + t.Errorf("Expected no match, but got one, \nfor GenerateUniqueTokenRegex(/%v/, caseInsensitive=%v).MatchString(`%v`)\n%v", + tt.secretRegex, tt.isCaseInsensitive, invalidString, regex) + } + } + }) + } +} diff --git a/cmd/generate/config/utils/patterns.go b/cmd/generate/config/utils/patterns.go new file mode 100644 index 0000000..fd27a15 --- /dev/null +++ b/cmd/generate/config/utils/patterns.go @@ -0,0 +1,37 @@ +// == WARNING == +// These functions are used to generate GitLeak's default config. +// You are free to use these in your own project, HOWEVER, no API stability is guaranteed. + +package utils + +import ( + "fmt" +) + +func Numeric(size string) string { + return fmt.Sprintf(`[0-9]{%s}`, size) +} + +func Hex(size string) string { + return fmt.Sprintf(`[a-f0-9]{%s}`, size) +} + +func AlphaNumeric(size string) string { + return fmt.Sprintf(`[a-z0-9]{%s}`, size) +} + +func AlphaNumericExtendedShort(size string) string { + return fmt.Sprintf(`[a-z0-9_-]{%s}`, size) +} + +func AlphaNumericExtended(size string) string { + return fmt.Sprintf(`[a-z0-9=_\-]{%s}`, size) +} + +func AlphaNumericExtendedLong(size string) string { + return fmt.Sprintf(`[a-z0-9\/=_\+\-]{%s}`, size) +} + +func Hex8_4_4_4_12() string { + return `[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}` +} diff --git a/cmd/generate/config/utils/validate.go b/cmd/generate/config/utils/validate.go new file mode 100644 index 0000000..8b19292 --- /dev/null +++ b/cmd/generate/config/utils/validate.go @@ -0,0 +1,97 @@ +// == WARNING == +// These functions are used to generate GitLeak's default config. +// You are free to use these in your own project, HOWEVER, no API stability is guaranteed. + +package utils + +import ( + "strings" + + "github.com/zricethezav/gitleaks/v8/cmd/generate/config/base" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/detect" + "github.com/zricethezav/gitleaks/v8/logging" +) + +func Validate(rule config.Rule, truePositives []string, falsePositives []string) *config.Rule { + r := &rule + d := createSingleRuleDetector(r) + for _, tp := range truePositives { + if len(d.DetectString(tp)) < 1 { + logging.Fatal(). + Str("rule", r.RuleID). + Str("value", tp). + Str("regex", r.Regex.String()). + Msg("Failed to Validate. True positive was not detected by regex.") + } + } + for _, fp := range falsePositives { + findings := d.DetectString(fp) + if len(findings) != 0 { + logging.Fatal(). + Str("rule", r.RuleID). + Str("value", fp). + Str("regex", r.Regex.String()). + Msg("Failed to Validate. False positive was detected by regex.") + } + } + return r +} + +func ValidateWithPaths(rule config.Rule, truePositives map[string]string, falsePositives map[string]string) *config.Rule { + r := &rule + d := createSingleRuleDetector(r) + for path, tp := range truePositives { + f := detect.Fragment{Raw: tp, FilePath: path} + if len(d.Detect(f)) != 1 { + logging.Fatal(). + Str("rule", r.RuleID). + Str("value", tp). + Str("regex", r.Regex.String()). + Str("path", r.Path.String()). + Msg("Failed to Validate. True positive was not detected by regex and/or path.") + } + } + for path, fp := range falsePositives { + f := detect.Fragment{Raw: fp, FilePath: path} + if len(d.Detect(f)) != 0 { + logging.Fatal(). + Str("rule", r.RuleID). + Str("value", fp). + Str("regex", r.Regex.String()). + Str("path", r.Path.String()). + Msg("Failed to Validate. False positive was detected by regex and/or path.") + } + } + return r +} + +func createSingleRuleDetector(r *config.Rule) *detect.Detector { + // normalize keywords like in the config package + var ( + uniqueKeywords = make(map[string]struct{}) + keywords []string + ) + for _, keyword := range r.Keywords { + k := strings.ToLower(keyword) + if _, ok := uniqueKeywords[k]; ok { + continue + } + keywords = append(keywords, k) + uniqueKeywords[k] = struct{}{} + } + r.Keywords = keywords + + rules := map[string]config.Rule{ + r.RuleID: *r, + } + cfg := base.CreateGlobalConfig() + cfg.Rules = rules + cfg.Keywords = uniqueKeywords + for _, a := range cfg.Allowlists { + if err := a.Validate(); err != nil { + logging.Fatal().Err(err).Msg("invalid global allowlist") + } + } + return detect.NewDetector(cfg) +} diff --git a/cmd/generate/secrets/regen.go b/cmd/generate/secrets/regen.go new file mode 100644 index 0000000..ee7496e --- /dev/null +++ b/cmd/generate/secrets/regen.go @@ -0,0 +1,15 @@ +// Package reggen generates text based on regex definitions +// This is a slightly altered version of https://github.com/lucasjones/reggen +package secrets + +import ( + "github.com/lucasjones/reggen" +) + +func NewSecret(regex string) string { + g, err := reggen.NewGenerator(regex) + if err != nil { + panic(err) + } + return g.Generate(1) +} diff --git a/cmd/git.go b/cmd/git.go new file mode 100644 index 0000000..0d4f7d2 --- /dev/null +++ b/cmd/git.go @@ -0,0 +1,96 @@ +package cmd + +import ( + "time" + + "github.com/spf13/cobra" + + "github.com/zricethezav/gitleaks/v8/cmd/scm" + "github.com/zricethezav/gitleaks/v8/logging" + "github.com/zricethezav/gitleaks/v8/report" + "github.com/zricethezav/gitleaks/v8/sources" +) + +func init() { + rootCmd.AddCommand(gitCmd) + gitCmd.Flags().String("platform", "", "the target platform used to generate links (github, gitlab)") + gitCmd.Flags().Bool("staged", false, "scan staged commits (good for pre-commit)") + gitCmd.Flags().Bool("pre-commit", false, "scan using git diff") + gitCmd.Flags().String("log-opts", "", "git log options") +} + +var gitCmd = &cobra.Command{ + Use: "git [flags] [repo]", + Short: "scan git repositories for secrets", + Args: cobra.MaximumNArgs(1), + Run: runGit, +} + +func runGit(cmd *cobra.Command, args []string) { + // start timer + start := time.Now() + + // grab source + source := "." + if len(args) == 1 { + source = args[0] + if source == "" { + source = "." + } + } + + // setup config (aka, the thing that defines rules) + initConfig(source) + initDiagnostics() + + cfg := Config(cmd) + + // create detector + detector := Detector(cmd, cfg, source) + + // parse flags + exitCode := mustGetIntFlag(cmd, "exit-code") + logOpts := mustGetStringFlag(cmd, "log-opts") + staged := mustGetBoolFlag(cmd, "staged") + preCommit := mustGetBoolFlag(cmd, "pre-commit") + + var ( + findings []report.Finding + err error + gitCmd *sources.GitCmd + scmPlatform scm.Platform + ) + + if preCommit || staged { + if gitCmd, err = sources.NewGitDiffCmdContext(cmd.Context(), source, staged); err != nil { + logging.Fatal().Err(err).Msg("could not create Git diff cmd") + } + // Remote info + links are irrelevant for staged changes. + scmPlatform = scm.NoPlatform + } else { + if gitCmd, err = sources.NewGitLogCmdContext(cmd.Context(), source, logOpts); err != nil { + logging.Fatal().Err(err).Msg("could not create Git log cmd") + } + if scmPlatform, err = scm.PlatformFromString(mustGetStringFlag(cmd, "platform")); err != nil { + logging.Fatal().Err(err).Send() + } + } + + findings, err = detector.DetectSource( + cmd.Context(), + &sources.Git{ + Cmd: gitCmd, + Config: &detector.Config, + Remote: sources.NewRemoteInfoContext(cmd.Context(), scmPlatform, source), + Sema: detector.Sema, + MaxArchiveDepth: detector.MaxArchiveDepth, + }, + ) + + if err != nil { + // don't exit on error, just log it + logging.Error().Err(err).Msg("failed to scan Git repository") + } + + findingSummaryAndExit(detector, findings, exitCode, start, err) +} diff --git a/cmd/protect.go b/cmd/protect.go new file mode 100644 index 0000000..441ca6c --- /dev/null +++ b/cmd/protect.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "time" + + "github.com/spf13/cobra" + + "github.com/zricethezav/gitleaks/v8/cmd/scm" + "github.com/zricethezav/gitleaks/v8/detect" + "github.com/zricethezav/gitleaks/v8/logging" + "github.com/zricethezav/gitleaks/v8/report" + "github.com/zricethezav/gitleaks/v8/sources" +) + +func init() { + protectCmd.Flags().Bool("staged", false, "detect secrets in a --staged state") + protectCmd.Flags().String("log-opts", "", "git log options") + protectCmd.Flags().StringP("source", "s", ".", "path to source") + rootCmd.AddCommand(protectCmd) +} + +var protectCmd = &cobra.Command{ + Use: "protect", + Short: "protect secrets in code", + Run: runProtect, + Hidden: true, +} + +func runProtect(cmd *cobra.Command, args []string) { + // start timer + start := time.Now() + source := mustGetStringFlag(cmd, "source") + + // setup config (aka, the thing that defines rules) + initConfig(source) + initDiagnostics() + + cfg := Config(cmd) + + // create detector + detector := Detector(cmd, cfg, source) + + // parse flags + exitCode := mustGetIntFlag(cmd, "exit-code") + staged := mustGetBoolFlag(cmd, "staged") + + // start git scan + var ( + findings []report.Finding + err error + + gitCmd *sources.GitCmd + remote *detect.RemoteInfo + ) + + if gitCmd, err = sources.NewGitDiffCmdContext(cmd.Context(), source, staged); err != nil { + logging.Fatal().Err(err).Msg("could not create Git diff cmd") + } + remote = &detect.RemoteInfo{Platform: scm.NoPlatform} + + if findings, err = detector.DetectGit(gitCmd, remote); err != nil { + // don't exit on error, just log it + logging.Error().Err(err).Msg("failed to scan Git repository") + } + findingSummaryAndExit(detector, findings, exitCode, start, err) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..61a163e --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,548 @@ +package cmd + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/rs/zerolog" + "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/logging" + "github.com/zricethezav/gitleaks/v8/regexp" + "github.com/zricethezav/gitleaks/v8/report" + "github.com/zricethezav/gitleaks/v8/version" +) + +const banner = ` + ○ + │╲ + │ ○ + ○ ░ + ░ gitleaks + +` + +const configDescription = `config file path +order of precedence: +1. --config/-c +2. env var GITLEAKS_CONFIG +3. env var GITLEAKS_CONFIG_TOML with the file content +4. (target path)/.gitleaks.toml +If none of the four options are used, then gitleaks will use the default config` + +var ( + rootCmd = &cobra.Command{ + Use: "gitleaks", + Short: "Gitleaks scans code, past or present, for secrets", + Version: version.Version, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // Set the timeout for all the commands + if timeout, err := cmd.Flags().GetInt("timeout"); err != nil { + return err + } else if timeout > 0 { + ctx, cancel := context.WithTimeout(cmd.Context(), time.Duration(timeout)*time.Second) + cmd.SetContext(ctx) + cobra.OnFinalize(cancel) + } + return nil + }, + } + + // diagnostics manager is global to ensure it can be started before a scan begins + // and stopped after a scan completes + diagnosticsManager *DiagnosticsManager +) + +const ( + BYTE = 1.0 + KILOBYTE = BYTE * 1000 + MEGABYTE = KILOBYTE * 1000 + GIGABYTE = MEGABYTE * 1000 +) + +func init() { + cobra.OnInitialize(initLog) + rootCmd.PersistentFlags().StringP("config", "c", "", configDescription) + rootCmd.PersistentFlags().Int("exit-code", 1, "exit code when leaks have been encountered") + rootCmd.PersistentFlags().StringP("report-path", "r", "", "report file (use \"-\" for stdout)") + rootCmd.PersistentFlags().StringP("report-format", "f", "", "output format (json, csv, junit, sarif, template)") + rootCmd.PersistentFlags().StringP("report-template", "", "", "template file used to generate the report (implies --report-format=template)") + rootCmd.PersistentFlags().StringP("baseline-path", "b", "", "path to baseline with issues that can be ignored") + rootCmd.PersistentFlags().StringP("log-level", "l", "info", "log level (trace, debug, info, warn, error, fatal)") + rootCmd.PersistentFlags().BoolP("verbose", "v", false, "show verbose output from scan") + rootCmd.PersistentFlags().BoolP("no-color", "", false, "turn off color for verbose output") + rootCmd.PersistentFlags().Int("max-target-megabytes", 0, "files larger than this will be skipped") + rootCmd.PersistentFlags().BoolP("ignore-gitleaks-allow", "", false, "ignore gitleaks:allow comments") + rootCmd.PersistentFlags().Uint("redact", 0, "redact secrets from logs and stdout. To redact only parts of the secret just apply a percent value from 0..100. For example --redact=20 (default 100%)") + rootCmd.Flag("redact").NoOptDefVal = "100" + rootCmd.PersistentFlags().Bool("no-banner", false, "suppress banner") + rootCmd.PersistentFlags().StringSlice("enable-rule", []string{}, "only enable specific rules by id") + rootCmd.PersistentFlags().StringP("gitleaks-ignore-path", "i", ".", "path to .gitleaksignore file or folder containing one") + rootCmd.PersistentFlags().Int("max-decode-depth", 5, "allow recursive decoding up to this depth") + rootCmd.PersistentFlags().Int("max-archive-depth", 0, "allow scanning into nested archives up to this depth (default \"0\", no archive traversal is done)") + rootCmd.PersistentFlags().Int("timeout", 0, "set a timeout for gitleaks commands in seconds (default \"0\", no timeout is set)") + + // Add diagnostics flags + rootCmd.PersistentFlags().String("diagnostics", "", "enable diagnostics (http OR comma-separated list: cpu,mem,trace). cpu=CPU prof, mem=memory prof, trace=exec tracing, http=serve via net/http/pprof") + rootCmd.PersistentFlags().String("diagnostics-dir", "", "directory to store diagnostics output files when not using http mode (defaults to current directory)") + + err := viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config")) + if err != nil { + logging.Fatal().Msgf("err binding config %s", err.Error()) + } +} + +var logLevel = zerolog.InfoLevel + +func initLog() { + ll, err := rootCmd.Flags().GetString("log-level") + if err != nil { + logging.Fatal().Msg(err.Error()) + } + + switch strings.ToLower(ll) { + case "trace": + logLevel = zerolog.TraceLevel + case "debug": + logLevel = zerolog.DebugLevel + case "info": + logLevel = zerolog.InfoLevel + case "warn": + logLevel = zerolog.WarnLevel + case "err", "error": + logLevel = zerolog.ErrorLevel + case "fatal": + logLevel = zerolog.FatalLevel + default: + logging.Warn().Msgf("unknown log level: %s", ll) + } + logging.Logger = logging.Logger.Level(logLevel) +} + +func initConfig(source string) { + hideBanner, err := rootCmd.Flags().GetBool("no-banner") + viper.SetConfigType("toml") + + if err != nil { + logging.Fatal().Msg(err.Error()) + } + if !hideBanner { + _, _ = fmt.Fprint(os.Stderr, banner) + } + + logging.Debug().Msgf("using %s regex engine", regexp.Version) + + cfgPath, err := rootCmd.Flags().GetString("config") + if err != nil { + logging.Fatal().Msg(err.Error()) + } + if cfgPath != "" { + viper.SetConfigFile(cfgPath) + logging.Debug().Msgf("using gitleaks config %s from `--config`", cfgPath) + } else if os.Getenv("GITLEAKS_CONFIG") != "" { + envPath := os.Getenv("GITLEAKS_CONFIG") + viper.SetConfigFile(envPath) + logging.Debug().Msgf("using gitleaks config from GITLEAKS_CONFIG env var: %s", envPath) + } else if os.Getenv("GITLEAKS_CONFIG_TOML") != "" { + configContent := []byte(os.Getenv("GITLEAKS_CONFIG_TOML")) + if err := viper.ReadConfig(bytes.NewBuffer(configContent)); err != nil { + logging.Fatal().Err(err).Str("content", os.Getenv("GITLEAKS_CONFIG_TOML")).Msg("unable to load gitleaks config from GITLEAKS_CONFIG_TOML env var") + } + logging.Debug().Str("content", os.Getenv("GITLEAKS_CONFIG_TOML")).Msg("using gitleaks config from GITLEAKS_CONFIG_TOML env var content") + return + } else { + fileInfo, err := os.Stat(source) + if err != nil { + logging.Fatal().Msg(err.Error()) + } + + if !fileInfo.IsDir() { + logging.Debug().Msgf("unable to load gitleaks config from %s since --source=%s is a file, using default config", + filepath.Join(source, ".gitleaks.toml"), source) + if err = viper.ReadConfig(strings.NewReader(config.DefaultConfig)); err != nil { + logging.Fatal().Msgf("err reading toml %s", err.Error()) + } + return + } + + if _, err := os.Stat(filepath.Join(source, ".gitleaks.toml")); os.IsNotExist(err) { + logging.Debug().Msgf("no gitleaks config found in path %s, using default gitleaks config", filepath.Join(source, ".gitleaks.toml")) + + if err = viper.ReadConfig(strings.NewReader(config.DefaultConfig)); err != nil { + logging.Fatal().Msgf("err reading default config toml %s", err.Error()) + } + return + } else { + logging.Debug().Msgf("using existing gitleaks config %s from `(--source)/.gitleaks.toml`", filepath.Join(source, ".gitleaks.toml")) + } + + viper.AddConfigPath(source) + viper.SetConfigName(".gitleaks") + } + if err := viper.ReadInConfig(); err != nil { + logging.Fatal().Msgf("unable to load gitleaks config, err: %s", err) + } +} + +func initDiagnostics() { + // Initialize diagnostics manager + diagnosticsFlag, err := rootCmd.PersistentFlags().GetString("diagnostics") + if err != nil { + logging.Fatal().Err(err).Msg("Error getting diagnostics flag") + } + + diagnosticsDir, err := rootCmd.PersistentFlags().GetString("diagnostics-dir") + if err != nil { + logging.Fatal().Err(err).Msg("Error getting diagnostics-dir flag") + } + + var diagErr error + diagnosticsManager, diagErr = NewDiagnosticsManager(diagnosticsFlag, diagnosticsDir) + if diagErr != nil { + logging.Fatal().Err(diagErr).Msg("Error initializing diagnostics") + } + + if diagnosticsManager.Enabled { + logging.Info().Msg("Starting diagnostics...") + if diagErr := diagnosticsManager.StartDiagnostics(); diagErr != nil { + logging.Fatal().Err(diagErr).Msg("Failed to start diagnostics") + } + } + +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + if strings.Contains(err.Error(), "unknown flag") { + // exit code 126: Command invoked cannot execute + os.Exit(126) + } + logging.Fatal().Msg(err.Error()) + } +} + +func Config(cmd *cobra.Command) config.Config { + var vc config.ViperConfig + if err := viper.Unmarshal(&vc); err != nil { + logging.Fatal().Err(err).Msg("Failed to load config") + } + + cfg, err := vc.Translate() + if err != nil { + logging.Fatal().Err(err).Msg("Failed to load config") + } + cfg.Path, _ = cmd.Flags().GetString("config") + + return cfg +} + +func Detector(cmd *cobra.Command, cfg config.Config, source string) *detect.Detector { + var err error + + // Setup common detector + detector := detect.NewDetectorContext(cmd.Context(), cfg) + + if detector.MaxDecodeDepth, err = cmd.Flags().GetInt("max-decode-depth"); err != nil { + logging.Fatal().Err(err).Send() + } + + if detector.MaxArchiveDepth, err = cmd.Flags().GetInt("max-archive-depth"); err != nil { + logging.Fatal().Err(err).Send() + } + + // set color flag at first + if detector.NoColor, err = cmd.Flags().GetBool("no-color"); err != nil { + logging.Fatal().Err(err).Send() + } + // also init logger again without color + if detector.NoColor { + logging.Logger = log.Output(zerolog.ConsoleWriter{ + Out: os.Stderr, + NoColor: detector.NoColor, + }).Level(logLevel) + } + detector.Config.Path, err = cmd.Flags().GetString("config") + if err != nil { + logging.Fatal().Err(err).Send() + } + + // if config path is not set, then use the {source}/.gitleaks.toml path. + // note that there may not be a `{source}/.gitleaks.toml` file, this is ok. + if detector.Config.Path == "" { + detector.Config.Path = filepath.Join(source, ".gitleaks.toml") + } + // set verbose flag + if detector.Verbose, err = cmd.Flags().GetBool("verbose"); err != nil { + logging.Fatal().Err(err).Send() + } + // set redact flag + if detector.Redact, err = cmd.Flags().GetUint("redact"); err != nil { + logging.Fatal().Err(err).Send() + } + if detector.MaxTargetMegaBytes, err = cmd.Flags().GetInt("max-target-megabytes"); err != nil { + logging.Fatal().Err(err).Send() + } + // set ignore gitleaks:allow flag + if detector.IgnoreGitleaksAllow, err = cmd.Flags().GetBool("ignore-gitleaks-allow"); err != nil { + logging.Fatal().Err(err).Send() + } + + gitleaksIgnorePath, err := cmd.Flags().GetString("gitleaks-ignore-path") + if err != nil { + logging.Fatal().Err(err).Msg("could not get .gitleaksignore path") + } + + if fileExists(gitleaksIgnorePath) { + if err = detector.AddGitleaksIgnore(gitleaksIgnorePath); err != nil { + logging.Fatal().Err(err).Msg("could not call AddGitleaksIgnore") + } + } + + if fileExists(filepath.Join(gitleaksIgnorePath, ".gitleaksignore")) { + if err = detector.AddGitleaksIgnore(filepath.Join(gitleaksIgnorePath, ".gitleaksignore")); err != nil { + logging.Fatal().Err(err).Msg("could not call AddGitleaksIgnore") + } + } + + if fileExists(filepath.Join(source, ".gitleaksignore")) { + if err = detector.AddGitleaksIgnore(filepath.Join(source, ".gitleaksignore")); err != nil { + logging.Fatal().Err(err).Msg("could not call AddGitleaksIgnore") + } + } + + // ignore findings from the baseline (an existing report in json format generated earlier) + baselinePath, _ := cmd.Flags().GetString("baseline-path") + if baselinePath != "" { + err = detector.AddBaseline(baselinePath, source) + if err != nil { + logging.Error().Msgf("Could not load baseline. The path must point of a gitleaks report generated using the default format: %s", err) + } + } + + // If set, only apply rules that are defined in the flag + rules, _ := cmd.Flags().GetStringSlice("enable-rule") + if len(rules) > 0 { + logging.Info().Msg("Overriding enabled rules: " + strings.Join(rules, ", ")) + ruleOverride := make(map[string]config.Rule) + for _, ruleName := range rules { + if r, ok := cfg.Rules[ruleName]; ok { + ruleOverride[ruleName] = r + } else { + logging.Fatal().Msgf("Requested rule %s not found in rules", ruleName) + } + } + detector.Config.Rules = ruleOverride + } + + // Validate report settings. + reportPath := mustGetStringFlag(cmd, "report-path") + if reportPath != "" { + if reportPath != report.StdoutReportPath { + // Ensure the path is writable. + if f, err := os.Create(reportPath); err != nil { + logging.Fatal().Err(err).Msgf("Report path is not writable: %s", reportPath) + } else { + _ = f.Close() + _ = os.Remove(reportPath) + } + } + + // Build report writer. + var ( + reporter report.Reporter + reportFormat = mustGetStringFlag(cmd, "report-format") + reportTemplate = mustGetStringFlag(cmd, "report-template") + ) + if reportFormat == "" { + ext := strings.ToLower(filepath.Ext(reportPath)) + switch ext { + case ".csv": + reportFormat = "csv" + case ".json": + reportFormat = "json" + case ".sarif": + reportFormat = "sarif" + default: + logging.Fatal().Msgf("Unknown report format: %s", reportFormat) + } + logging.Debug().Msgf("No report format specified, inferred %q from %q", reportFormat, ext) + } + switch strings.TrimSpace(strings.ToLower(reportFormat)) { + case "csv": + reporter = &report.CsvReporter{} + case "json": + reporter = &report.JsonReporter{} + case "junit": + reporter = &report.JunitReporter{} + case "sarif": + reporter = &report.SarifReporter{ + OrderedRules: cfg.GetOrderedRules(), + } + case "template": + if reporter, err = report.NewTemplateReporter(reportTemplate); err != nil { + logging.Fatal().Err(err).Msg("Invalid report template") + } + default: + logging.Fatal().Msgf("unknown report format %s", reportFormat) + } + + // Sanity check. + if reportTemplate != "" && reportFormat != "template" { + logging.Fatal().Msgf("Report format must be 'template' if --report-template is specified") + } + + detector.ReportPath = reportPath + detector.Reporter = reporter + } + + return detector +} + +func bytesConvert(bytes uint64) string { + unit := "" + value := float32(bytes) + + switch { + case bytes >= GIGABYTE: + unit = "GB" + value = value / GIGABYTE + case bytes >= MEGABYTE: + unit = "MB" + value = value / MEGABYTE + case bytes >= KILOBYTE: + unit = "KB" + value = value / KILOBYTE + case bytes >= BYTE: + unit = "bytes" + case bytes == 0: + return "0" + } + + stringValue := strings.TrimSuffix( + fmt.Sprintf("%.2f", value), ".00", + ) + + return fmt.Sprintf("%s %s", stringValue, unit) +} + +func findingSummaryAndExit(detector *detect.Detector, findings []report.Finding, exitCode int, start time.Time, err error) { + if diagnosticsManager.Enabled { + logging.Debug().Msg("Finalizing diagnostics...") + diagnosticsManager.StopDiagnostics() + } + + totalBytes := detector.TotalBytes.Load() + bytesMsg := fmt.Sprintf("scanned ~%d bytes (%s)", totalBytes, bytesConvert(totalBytes)) + if err == nil { + logging.Info().Msgf("%s in %s", bytesMsg, FormatDuration(time.Since(start))) + if len(findings) != 0 { + logging.Warn().Msgf("leaks found: %d", len(findings)) + } else { + logging.Info().Msg("no leaks found") + } + } else { + logging.Warn().Msg(bytesMsg) + logging.Warn().Msgf("partial scan completed in %s", FormatDuration(time.Since(start))) + if len(findings) != 0 { + logging.Warn().Msgf("%d leaks found in partial scan", len(findings)) + } else { + logging.Warn().Msg("no leaks found in partial scan") + } + } + + // write report if desired + if detector.Reporter != nil { + var ( + file io.WriteCloser + reportErr error + ) + + if detector.ReportPath == report.StdoutReportPath { + file = os.Stdout + } else { + // Open the file. + if file, reportErr = os.Create(detector.ReportPath); reportErr != nil { + goto ReportEnd + } + defer func() { + _ = file.Close() + }() + } + + // Write to the file. + if reportErr = detector.Reporter.Write(file, findings); reportErr != nil { + goto ReportEnd + } + + ReportEnd: + if reportErr != nil { + logging.Fatal().Err(reportErr).Msg("failed to write report") + } + } + + if err != nil { + os.Exit(1) + } + + if len(findings) != 0 { + os.Exit(exitCode) + } +} + +func fileExists(fileName string) bool { + // check for a .gitleaksignore file + info, err := os.Stat(fileName) + if err != nil && !os.IsNotExist(err) { + return false + } + + if info != nil && err == nil { + if !info.IsDir() { + return true + } + } + return false +} + +func FormatDuration(d time.Duration) string { + scale := 100 * time.Second + // look for the max scale that is smaller than d + for scale > d { + scale = scale / 10 + } + return d.Round(scale / 100).String() +} + +func mustGetBoolFlag(cmd *cobra.Command, name string) bool { + value, err := cmd.Flags().GetBool(name) + if err != nil { + logging.Fatal().Err(err).Msgf("could not get flag: %s", name) + } + return value +} + +func mustGetIntFlag(cmd *cobra.Command, name string) int { + value, err := cmd.Flags().GetInt(name) + if err != nil { + logging.Fatal().Err(err).Msgf("could not get flag: %s", name) + } + return value +} + +func mustGetStringFlag(cmd *cobra.Command, name string) string { + value, err := cmd.Flags().GetString(name) + if err != nil { + logging.Fatal().Err(err).Msgf("could not get flag: %s", name) + } + return value +} diff --git a/cmd/scm/scm.go b/cmd/scm/scm.go new file mode 100644 index 0000000..23022a9 --- /dev/null +++ b/cmd/scm/scm.go @@ -0,0 +1,52 @@ +package scm + +import ( + "fmt" + "strings" +) + +type Platform int + +const ( + UnknownPlatform Platform = iota + NoPlatform // Explicitly disable the feature + GitHubPlatform + GitLabPlatform + AzureDevOpsPlatform + GiteaPlatform + BitbucketPlatform + // TODO: Add others. +) + +func (p Platform) String() string { + return [...]string{ + "unknown", + "none", + "github", + "gitlab", + "azuredevops", + "gitea", + "bitbucket", + }[p] +} + +func PlatformFromString(s string) (Platform, error) { + switch strings.ToLower(s) { + case "", "unknown": + return UnknownPlatform, nil + case "none": + return NoPlatform, nil + case "github": + return GitHubPlatform, nil + case "gitlab": + return GitLabPlatform, nil + case "azuredevops": + return AzureDevOpsPlatform, nil + case "gitea": + return GiteaPlatform, nil + case "bitbucket": + return BitbucketPlatform, nil + default: + return UnknownPlatform, fmt.Errorf("invalid scm platform value: %s", s) + } +} diff --git a/cmd/stdin.go b/cmd/stdin.go new file mode 100644 index 0000000..e7b5a82 --- /dev/null +++ b/cmd/stdin.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "os" + "time" + + "github.com/spf13/cobra" + + "github.com/zricethezav/gitleaks/v8/logging" + "github.com/zricethezav/gitleaks/v8/sources" +) + +func init() { + rootCmd.AddCommand(stdInCmd) +} + +var stdInCmd = &cobra.Command{ + Use: "stdin", + Short: "detect secrets from stdin", + Run: runStdIn, +} + +func runStdIn(cmd *cobra.Command, _ []string) { + // start timer + start := time.Now() + + // setup config (aka, the thing that defines rules) + initConfig(".") + initDiagnostics() + + cfg := Config(cmd) + + // create detector + detector := Detector(cmd, cfg, "") + + // parse flag(s) + exitCode := mustGetIntFlag(cmd, "exit-code") + + findings, err := detector.DetectSource( + cmd.Context(), + &sources.File{ + Content: os.Stdin, + MaxArchiveDepth: detector.MaxArchiveDepth, + }, + ) + + if err != nil { + // log fatal to exit, no need to continue since a report will not be + // generated when scanning from a pipe...for now + logging.Fatal().Err(err).Msg("failed scan input from stdin") + } + + findingSummaryAndExit(detector, findings, exitCode, start, err) +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..18777a6 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/zricethezav/gitleaks/v8/version" +) + +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.Version) +} diff --git a/config/allowlist.go b/config/allowlist.go new file mode 100644 index 0000000..5ad10fa --- /dev/null +++ b/config/allowlist.go @@ -0,0 +1,179 @@ +package config + +import ( + "errors" + "strings" + + ahocorasick "github.com/BobuSumisu/aho-corasick" + "golang.org/x/exp/maps" + + "github.com/zricethezav/gitleaks/v8/regexp" +) + +type AllowlistMatchCondition int + +const ( + AllowlistMatchOr AllowlistMatchCondition = iota + AllowlistMatchAnd +) + +func (a AllowlistMatchCondition) String() string { + return [...]string{ + "OR", + "AND", + }[a] +} + +// Allowlist allows a rule to be ignored for specific +// regexes, paths, and/or commits +type Allowlist struct { + // Short human readable description of the allowlist. + Description string + + // MatchCondition determines whether all criteria must match. Defaults to "OR". + MatchCondition AllowlistMatchCondition + + // Commits is a slice of commit SHAs that are allowed to be ignored. + Commits []string + + // Paths is a slice of path regular expressions that are allowed to be ignored. + Paths []*regexp.Regexp + + // Can be `match` or `line`. + // + // If `match` the _Regexes_ will be tested against the match of the _Rule.Regex_. + // + // If `line` the _Regexes_ will be tested against the entire line. + // + // If RegexTarget is empty, it will be tested against the found secret. + RegexTarget string + + // Regexes is slice of content regular expressions that are allowed to be ignored. + Regexes []*regexp.Regexp + + // StopWords is a slice of stop words that are allowed to be ignored. + // This targets the _secret_, not the content of the regex match like the + // Regexes slice. + StopWords []string + + // validated is an internal flag to track whether `Validate()` has been called. + validated bool + + // commitMap is a normalized version of Commits, used for efficiency purposes. + // TODO: possible optimizations so that both short and long hashes work. + commitMap map[string]struct{} + regexPat *regexp.Regexp + pathPat *regexp.Regexp + stopwordTrie *ahocorasick.Trie +} + +func (a *Allowlist) Validate() error { + if a.validated { + return nil + } + + // Disallow empty allowlists. + if len(a.Commits) == 0 && + len(a.Paths) == 0 && + len(a.Regexes) == 0 && + len(a.StopWords) == 0 { + return errors.New("must contain at least one check for: commits, paths, regexes, or stopwords") + } + + // Deduplicate commits and stopwords. + if len(a.Commits) > 0 { + uniqueCommits := make(map[string]struct{}) + for _, commit := range a.Commits { + // Commits are case-insensitive. + uniqueCommits[strings.TrimSpace(strings.ToLower(commit))] = struct{}{} + } + a.Commits = maps.Keys(uniqueCommits) + a.commitMap = uniqueCommits + } + if len(a.StopWords) > 0 { + uniqueStopwords := make(map[string]struct{}) + for _, stopWord := range a.StopWords { + uniqueStopwords[strings.ToLower(stopWord)] = struct{}{} + } + + values := maps.Keys(uniqueStopwords) + a.StopWords = values + a.stopwordTrie = ahocorasick.NewTrieBuilder().AddStrings(values).Build() + } + + // Combine patterns into a single expression. + if len(a.Paths) > 0 { + a.pathPat = joinRegexOr(a.Paths) + } + if len(a.Regexes) > 0 { + a.regexPat = joinRegexOr(a.Regexes) + } + + a.validated = true + return nil +} + +// CommitAllowed returns true if the commit is allowed to be ignored. +func (a *Allowlist) CommitAllowed(c string) (bool, string) { + if a == nil || c == "" { + return false, "" + } + if a.commitMap != nil { + if _, ok := a.commitMap[strings.ToLower(c)]; ok { + return true, "" + } + } else if len(a.Commits) > 0 { + for _, commit := range a.Commits { + if commit == c { + return true, c + } + } + } + return false, "" +} + +// PathAllowed returns true if the path is allowed to be ignored. +func (a *Allowlist) PathAllowed(path string) bool { + if a == nil || path == "" { + return false + } + if a.pathPat != nil { + return a.pathPat.MatchString(path) + } else if len(a.Paths) > 0 { + return anyRegexMatch(path, a.Paths) + } + return false +} + +// RegexAllowed returns true if the regex is allowed to be ignored. +func (a *Allowlist) RegexAllowed(secret string) bool { + if a == nil || secret == "" { + return false + } + if a.regexPat != nil { + return a.regexPat.MatchString(secret) + } else if len(a.Regexes) > 0 { + return anyRegexMatch(secret, a.Regexes) + } + return false +} + +func (a *Allowlist) ContainsStopWord(s string) (bool, string) { + if a == nil || s == "" { + return false, "" + } + + s = strings.ToLower(s) + if a.stopwordTrie != nil { + if m := a.stopwordTrie.MatchFirstString(s); m != nil { + return true, m.MatchString() + } + } else if len(a.StopWords) > 0 { + for _, stopWord := range a.StopWords { + if strings.Contains(s, stopWord) { + return true, stopWord + } + } + } + return false, "" +} diff --git a/config/allowlist_test.go b/config/allowlist_test.go new file mode 100644 index 0000000..aced6f1 --- /dev/null +++ b/config/allowlist_test.go @@ -0,0 +1,380 @@ +package config + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" + + "github.com/zricethezav/gitleaks/v8/regexp" +) + +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 { + isAllowed, _ := tt.allowlist.CommitAllowed(tt.commit) + assert.Equal(t, tt.commitAllowed, isAllowed) + } +} + +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)) + } +} + +func TestValidate(t *testing.T) { + tests := map[string]struct { + input Allowlist + expected Allowlist + wantErr error + }{ + "empty conditions": { + input: Allowlist{}, + wantErr: errors.New("must contain at least one check for: commits, paths, regexes, or stopwords"), + }, + "deduplicated commits and stopwords": { + input: Allowlist{ + Commits: []string{"commitA", "commitB", "commitA"}, + StopWords: []string{"stopwordA", "stopwordB", "stopwordA"}, + }, + expected: Allowlist{ + Commits: []string{"commita", "commitb"}, + StopWords: []string{"stopworda", "stopwordb"}, + }, + }, + } + + for _, tt := range tests { + // Expected an error. + err := tt.input.Validate() + if err != nil { + if tt.wantErr == nil { + t.Fatalf("Received unexpected error: %v", err) + } else if !assert.EqualError(t, err, tt.wantErr.Error()) { + t.Fatalf("Received unexpected error, expected '%v', got '%v'", tt.wantErr, err) + } + } else { + if tt.wantErr != nil { + t.Fatalf("Did not receive expected error: %v", tt.wantErr) + } + } + + var ( + regexComparer = func(x, y *regexp.Regexp) bool { + // Compare the string representation of the regex patterns. + if x == nil || y == nil { + return x == y + } + return x.String() == y.String() + } + arrayComparer = func(a, b string) bool { + return a < b + } + opts = cmp.Options{ + cmp.Comparer(regexComparer), + cmpopts.SortSlices(arrayComparer), + cmpopts.IgnoreUnexported(Allowlist{}), + } + ) + if diff := cmp.Diff(tt.expected, tt.input, opts); diff != "" { + t.Errorf("diff: (-want +got)\n%s", diff) + } + } +} + +var benchCommitAllowlist = func() *Allowlist { + allowlist := Allowlist{ + Commits: []string{ + "ba1beca8ac634d8b202e0daffebecceeb6dda2fc", + "74b19abcd33ff3a6cac8aebdcfccacde9ad40f5c", + "11ed23ff2df37b2c114ac13dbd902e3bdb8b9c63", + "d39bcd0ebd3c9fb8d2f1bfbc98fc92bceeb3eed3", + "faf321cf8f9f2cf654fab12f72ef64e6ffb7237e", + "5486ce1abcdfcc862efeb6f5071a1ddef0fb3926", + "fbcfdbdffdabf1c0f627ecdfcda91ca8c1b533ce", + "3db86dce50f1d99fe820a4bf42ffedaefedb1bcf", + "18ccec6cb316ce1abed9e8ec262ecd3dd4a3e222", + "20f1cc29eaa028e7e6bcdc82f2103c1af3899c5c", + "d90b5572e9bdb0cebf5edc3ad412c38ecd6b8ca6", + "a0c7a0addf82d2a84babdd8be1f3cee887cb69cf", + "0c3c967d21bca179e2d60b738fab848be9a65a45", + "85cb3ac8817afad5ffde45a29036f4a1fa8af2eb", + "cb9c19a9dcb05bebca7dacbed9b8afefeb2dc5ad", + "4bac7b9fbdc851caf3c5f0cece85acaa3ebb7fd2", + "ad2bfd0c271bbb1ac991fdfefcabb6dcc43cb3f5", + "0c522c2750ec2fdf134721ea6000841fed5ffdd1", + "fedbf91dddc017f9994edc7f3a75dcd9eabfdd2c", + "9e5205bdeb080d84bfb25dfcc86dccc3fe5be499", + "b5476d7901b105c2fdb5fc87a9f4fabdb40daf81", + "f8fdbc509fb3c3ef1c1cbe8e70ac933ac4109cc3", + "92727ea97463cd55eed3069ab54483ba20b94f3c", + "e4a01ce6fdb6f6a39d2c781f7dcba3ae37973d0b", + "dcdd273bdf8159faf60bfca6b2a20a98a9985cf3", + "3f1efb9b7705cc1cae9ebeafa8eaf95ab79d6bea", + "20831ce7da9e2cb78ad8cd2a73ca693d8ffaee9c", + "edffec40aa85fbf947b20b3ffcfe6e7f6a94d017", + "cfbbc60acefdc1f04ad26e022fba21f72ce5c25b", + "3beac0da108cbb4fac1ebcd060ba2b67aafbefdd", + "21a87495e1f1f4ee9d9dfab1c3e5ffbe4b6e7dba", + "a2a3e8ba9dcae74d7bc1267f07fec3a44a0c9b08", + "abe2c2ff0a9befc1cbae77ea6aebf8d3a46ee68f", + "9f9e4cea3bee21deb769f830bcf76185eba76ab6", + "48e2f92ccdd1cb33e10eba117386cb054e06f2e0", + "4e10cc0ae797adce6cec0beda8782b28ba2dcf8c", + "7d6eb5bc7bbee783ab012ee290f8b9acabe38367", + "bc5d1b99fd2cbacb225e574fd05c57a3f44ca35c", + "9a3ea6fd3dcfd135ea3b9bc63ab6494f5c54faaf", + "fc62eec1f10e646ec9a8dacaddf1becae7e31823", + "fd01d7f4dab422fba5b9abfeacee14aad6db0dee", + "d7a2bcbcab6a4f4af02dde8ceeef5b6ab0fd6ca5", + "0b1bf0ebfc1cca19eeb029cfb8e4abd8e72d82df", + "a1ea3f771dfbae1cadbc67c803b8b07081f3fdd0", + "78dfa84c70545f295b3b8fa3f8bd53d316ff682e", + "ca595eebf56ad6f118be2c5ddbeadac675a588ad", + "faadceaec7d0bb9cbef7e7f6cddc3a0fdb8e33b8", + "0b006e37964c7b1e3cdbafd21fe8e887886cb909", + "dffb3dfb72fa42ed49a69dae3838eaaf2a957ee0", + "2d927ebda284a25fb2cd8fc0dd42c9b7d333db5a", + "ffca0cdcbe9e33fd18a3bf692ea0f2a62d685aa4", + "ac0d9a8adbe32cbe7f99fdeccf22b5bf767d06fa", + "f4a2a0a2833f4cbcc2db2accd7cc4feacdeeeaed", + "5f4a1e0dcbd49ed99eadf4bf837f1efafea663c1", + "3e69fb30ea6761771f877d82ef8ee799eb1f56a0", + "2e13cb4dec42ca80918e2bdefb5df96bf9b1eebc", + "d480ed3e25cb82782bb6b3f0fffce8f7c3d9840b", + "7bd34bf663bd5900f0be9accb2091e3dc4a378bc", + "a9fdc3ccc7cfd3b2a73b55b957bbe73beedba2ea", + "b7ca933acc3ef5c8c5bfaede6bfc4f9acc7f4f39", + "248a04fea1bfc38f4a1b76ea4ce399ef0affe4cb", + "8ae496b4fb06c46ae5bfcdceda5e9773ca9ce25b", + "0ac468471f71e8d17c21eb9fe4d922549e13bc3a", + "db082f214cdbff93cdaefebabae455f2ed994730", + "df0c6eabe5ccffed0bf840f3c45a8fdf39c04f9b", + "aacaadfd5bd1e9efd6fc8a9b7ad8fe93ec3bbf23", + "4e9cbeaabc66d01da99cd85cbaf8db5bfaccf5c4", + "28b39fd14b2c8dbeab86aea69de0b5ccdf4ff7b2", + "a20ebfbef4c75e906a114eaad6a67deadb810ed9", + "f8fd3b4cd75b6fc28e21289e6d4ef7c4c7f22f68", + "ccf7e03c5f5c7d6fdbd9fbde2fd342ae15cbbe4e", + "cdcbcfe7c6d4cee31a27f338d1ff66d3cad4bbd5", + "d7c03aba693ba8de8aaea7fa9d8d46a733cd5989", + "48e3f4abb26eb44eefcaf3f188cea00cce46ddc1", + "c1a9c25e5e29804abbc8a0fccf1faf384fea28a5", + "f9e090c6b2af0ee3bde57f4a0352f5b02e675d35", + "1df2354e9eee4934c34dcbd2de0b1df9b5dcadac", + "7da5ad4d8fa965ea4aa4613a9dc4eabdc3406eeb", + "fe33bfb676572e7fafd8ae9ea88e0efa3c109bdd", + "fefeea80b187eefce0a9c184cb76976c2fcef98a", + "eea8efae44ab9eb43a5954df7fa63bb55fa1d8dc", + "f559c7bbe6b6bfba56e7f362604c721c2fee6d0d", + "bf1dbe006b93fb443aebbe77a2c9cfdfc3f6a6a0", + "cfd00d87f7bc44ed7bdcebbf493869a495ebfef8", + "7ca45a388fd9aaeb8ed7701c2af2beff9e52eebb", + "ca47c2fdb73b2d39a1eab115dffa1ef7a5ffacd0", + "cc0954dfddaabb996abc78c28dff83f7e3edb828", + "ad4adc1195dabcfaf0f7eaac4f3f3f6b152d7fd4", + "ab3e6b6ed2eb6dcd4ae4fd0db76efdecd72ecfaa", + "d16ccd31d433f7a41eceb7d544081c72b4d9bf3e", + "d0dbe09bb150bbd5bb4b85adc273df87350e7e6c", + "492bbbcaf6edd864dd3ca0aee5d4d60b1cd4214b", + "d73dbbe7a9effb828793a3adad04cecb1843ccbd", + "89b12dcbfc40e30ddecd7a19e0cf1e32a29ebccb", + "1cd815bd965ee0fd2e15bbe28c94688f15ebb3ad", + "a7f9b3babda7abfe0316bf7aeded7df7cfcf7bdf", + "9e832d5ebecc2e5aead9b5cdcdd6d2ba4dfeaee7", + "43fa54761faa5f8beffe9acfd402fcd24dfede71", + "dada4b08ae4aa37cff754b4ca29ebc134c54bac1", + "1b708f1fad29cc62bc39ae5dda8e9124acc00d2d", + }, + } + _ = allowlist.Validate() + return &allowlist +}() + +func BenchmarkCommitAllowed(b *testing.B) { + for n := 0; n < b.N; n++ { + ok, _ := benchCommitAllowlist.CommitAllowed("d0dbe09bb150bbd5bb4b85adc273df87350e7e6c") + assert.True(b, ok) + } +} + +func BenchmarkCommitNotAllowed(b *testing.B) { + for n := 0; n < b.N; n++ { + ok, _ := benchCommitAllowlist.CommitAllowed("5fe58bf0b0be1735ad27aa6053b56323a905c223") + assert.False(b, ok) + } +} + +var benchRegexAllowlist = func() *Allowlist { + a := Allowlist{ + RegexTarget: "match", + Regexes: []*regexp.Regexp{ + // Based on patterns from `generic.go` + regexp.MustCompile(`(?i)access(ibility|or)`), + regexp.MustCompile(`(?i)access[_.-]?id`), + regexp.MustCompile(`(?i)random[_.-]?access`), + regexp.MustCompile(`(?i)api[_.-]?(id|name|version)`), + regexp.MustCompile(`(?i)rapid|capital`), + regexp.MustCompile(`(?i)[a-z0-9-]*?api[a-z0-9-]*?:jar:`), + regexp.MustCompile(`(?i)author`), + regexp.MustCompile(`(?i)X-MS-Exchange-Organization-Auth`), + regexp.MustCompile(`(?i)Authentication-Results`), + regexp.MustCompile(`(?i)(credentials?[_.-]?id|withCredentials)`), + regexp.MustCompile(`(?i)(bucket|foreign|hot|idx|natural|primary|pub(lic)?|schema|sequence)[_.-]?key`), + regexp.MustCompile(`(?i)key[_.-]?(alias|board|code|frame|id|length|mesh|name|pair|ring|selector|signature|size|stone|storetype|word|up|down|left|right)`), + regexp.MustCompile(`(?i)key[_.-]?vault[_.-]?(id|name)|keyVaultToStoreSecrets`), + regexp.MustCompile(`(?i)key(store|tab)[_.-]?(file|path)`), + regexp.MustCompile(`(?i)issuerkeyhash`), + regexp.MustCompile(`(?i)(?-i:[DdMm]onkey|[DM]ONKEY)|keying`), + regexp.MustCompile(`(?i)(secret)[_.-]?(length|name|size)`), + regexp.MustCompile(`(?i)UserSecretsId`), + regexp.MustCompile(`(?i)(io\.jsonwebtoken[ \t]?:[ \t]?[\w-]+)`), + regexp.MustCompile(`(?i)(api|credentials|token)[_.-]?(endpoint|ur[il])`), + regexp.MustCompile(`(?i)public[_.-]?token`), + regexp.MustCompile(`(?i)(key|token)[_.-]?file`), + regexp.MustCompile(`([A-Z_]+=\n[A-Z_]+=|[a-z_]+=\n[a-z_]+=)(\n|\z)`), + regexp.MustCompile(`([A-Z.]+=\n[A-Z.]+=|[a-z.]+=\n[a-z.]+=)(\n|\z)`), + }, + } + _ = a.Validate() + return &a +}() + +func BenchmarkRegexAllowed(b *testing.B) { + for n := 0; n < b.N; n++ { + ok := benchRegexAllowlist.RegexAllowed(`environment { + CREDENTIALS_ID = "K8S_CRED" +}`) + assert.True(b, ok) + } +} + +func BenchmarkRegexNotAllowed(b *testing.B) { + for n := 0; n < b.N; n++ { + ok := benchRegexAllowlist.RegexAllowed(`"credentials" : "0afae57f3ccfd9d7f5767067bc48b30f719e271ba470488056e37ab35d4b6506"`) + assert.False(b, ok) + } +} + +var benchPathAllowlist = func() *Allowlist { + a := Allowlist{ + Paths: []*regexp.Regexp{ + // Copied from `base/config.go` + regexp.MustCompile(`gitleaks\.toml`), + regexp.MustCompile(`(?i)\.(bmp|gif|jpe?g|svg|tiff?)$`), + regexp.MustCompile(`\.(eot|[ot]tf|woff2?)$`), + regexp.MustCompile(`(.*?)(doc|docx|zip|xls|pdf|bin|socket|vsidx|v2|suo|wsuo|.dll|pdb|exe|gltf)$`), + regexp.MustCompile(`go\.(mod|sum|work(\.sum)?)$`), + regexp.MustCompile(`(^|/)vendor/modules\.txt$`), + regexp.MustCompile(`(^|/)vendor/(github\.com|golang\.org/x|google\.golang\.org|gopkg\.in|istio\.io|k8s\.io|sigs\.k8s\.io)(/.*)?$`), + regexp.MustCompile(`(^|/)gradlew(\.bat)?$`), + regexp.MustCompile(`(^|/)gradle\.lockfile$`), + regexp.MustCompile(`(^|/)mvnw(\.cmd)?$`), + regexp.MustCompile(`(^|/)\.mvn/wrapper/MavenWrapperDownloader\.java$`), + regexp.MustCompile(`(^|/)node_modules(/.*)?$`), + regexp.MustCompile(`(^|/)(npm-shrinkwrap\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$`), + regexp.MustCompile(`(^|/)bower_components(/.*)?$`), + regexp.MustCompile(`(^|/)(angular|bootstrap|jquery(-?ui)?|plotly|swagger-?ui)[a-zA-Z0-9.-]*(\.min)?\.js(\.map)?$`), + regexp.MustCompile(`(^|/)javascript\.json$`), + regexp.MustCompile(`(^|/)(Pipfile|poetry)\.lock$`), + regexp.MustCompile(`(?i)/?(v?env|virtualenv)/lib(64)?(/.*)?$`), + regexp.MustCompile(`(?i)(^|/)(lib(64)?/python[23](\.\d{1,2})+|python/[23](\.\d{1,2})+/lib(64)?)(/.*)?$`), + regexp.MustCompile(`(?i)(^|/)[a-z0-9_.]+-[0-9.]+\.dist-info(/.+)?$`), + regexp.MustCompile(`(^|/)vendor/(bundle|ruby)(/.*?)?$`), + regexp.MustCompile(`\.gem$`), + regexp.MustCompile(`verification-metadata\.xml`), + regexp.MustCompile(`Database.refactorlog`), + }, + } + _ = a.Validate() + return &a +}() + +func BenchmarkPathAllowed(b *testing.B) { + for n := 0; n < b.N; n++ { + ok := benchPathAllowlist.PathAllowed(`src/main/resources/static/js/jquery-ui-1.10.4.min.js`) + assert.True(b, ok) + } +} + +func BenchmarkPathNotAllowed(b *testing.B) { + for n := 0; n < b.N; n++ { + ok := benchPathAllowlist.PathAllowed(`azure_scale_templates/sub_modules/vpc_template/inputs.auto.tfvars.json_backup`) + assert.False(b, ok) + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..74fd3d2 --- /dev/null +++ b/config/config.go @@ -0,0 +1,492 @@ +package config + +import ( + _ "embed" + "errors" + "fmt" + "sort" + "strings" + + gv "github.com/hashicorp/go-version" + "github.com/spf13/viper" + + "github.com/zricethezav/gitleaks/v8/logging" + "github.com/zricethezav/gitleaks/v8/regexp" + "github.com/zricethezav/gitleaks/v8/version" +) + +var ( + //go:embed gitleaks.toml + DefaultConfig string + + // use to keep track of how many configs we can extend + // yea I know, globals bad + extendDepth int +) + +const maxExtendDepth = 2 + +// 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 { + Title string + Description string + Extend Extend + Rules []struct { + ID string + Description string + Path string + Regex string + SecretGroup int + Entropy float64 + Keywords []string + Tags []string + + // Deprecated: this is a shim for backwards-compatibility. + // TODO: Remove this in 9.x. + AllowList *viperRuleAllowlist + + Allowlists []*viperRuleAllowlist + Required []*viperRequired + SkipReport bool + } + // Deprecated: this is a shim for backwards-compatibility. + // TODO: Remove this in 9.x. + AllowList *viperGlobalAllowlist + + Allowlists []*viperGlobalAllowlist + + MinVersion string + + configPath string +} + +type viperRequired struct { + ID string + WithinLines *int `mapstructure:"withinLines"` + WithinColumns *int `mapstructure:"withinColumns"` +} + +type viperRuleAllowlist struct { + Description string + Condition string + Commits []string + Paths []string + RegexTarget string + Regexes []string + StopWords []string +} + +type viperGlobalAllowlist struct { + TargetRules []string + viperRuleAllowlist `mapstructure:",squash"` +} + +// Config is a configuration struct that contains rules and an allowlist if present. +type Config struct { + Title string + Extend Extend + Path string + Description string + Rules map[string]Rule + Keywords map[string]struct{} + // used to keep sarif results consistent + OrderedRules []string + Allowlists []*Allowlist + MinVersion string +} + +// Extend is a struct that allows users to define how they want their +// configuration extended by other configuration files. +type Extend struct { + Path string + URL string + UseDefault bool + DisabledRules []string +} + +func (vc *ViperConfig) Translate() (Config, error) { + var ( + keywords = make(map[string]struct{}) + orderedRules []string + rulesMap = make(map[string]Rule) + ruleAllowlists = make(map[string][]*Allowlist) + ) + + // Validate individual rules. + for _, vr := range vc.Rules { + var ( + pathPat *regexp.Regexp + regexPat *regexp.Regexp + ) + if vr.Path != "" { + pathPat = regexp.MustCompile(vr.Path) + } + if vr.Regex != "" { + regexPat = regexp.MustCompile(vr.Regex) + } + if vr.Keywords == nil { + vr.Keywords = []string{} + } else { + for i, k := range vr.Keywords { + keyword := strings.ToLower(k) + keywords[keyword] = struct{}{} + vr.Keywords[i] = keyword + } + } + if vr.Tags == nil { + vr.Tags = []string{} + } + cr := Rule{ + RuleID: vr.ID, + Description: vr.Description, + Regex: regexPat, + SecretGroup: vr.SecretGroup, + Entropy: vr.Entropy, + Path: pathPat, + Keywords: vr.Keywords, + Tags: vr.Tags, + SkipReport: vr.SkipReport, + } + + // Parse the rule allowlists, including the older format for backwards compatibility. + if vr.AllowList != nil { + // TODO: Remove this in v9. + if len(vr.Allowlists) > 0 { + return Config{}, fmt.Errorf("%s: [rules.allowlist] is deprecated, it cannot be used alongside [[rules.allowlist]]", cr.RuleID) + } + vr.Allowlists = append(vr.Allowlists, vr.AllowList) + } + for _, a := range vr.Allowlists { + allowlist, err := vc.parseAllowlist(a) + if err != nil { + return Config{}, fmt.Errorf("%s: [[rules.allowlists]] %w", cr.RuleID, err) + } + cr.Allowlists = append(cr.Allowlists, allowlist) + } + + for _, r := range vr.Required { + if r.ID == "" { + return Config{}, fmt.Errorf("%s: [[rules.required]] rule ID is empty", cr.RuleID) + } + requiredRule := Required{ + RuleID: r.ID, + WithinLines: r.WithinLines, + WithinColumns: r.WithinColumns, + // Distance: r.Distance, + } + cr.RequiredRules = append(cr.RequiredRules, &requiredRule) + } + + orderedRules = append(orderedRules, cr.RuleID) + rulesMap[cr.RuleID] = cr + } + + // after all the rules have been processed, let's ensure the required rules + // actually exist. + for _, r := range rulesMap { + for _, rr := range r.RequiredRules { + if _, ok := rulesMap[rr.RuleID]; !ok { + return Config{}, fmt.Errorf("%s: [[rules.required]] rule ID '%s' does not exist", r.RuleID, rr.RuleID) + } + } + } + + // Assemble the config. + c := Config{ + Title: vc.Title, + Description: vc.Description, + Extend: vc.Extend, + Rules: rulesMap, + Keywords: keywords, + OrderedRules: orderedRules, + MinVersion: vc.MinVersion, + } + + if extendDepth > 0 { + // annoying hack to set the current config with the extended path + // since if extendDepth > 0 we are operating an extended config. + c.Path = vc.configPath + } else { + // I don't love this + c.Path = viper.ConfigFileUsed() + } + + if err := validateMinVersion(c.MinVersion, c.Path); err != nil { + return Config{}, err + } + + // Parse the config allowlists, including the older format for backwards compatibility. + if vc.AllowList != nil { + // TODO: Remove this in v9. + if len(vc.Allowlists) > 0 { + return Config{}, errors.New("[allowlist] is deprecated, it cannot be used alongside [[allowlists]]") + } + vc.Allowlists = append(vc.Allowlists, vc.AllowList) + } + for _, a := range vc.Allowlists { + allowlist, err := vc.parseAllowlist(&a.viperRuleAllowlist) + if err != nil { + return Config{}, fmt.Errorf("[[allowlists]] %w", err) + } + // Allowlists with |targetRules| aren't added to the global list. + if len(a.TargetRules) > 0 { + for _, ruleID := range a.TargetRules { + // It's not possible to validate |ruleID| until after extend. + ruleAllowlists[ruleID] = append(ruleAllowlists[ruleID], allowlist) + } + } else { + c.Allowlists = append(c.Allowlists, allowlist) + } + } + + currentExtendDepth := extendDepth + if maxExtendDepth != currentExtendDepth { + // disallow both usedefault and path from being set + if c.Extend.Path != "" && c.Extend.UseDefault { + return Config{}, errors.New("unable to load config due to extend.path and extend.useDefault being set") + } + if c.Extend.UseDefault { + if err := c.extendDefault(vc); err != nil { + return Config{}, err + } + } else if c.Extend.Path != "" { + if err := c.extendPath(vc); err != nil { + return Config{}, err + } + } + } + + // Validate the rules after everything has been assembled (including extended configs). + if currentExtendDepth == 0 { + for _, rule := range c.Rules { + if err := rule.Validate(); err != nil { + return Config{}, err + } + } + + // Populate targeted configs. + for ruleID, allowlists := range ruleAllowlists { + rule, ok := c.Rules[ruleID] + if !ok { + return Config{}, fmt.Errorf("[[allowlists]] target rule ID '%s' does not exist", ruleID) + } + rule.Allowlists = append(rule.Allowlists, allowlists...) + c.Rules[ruleID] = rule + } + } + + return c, nil +} + +func validateMinVersion(minVer string, configPath string) error { + if minVer == "" { + logging.Debug().Str("config path", configPath). + Msg("no minVersion specified in config... consider adding minVersion to ensure compatibility.") + return nil + } + + if version.Version == version.DefaultMsg { + logging.Debug(). + Str("required", minVer). + Msg("dev build, skipping config version check.") + return nil + } + + minSemVer, err := gv.NewSemver(minVer) + if err != nil { + return fmt.Errorf("invalid minVersion '%s': %w", minVer, err) + } + + currentSemVer, err := gv.NewSemver(version.Version) + if err != nil { + return fmt.Errorf("unable to parse current version: %w", err) + } + + if currentSemVer.LessThan(minSemVer) { + logging.Warn(). + Str("required", minVer). + Str("current", version.Version). + Str("config path", configPath). + Msg("config requires a newer Gitleaks version...") + } + + return nil +} + +func (vc *ViperConfig) parseAllowlist(a *viperRuleAllowlist) (*Allowlist, error) { + var matchCondition AllowlistMatchCondition + switch strings.ToUpper(a.Condition) { + case "AND", "&&": + matchCondition = AllowlistMatchAnd + case "", "OR", "||": + matchCondition = AllowlistMatchOr + default: + return nil, fmt.Errorf("unknown allowlist |condition| '%s' (expected 'and', 'or')", a.Condition) + } + + // Validate the target. + regexTarget := a.RegexTarget + if regexTarget != "" { + switch regexTarget { + case "secret": + regexTarget = "" + case "match", "line": + // do nothing + default: + return nil, fmt.Errorf("unknown allowlist |regexTarget| '%s' (expected 'match', 'line')", regexTarget) + } + } + var allowlistRegexes []*regexp.Regexp + for _, a := range a.Regexes { + allowlistRegexes = append(allowlistRegexes, regexp.MustCompile(a)) + } + var allowlistPaths []*regexp.Regexp + for _, a := range a.Paths { + allowlistPaths = append(allowlistPaths, regexp.MustCompile(a)) + } + + allowlist := &Allowlist{ + Description: a.Description, + MatchCondition: matchCondition, + Commits: a.Commits, + Paths: allowlistPaths, + RegexTarget: regexTarget, + Regexes: allowlistRegexes, + StopWords: a.StopWords, + } + if err := allowlist.Validate(); err != nil { + return nil, err + } + return allowlist, nil +} + +func (c *Config) GetOrderedRules() []Rule { + var orderedRules []Rule + for _, id := range c.OrderedRules { + if _, ok := c.Rules[id]; ok { + orderedRules = append(orderedRules, c.Rules[id]) + } + } + return orderedRules +} + +func (c *Config) extendDefault(parent *ViperConfig) error { + extendDepth++ + viper.SetConfigType("toml") + if err := viper.ReadConfig(strings.NewReader(DefaultConfig)); err != nil { + return fmt.Errorf("failed to load extended default config, err: %w", err) + } + defaultViperConfig := ViperConfig{} + if err := viper.Unmarshal(&defaultViperConfig); err != nil { + return fmt.Errorf("failed to load extended default config, err: %w", err) + } + cfg, err := defaultViperConfig.Translate() + if err != nil { + return fmt.Errorf("failed to load extended default config, err: %w", err) + + } + logging.Debug().Msg("extending config with default config") + c.extend(cfg) + return nil +} + +func (c *Config) extendPath(parent *ViperConfig) error { + extendDepth++ + viper.SetConfigFile(c.Extend.Path) + if err := viper.ReadInConfig(); err != nil { + return fmt.Errorf("failed to load extended config, err: %w", err) + } + extensionViperConfig := ViperConfig{} + if err := viper.Unmarshal(&extensionViperConfig); err != nil { + return fmt.Errorf("failed to load extended config, err: %w", err) + } + + extensionViperConfig.configPath = c.Extend.Path + logging.Debug().Msgf("extending config with %s", c.Extend.Path) + cfg, err := extensionViperConfig.Translate() + if err != nil { + return fmt.Errorf("failed to load extended config, err: %w", err) + } + c.extend(cfg) + return nil +} + +func (c *Config) extendURL() { + // TODO +} + +func (c *Config) extend(extensionConfig Config) { + // Get config name for helpful log messages. + var configName string + if c.Extend.Path != "" { + configName = c.Extend.Path + } else { + configName = "default" + } + // Convert |Config.DisabledRules| into a map for ease of access. + disabledRuleIDs := map[string]struct{}{} + for _, id := range c.Extend.DisabledRules { + if _, ok := extensionConfig.Rules[id]; !ok { + logging.Warn(). + Str("rule-id", id). + Str("config", configName). + Msg("Disabled rule doesn't exist in extended config.") + } + disabledRuleIDs[id] = struct{}{} + } + + for ruleID, baseRule := range extensionConfig.Rules { + // Skip the rule. + if _, ok := disabledRuleIDs[ruleID]; ok { + logging.Debug(). + Str("rule-id", ruleID). + Str("config", configName). + Msg("Ignoring rule from extended config.") + continue + } + + currentRule, ok := c.Rules[ruleID] + if !ok { + // Rule doesn't exist, add it to the config. + c.Rules[ruleID] = baseRule + for _, k := range baseRule.Keywords { + c.Keywords[k] = struct{}{} + } + c.OrderedRules = append(c.OrderedRules, ruleID) + } else { + // Rule exists, merge our changes into the base. + if currentRule.Description != "" { + baseRule.Description = currentRule.Description + } + if currentRule.Entropy != 0 { + baseRule.Entropy = currentRule.Entropy + } + if currentRule.SecretGroup != 0 { + baseRule.SecretGroup = currentRule.SecretGroup + } + if currentRule.Regex != nil { + baseRule.Regex = currentRule.Regex + } + if currentRule.Path != nil { + baseRule.Path = currentRule.Path + } + baseRule.Tags = append(baseRule.Tags, currentRule.Tags...) + baseRule.Keywords = append(baseRule.Keywords, currentRule.Keywords...) + baseRule.Allowlists = append(baseRule.Allowlists, currentRule.Allowlists...) + // The keywords from the base rule and the extended rule must be merged into the global keywords list + for _, k := range baseRule.Keywords { + c.Keywords[k] = struct{}{} + } + c.Rules[ruleID] = baseRule + } + } + + // append allowlists, not attempting to merge + c.Allowlists = append(c.Allowlists, extensionConfig.Allowlists...) + + // sort to keep extended rules in order + sort.Strings(c.OrderedRules) + return +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..8c7c920 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,672 @@ +package config + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zricethezav/gitleaks/v8/regexp" +) + +const configPath = "../testdata/config/" + +var regexComparer = func(x, y *regexp.Regexp) bool { + if x == nil || y == nil { + return x == y + } + return x.String() == y.String() +} + +type translateCase struct { + // Configuration file basename to load, from `../testdata/config/`. + cfgName string + // Expected result. + cfg Config + // Rules to compare. + rules []string + // Error to expect. + wantError error +} + +func TestTranslate(t *testing.T) { + tests := []translateCase{ + // Valid + { + cfgName: "generic", + cfg: Config{ + Title: "gitleaks config", + Rules: map[string]Rule{"generic-api-key": { + RuleID: "generic-api-key", + Description: "Generic API Key", + Regex: regexp.MustCompile(`(?i)(?:key|api|token|secret|client|passwd|password|auth|access)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:{1,3}=|\|\|:|<=|=>|:|\?=)(?:'|\"|\s|=|\x60){0,5}([0-9a-z\-_.=]{10,150})(?:['|\"|\n|\r|\s|\x60|;]|$)`), + Entropy: 3.5, + Keywords: []string{"key", "api", "token", "secret", "client", "passwd", "password", "auth", "access"}, + Tags: []string{}, + }}, + }, + }, + { + cfgName: "valid/rule_path_only", + cfg: Config{ + Rules: map[string]Rule{"python-files-only": { + RuleID: "python-files-only", + Description: "Python Files", + Path: regexp.MustCompile(`.py`), + Keywords: []string{}, + Tags: []string{}, + }}, + }, + }, + { + cfgName: "valid/rule_regex_escaped_character_group", + cfg: Config{ + Rules: map[string]Rule{"pypi-upload-token": { + RuleID: "pypi-upload-token", + Description: "PyPI upload token", + Regex: regexp.MustCompile(`pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\-_]{50,1000}`), + Keywords: []string{}, + Tags: []string{"key", "pypi"}, + }}, + }, + }, + { + cfgName: "valid/rule_entropy_group", + cfg: Config{ + Rules: map[string]Rule{"discord-api-key": { + RuleID: "discord-api-key", + Description: "Discord API key", + Regex: regexp.MustCompile(`(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{64})['\"]`), + Entropy: 3.5, + SecretGroup: 3, + Keywords: []string{}, + Tags: []string{}, + }}, + }, + }, + + // Invalid + { + cfgName: "invalid/rule_missing_id", + cfg: Config{}, + wantError: errors.New("rule |id| is missing or empty, description: Discord API key, regex: (?i)(discord[a-z0-9_ .\\-,]{0,25})(=|>|:=|\\|\\|:|<=|=>|:).{0,5}['\\\"]([a-h0-9]{64})['\\\"]"), + }, + { + cfgName: "invalid/rule_no_regex_or_path", + cfg: Config{}, + wantError: errors.New("discord-api-key: both |regex| and |path| are empty, this rule will have no effect"), + }, + { + cfgName: "invalid/rule_bad_entropy_group", + cfg: Config{}, + wantError: errors.New("discord-api-key: invalid regex secret group 5, max regex secret group 3"), + }, + } + for _, tt := range tests { + t.Run(tt.cfgName, func(t *testing.T) { + testTranslate(t, tt) + }) + } +} + +func TestTranslateAllowlists(t *testing.T) { + tests := []translateCase{ + // Global + { + cfgName: "valid/allowlist_global_old_compat", + cfg: Config{ + Rules: map[string]Rule{}, + Allowlists: []*Allowlist{ + { + StopWords: []string{"0989c462-69c9-49fa-b7d2-30dc5c576a97"}, + }, + }, + }, + }, + { + cfgName: "valid/allowlist_global_multiple", + cfg: Config{ + Rules: map[string]Rule{ + "test": { + RuleID: "test", + Regex: regexp.MustCompile(`token = "(.+)"`), + Keywords: []string{}, + Tags: []string{}, + }, + }, + Allowlists: []*Allowlist{ + { + Regexes: []*regexp.Regexp{regexp.MustCompile("^changeit$")}, + }, + { + MatchCondition: AllowlistMatchAnd, + Paths: []*regexp.Regexp{regexp.MustCompile("^node_modules/.*")}, + StopWords: []string{"mock"}, + }, + }, + }, + }, + { + cfgName: "valid/allowlist_global_target_rules", + cfg: Config{ + Rules: map[string]Rule{ + "github-app-token": { + RuleID: "github-app-token", + Regex: regexp.MustCompile(`(?:ghu|ghs)_[0-9a-zA-Z]{36}`), + Tags: []string{}, + Keywords: []string{}, + Allowlists: []*Allowlist{ + { + Paths: []*regexp.Regexp{regexp.MustCompile(`(?:^|/)@octokit/auth-token/README\.md$`)}, + }, + }, + }, + "github-oauth": { + RuleID: "github-oauth", + Regex: regexp.MustCompile(`gho_[0-9a-zA-Z]{36}`), + Tags: []string{}, + Keywords: []string{}, + Allowlists: nil, + }, + "github-pat": { + RuleID: "github-pat", + Regex: regexp.MustCompile(`ghp_[0-9a-zA-Z]{36}`), + Tags: []string{}, + Keywords: []string{}, + Allowlists: []*Allowlist{ + { + Paths: []*regexp.Regexp{regexp.MustCompile(`(?:^|/)@octokit/auth-token/README\.md$`)}, + }, + }, + }, + }, + Allowlists: []*Allowlist{ + { + Regexes: []*regexp.Regexp{regexp.MustCompile(".*fake.*")}, + }, + }, + }, + }, + { + cfgName: "valid/allowlist_global_regex", + cfg: Config{ + Rules: map[string]Rule{}, + Allowlists: []*Allowlist{ + { + MatchCondition: AllowlistMatchOr, + Regexes: []*regexp.Regexp{regexp.MustCompile("AKIALALEM.L33243OLIA")}, + }, + }, + }, + }, + { + cfgName: "invalid/allowlist_global_empty", + cfg: Config{}, + wantError: errors.New("[[allowlists]] must contain at least one check for: commits, paths, regexes, or stopwords"), + }, + { + cfgName: "invalid/allowlist_global_old_and_new", + cfg: Config{}, + wantError: errors.New("[allowlist] is deprecated, it cannot be used alongside [[allowlists]]"), + }, + { + cfgName: "invalid/allowlist_global_target_rule_id", + cfg: Config{}, + wantError: errors.New("[[allowlists]] target rule ID 'github-pat' does not exist"), + }, + { + cfgName: "invalid/allowlist_global_regextarget", + cfg: Config{}, + wantError: errors.New("[[allowlists]] unknown allowlist |regexTarget| 'mtach' (expected 'match', 'line')"), + }, + + // Rule + { + cfgName: "valid/allowlist_rule_old_compat", + cfg: Config{ + Rules: map[string]Rule{"example": { + RuleID: "example", + Regex: regexp.MustCompile(`example\d+`), + Tags: []string{}, + Keywords: []string{}, + Allowlists: []*Allowlist{ + { + MatchCondition: AllowlistMatchOr, + Regexes: []*regexp.Regexp{regexp.MustCompile("123")}, + }, + }, + }}, + }, + }, + { + cfgName: "valid/allowlist_rule_regex", + cfg: Config{ + Title: "simple config with allowlist for aws", + Rules: map[string]Rule{"aws-access-key": { + RuleID: "aws-access-key", + Description: "AWS Access Key", + Regex: regexp.MustCompile("(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}"), + Keywords: []string{}, + Tags: []string{"key", "AWS"}, + Allowlists: []*Allowlist{ + { + MatchCondition: AllowlistMatchOr, + Regexes: []*regexp.Regexp{regexp.MustCompile("AKIALALEMEL33243OLIA")}, + }, + }, + }}, + }, + }, + { + cfgName: "valid/allowlist_rule_commit", + cfg: Config{ + Title: "simple config with allowlist for a specific commit", + Rules: map[string]Rule{"aws-access-key": { + RuleID: "aws-access-key", + Description: "AWS Access Key", + Regex: regexp.MustCompile("(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}"), + Keywords: []string{}, + Tags: []string{"key", "AWS"}, + Allowlists: []*Allowlist{ + { + MatchCondition: AllowlistMatchOr, + Commits: []string{"allowthiscommit"}, + }, + }, + }}, + }, + }, + { + cfgName: "valid/allowlist_rule_path", + cfg: Config{ + Title: "simple config with allowlist for .go files", + Rules: map[string]Rule{"aws-access-key": { + RuleID: "aws-access-key", + Description: "AWS Access Key", + Regex: regexp.MustCompile("(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}"), + Keywords: []string{}, + Tags: []string{"key", "AWS"}, + Allowlists: []*Allowlist{ + { + MatchCondition: AllowlistMatchOr, + Paths: []*regexp.Regexp{regexp.MustCompile(".go")}, + }, + }, + }}, + }, + }, + { + cfgName: "invalid/allowlist_rule_empty", + cfg: Config{}, + wantError: errors.New("example: [[rules.allowlists]] must contain at least one check for: commits, paths, regexes, or stopwords"), + }, + { + cfgName: "invalid/allowlist_rule_old_and_new", + cfg: Config{}, + wantError: errors.New("example: [rules.allowlist] is deprecated, it cannot be used alongside [[rules.allowlist]]"), + }, + { + cfgName: "invalid/allowlist_rule_regextarget", + cfg: Config{}, + wantError: errors.New("example: [[rules.allowlists]] unknown allowlist |regexTarget| 'mtach' (expected 'match', 'line')"), + }, + } + + for _, tt := range tests { + t.Run(tt.cfgName, func(t *testing.T) { + testTranslate(t, tt) + }) + } +} + +func TestTranslateExtend(t *testing.T) { + tests := []translateCase{ + // Valid + { + cfgName: "valid/extend", + cfg: Config{ + Rules: map[string]Rule{ + "aws-access-key": { + RuleID: "aws-access-key", + Description: "AWS Access Key", + Regex: regexp.MustCompile("(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}"), + Keywords: []string{}, + Tags: []string{"key", "AWS"}, + }, + "aws-secret-key": { + RuleID: "aws-secret-key", + Description: "AWS Secret Key", + Regex: regexp.MustCompile(`(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}`), + Keywords: []string{}, + Tags: []string{"key", "AWS"}, + }, + "aws-secret-key-again": { + RuleID: "aws-secret-key-again", + Description: "AWS Secret Key", + Regex: regexp.MustCompile(`(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}`), + Keywords: []string{}, + Tags: []string{"key", "AWS"}, + }, + }, + }, + }, + { + cfgName: "valid/extend_disabled", + cfg: Config{ + Title: "gitleaks extend disable", + Rules: map[string]Rule{ + "aws-secret-key": { + RuleID: "aws-secret-key", + Regex: regexp.MustCompile(`(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}`), + Tags: []string{"key", "AWS"}, + Keywords: []string{}, + }, + "pypi-upload-token": { + RuleID: "pypi-upload-token", + Regex: regexp.MustCompile(`pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\-_]{50,1000}`), + Tags: []string{}, + Keywords: []string{}, + }, + }, + }, + }, + { + cfgName: "valid/extend_rule_no_regexpath", + cfg: Config{ + Rules: map[string]Rule{ + "aws-secret-key-again-again": { + RuleID: "aws-secret-key-again-again", + Description: "AWS Secret Key", + Regex: regexp.MustCompile(`(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}`), + Keywords: []string{}, + Tags: []string{"key", "AWS"}, + Allowlists: []*Allowlist{ + { + Description: "False positive. Keys used for colors match the rule, and should be excluded.", + MatchCondition: AllowlistMatchOr, + Paths: []*regexp.Regexp{regexp.MustCompile(`something.py`)}, + }, + }, + }, + }, + }, + }, + { + cfgName: "valid/extend_rule_override_description", + rules: []string{"aws-access-key"}, + cfg: Config{ + Title: "override a built-in rule's description", + Rules: map[string]Rule{"aws-access-key": { + RuleID: "aws-access-key", + Description: "Puppy Doggy", + Regex: regexp.MustCompile("(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}"), + Keywords: []string{}, + Tags: []string{"key", "AWS"}, + }, + }, + }, + }, + { + cfgName: "valid/extend_rule_override_path", + rules: []string{"aws-access-key"}, + cfg: Config{ + Title: "override a built-in rule's path", + Rules: map[string]Rule{"aws-access-key": { + RuleID: "aws-access-key", + Description: "AWS Access Key", + Regex: regexp.MustCompile("(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}"), + Path: regexp.MustCompile("(?:puppy)"), + Keywords: []string{}, + Tags: []string{"key", "AWS"}, + }, + }, + }, + }, + { + cfgName: "valid/extend_rule_override_regex", + rules: []string{"aws-access-key"}, + cfg: Config{ + Title: "override a built-in rule's regex", + Rules: map[string]Rule{"aws-access-key": { + RuleID: "aws-access-key", + Description: "AWS Access Key", + Regex: regexp.MustCompile("(?:a)"), + Keywords: []string{}, + Tags: []string{"key", "AWS"}, + }, + }, + }, + }, + { + cfgName: "valid/extend_rule_override_secret_group", + rules: []string{"aws-access-key"}, + cfg: Config{ + Title: "override a built-in rule's secretGroup", + Rules: map[string]Rule{"aws-access-key": { + RuleID: "aws-access-key", + Description: "AWS Access Key", + Regex: regexp.MustCompile("(a)(a)"), + SecretGroup: 2, + Keywords: []string{}, + Tags: []string{"key", "AWS"}, + }, + }, + }, + }, + { + cfgName: "valid/extend_rule_override_entropy", + rules: []string{"aws-access-key"}, + cfg: Config{ + Title: "override a built-in rule's entropy", + Rules: map[string]Rule{"aws-access-key": { + RuleID: "aws-access-key", + Description: "AWS Access Key", + Regex: regexp.MustCompile("(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}"), + Entropy: 999.0, + Keywords: []string{}, + Tags: []string{"key", "AWS"}, + }, + }, + }, + }, + { + cfgName: "valid/extend_rule_override_keywords", + rules: []string{"aws-access-key"}, + cfg: Config{ + Title: "override a built-in rule's keywords", + Rules: map[string]Rule{"aws-access-key": { + RuleID: "aws-access-key", + Description: "AWS Access Key", + Regex: regexp.MustCompile("(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}"), + Keywords: []string{"puppy"}, + Tags: []string{"key", "AWS"}, + }, + }, + }, + }, + { + cfgName: "valid/extend_rule_override_tags", + rules: []string{"aws-access-key"}, + cfg: Config{ + Title: "override a built-in rule's tags", + Rules: map[string]Rule{"aws-access-key": { + RuleID: "aws-access-key", + Description: "AWS Access Key", + Regex: regexp.MustCompile("(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}"), + Keywords: []string{}, + Tags: []string{"key", "AWS", "puppy"}, + }, + }, + }, + }, + { + cfgName: "valid/extend_rule_allowlist_or", + cfg: Config{ + Title: "gitleaks extended 3", + Rules: map[string]Rule{ + "aws-secret-key-again-again": { + RuleID: "aws-secret-key-again-again", + Description: "AWS Secret Key", + Regex: regexp.MustCompile(`(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}`), + Keywords: []string{}, + Tags: []string{"key", "AWS"}, + Allowlists: []*Allowlist{ + { + MatchCondition: AllowlistMatchOr, + StopWords: []string{"fake"}, + }, + { + MatchCondition: AllowlistMatchOr, + Commits: []string{"abcdefg1"}, + Paths: []*regexp.Regexp{regexp.MustCompile(`ignore\.xaml`)}, + Regexes: []*regexp.Regexp{regexp.MustCompile(`foo.+bar`)}, + RegexTarget: "line", + StopWords: []string{"example"}, + }, + }, + }, + }, + }, + }, + { + cfgName: "valid/extend_rule_allowlist_and", + cfg: Config{ + Title: "gitleaks extended 3", + Rules: map[string]Rule{ + "aws-secret-key-again-again": { + RuleID: "aws-secret-key-again-again", + Description: "AWS Secret Key", + Regex: regexp.MustCompile(`(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}`), + Keywords: []string{}, + Tags: []string{"key", "AWS"}, + Allowlists: []*Allowlist{ + { + MatchCondition: AllowlistMatchOr, + StopWords: []string{"fake"}, + }, + { + MatchCondition: AllowlistMatchAnd, + Commits: []string{"abcdefg1"}, + Paths: []*regexp.Regexp{regexp.MustCompile(`ignore\.xaml`)}, + Regexes: []*regexp.Regexp{regexp.MustCompile(`foo.+bar`)}, + RegexTarget: "line", + StopWords: []string{"example"}, + }, + }, + }, + }, + }, + }, + + // Invalid + { + cfgName: "invalid/extend_invalid_ruleid", + wantError: errors.New("rule |id| is missing or empty"), + }, + } + + for _, tt := range tests { + t.Run(tt.cfgName, func(t *testing.T) { + testTranslate(t, tt) + }) + } +} + +func testTranslate(t *testing.T, test translateCase) { + t.Helper() + t.Cleanup(func() { + extendDepth = 0 + viper.Reset() + }) + + viper.AddConfigPath(configPath) + viper.SetConfigName(test.cfgName) + viper.SetConfigType("toml") + err := viper.ReadInConfig() + require.NoError(t, err) + + var vc ViperConfig + err = viper.Unmarshal(&vc) + require.NoError(t, err) + cfg, err := vc.Translate() + if err != nil { + if test.wantError != nil { + assert.EqualError(t, err, test.wantError.Error()) + } else { + require.NoError(t, err) + } + } else { + if test.wantError != nil { + t.Fatalf("expected error but got none: %v", test.wantError) + return + } + } + + if len(test.rules) > 0 { + rules := make(map[string]Rule) + for _, name := range test.rules { + rules[name] = cfg.Rules[name] + } + cfg.Rules = rules + } + + opts := cmp.Options{ + cmp.Comparer(regexComparer), + cmpopts.IgnoreUnexported(Rule{}, Allowlist{}), + } + if diff := cmp.Diff(test.cfg.Title, cfg.Title); diff != "" { + t.Errorf("%s diff: (-want +got)\n%s", test.cfgName, diff) + } + if diff := cmp.Diff(test.cfg.Rules, cfg.Rules, opts); diff != "" { + t.Errorf("%s diff: (-want +got)\n%s", test.cfgName, diff) + } + if diff := cmp.Diff(test.cfg.Allowlists, cfg.Allowlists, opts); diff != "" { + t.Errorf("%s diff: (-want +got)\n%s", test.cfgName, diff) + } +} + +func TestExtendedRuleKeywordsAreDowncase(t *testing.T) { + tests := []struct { + name string + cfgName string + expectedKeywords string + }{ + { + name: "Extend base rule that includes AWS keyword with new attribute", + cfgName: "valid/extend_base_rule_including_keywords_with_attribute", + expectedKeywords: "aws", + }, + { + name: "Extend base with a new rule with CMS keyword", + cfgName: "valid/extend_rule_new", + expectedKeywords: "cms", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(func() { + viper.Reset() + }) + + viper.AddConfigPath(configPath) + viper.SetConfigName(tt.cfgName) + viper.SetConfigType("toml") + err := viper.ReadInConfig() + require.NoError(t, err) + + var vc ViperConfig + err = viper.Unmarshal(&vc) + require.NoError(t, err) + cfg, err := vc.Translate() + require.NoError(t, err) + + _, exists := cfg.Keywords[tt.expectedKeywords] + require.Truef(t, exists, "The expected keyword %s did not exist as a key of cfg.Keywords", tt.expectedKeywords) + }) + } +} diff --git a/config/gitleaks.toml b/config/gitleaks.toml new file mode 100644 index 0000000..256f647 --- /dev/null +++ b/config/gitleaks.toml @@ -0,0 +1,3209 @@ +# This file has been auto-generated. Do not edit manually. +# If you would like to contribute new rules, please use +# cmd/generate/config/main.go and follow the contributing guidelines +# at https://github.com/gitleaks/gitleaks/blob/master/CONTRIBUTING.md +# +# How the hell does secret scanning work? Read this: +# https://lookingatcomputer.substack.com/p/regex-is-almost-all-you-need +# +# This is the default gitleaks configuration file. +# Rules and allowlists are defined within this file. +# Rules instruct gitleaks on what should be considered a secret. +# Allowlists instruct gitleaks on what is allowed, i.e. not a secret. + +title = "gitleaks config" + +# minVersion indicates the minimum Gitleaks version required to use this config. +# If the running version is older, a warning will be logged and not all +# config-enabled features are guaranteed to work. +minVersion = "v8.25.0" + +# TODO: change to [[allowlists]] +[allowlist] +description = "global allow lists" +paths = [ + '''gitleaks\.toml''', + '''(?i)\.(?:bmp|gif|jpe?g|png|svg|tiff?)$''', + '''(?i)\.(?:eot|[ot]tf|woff2?)$''', + '''(?i)\.(?:docx?|xlsx?|pdf|bin|socket|vsidx|v2|suo|wsuo|.dll|pdb|exe|gltf)$''', + '''go\.(?:mod|sum|work(?:\.sum)?)$''', + '''(?:^|/)vendor/modules\.txt$''', + '''(?:^|/)vendor/(?:github\.com|golang\.org/x|google\.golang\.org|gopkg\.in|istio\.io|k8s\.io|sigs\.k8s\.io)(?:/.*)?$''', + '''(?:^|/)gradlew(?:\.bat)?$''', + '''(?:^|/)gradle\.lockfile$''', + '''(?:^|/)mvnw(?:\.cmd)?$''', + '''(?:^|/)\.mvn/wrapper/MavenWrapperDownloader\.java$''', + '''(?:^|/)node_modules(?:/.*)?$''', + '''(?:^|/)(?:deno\.lock|npm-shrinkwrap\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$''', + '''(?:^|/)bower_components(?:/.*)?$''', + '''(?:^|/)(?:angular|bootstrap|jquery(?:-?ui)?|plotly|swagger-?ui)[a-zA-Z0-9.-]*(?:\.min)?\.js(?:\.map)?$''', + '''(?:^|/)javascript\.json$''', + '''(?:^|/)(?:Pipfile|poetry)\.lock$''', + '''(?i)(?:^|/)(?:v?env|virtualenv)/lib(?:64)?(?:/.*)?$''', + '''(?i)(?:^|/)(?:lib(?:64)?/python[23](?:\.\d{1,2})+|python/[23](?:\.\d{1,2})+/lib(?:64)?)(?:/.*)?$''', + '''(?i)(?:^|/)[a-z0-9_.]+-[0-9.]+\.dist-info(?:/.+)?$''', + '''(?:^|/)vendor/(?:bundle|ruby)(?:/.*?)?$''', + '''\.gem$''', + '''verification-metadata\.xml''', + '''Database.refactorlog''', + '''(?:^|/)\.git$''', +] +regexes = [ + '''(?i)^true|false|null$''', + '''^(?i:a+|b+|c+|d+|e+|f+|g+|h+|i+|j+|k+|l+|m+|n+|o+|p+|q+|r+|s+|t+|u+|v+|w+|x+|y+|z+|\*+|\.+)$''', + '''^\$(?:\d+|{\d+})$''', + '''^\$(?:[A-Z_]+|[a-z_]+)$''', + '''^\${(?:[A-Z_]+|[a-z_]+)}$''', + '''^\{\{[ \t]*[\w ().|]+[ \t]*}}$''', + '''^\$\{\{[ \t]*(?:(?:env|github|secrets|vars)(?:\.[A-Za-z]\w+)+[\w "'&./=|]*)[ \t]*}}$''', + '''^%(?:[A-Z_]+|[a-z_]+)%$''', + '''^%[+\-# 0]?[bcdeEfFgGoOpqstTUvxX]$''', + '''^\{\d{0,2}}$''', + '''^@(?:[A-Z_]+|[a-z_]+)@$''', + '''^/Users/(?i)[a-z0-9]+/[\w .-/]+$''', + '''^/(?:bin|etc|home|opt|tmp|usr|var)/[\w ./-]+$''', +] +stopwords = [ + "014df517-39d1-4453-b7b3-9930c563627c", + "abcdefghijklmnopqrstuvwxyz", +] + +[[rules]] +id = "1password-secret-key" +description = "Uncovered a possible 1Password secret key, potentially compromising access to secrets in vaults." +regex = '''\bA3-[A-Z0-9]{6}-(?:(?:[A-Z0-9]{11})|(?:[A-Z0-9]{6}-[A-Z0-9]{5}))-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}\b''' +entropy = 3.8 +keywords = ["a3-"] + +[[rules]] +id = "1password-service-account-token" +description = "Uncovered a possible 1Password service account token, potentially compromising access to secrets in vaults." +regex = '''ops_eyJ[a-zA-Z0-9+/]{250,}={0,3}''' +entropy = 4 +keywords = ["ops_"] + +[[rules]] +id = "adafruit-api-key" +description = "Identified a potential Adafruit API Key, which could lead to unauthorized access to Adafruit services and sensitive data exposure." +regex = '''(?i)[\w.-]{0,50}?(?:adafruit)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["adafruit"] + +[[rules]] +id = "adobe-client-id" +description = "Detected a pattern that resembles an Adobe OAuth Web Client ID, posing a risk of compromised Adobe integrations and data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:adobe)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["adobe"] + +[[rules]] +id = "adobe-client-secret" +description = "Discovered a potential Adobe Client Secret, which, if exposed, could allow unauthorized Adobe service access and data manipulation." +regex = '''\b(p8e-(?i)[a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["p8e-"] + +[[rules]] +id = "age-secret-key" +description = "Discovered a potential Age encryption tool secret key, risking data decryption and unauthorized access to sensitive information." +regex = '''AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}''' +keywords = ["age-secret-key-1"] + +[[rules]] +id = "airtable-api-key" +description = "Uncovered a possible Airtable API Key, potentially compromising database access and leading to data leakage or alteration." +regex = '''(?i)[\w.-]{0,50}?(?:airtable)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{17})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["airtable"] + +[[rules]] +id = "airtable-personnal-access-token" +description = "Uncovered a possible Airtable Personal AccessToken, potentially compromising database access and leading to data leakage or alteration." +regex = '''\b(pat[[:alnum:]]{14}\.[a-f0-9]{64})\b''' +keywords = ["airtable"] + +[[rules]] +id = "algolia-api-key" +description = "Identified an Algolia API Key, which could result in unauthorized search operations and data exposure on Algolia-managed platforms." +regex = '''(?i)[\w.-]{0,50}?(?:algolia)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["algolia"] + +[[rules]] +id = "alibaba-access-key-id" +description = "Detected an Alibaba Cloud AccessKey ID, posing a risk of unauthorized cloud resource access and potential data compromise." +regex = '''\b(LTAI(?i)[a-z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["ltai"] + +[[rules]] +id = "alibaba-secret-key" +description = "Discovered a potential Alibaba Cloud Secret Key, potentially allowing unauthorized operations and data access within Alibaba Cloud." +regex = '''(?i)[\w.-]{0,50}?(?:alibaba)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{30})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["alibaba"] + +[[rules]] +id = "anthropic-admin-api-key" +description = "Detected an Anthropic Admin API Key, risking unauthorized access to administrative functions and sensitive AI model configurations." +regex = '''\b(sk-ant-admin01-[a-zA-Z0-9_\-]{93}AA)(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["sk-ant-admin01"] + +[[rules]] +id = "anthropic-api-key" +description = "Identified an Anthropic API Key, which may compromise AI assistant integrations and expose sensitive data to unauthorized access." +regex = '''\b(sk-ant-api03-[a-zA-Z0-9_\-]{93}AA)(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["sk-ant-api03"] + +[[rules]] +id = "artifactory-api-key" +description = "Detected an Artifactory api key, posing a risk unauthorized access to the central repository." +regex = '''\bAKCp[A-Za-z0-9]{69}\b''' +entropy = 4.5 +keywords = ["akcp"] + +[[rules]] +id = "artifactory-reference-token" +description = "Detected an Artifactory reference token, posing a risk of impersonation and unauthorized access to the central repository." +regex = '''\bcmVmd[A-Za-z0-9]{59}\b''' +entropy = 4.5 +keywords = ["cmvmd"] + +[[rules]] +id = "asana-client-id" +description = "Discovered a potential Asana Client ID, risking unauthorized access to Asana projects and sensitive task information." +regex = '''(?i)[\w.-]{0,50}?(?:asana)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["asana"] + +[[rules]] +id = "asana-client-secret" +description = "Identified an Asana Client Secret, which could lead to compromised project management integrity and unauthorized access." +regex = '''(?i)[\w.-]{0,50}?(?:asana)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["asana"] + +[[rules]] +id = "atlassian-api-token" +description = "Detected an Atlassian API token, posing a threat to project management and collaboration tool security and data confidentiality." +regex = '''(?i)[\w.-]{0,50}?(?:(?-i:ATLASSIAN|[Aa]tlassian)|(?-i:CONFLUENCE|[Cc]onfluence)|(?-i:JIRA|[Jj]ira))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{20}[a-f0-9]{4})(?:[\x60'"\s;]|\\[nr]|$)|\b(ATATT3[A-Za-z0-9_\-=]{186})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3.5 +keywords = [ + "atlassian", + "confluence", + "jira", + "atatt3", +] + +[[rules]] +id = "authress-service-client-access-key" +description = "Uncovered a possible Authress Service Client Access Key, which may compromise access control services and sensitive data." +regex = '''\b((?:sc|ext|scauth|authress)_(?i)[a-z0-9]{5,30}\.[a-z0-9]{4,6}\.(?-i:acc)[_-][a-z0-9-]{10,32}\.[a-z0-9+/_=-]{30,120})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = [ + "sc_", + "ext_", + "scauth_", + "authress_", +] + +[[rules]] +id = "aws-access-token" +description = "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms." +regex = '''\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\b''' +entropy = 3 +keywords = [ + "a3t", + "akia", + "asia", + "abia", + "acca", +] +[[rules.allowlists]] +regexes = [ + '''.+EXAMPLE$''', +] + +[[rules]] +id = "aws-amazon-bedrock-api-key-long-lived" +description = "Identified a pattern that may indicate long-lived Amazon Bedrock API keys, risking unauthorized Amazon Bedrock usage" +regex = '''\b(ABSK[A-Za-z0-9+/]{109,269}={0,2})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["absk"] + +[[rules]] +id = "aws-amazon-bedrock-api-key-short-lived" +description = "Identified a pattern that may indicate short-lived Amazon Bedrock API keys, risking unauthorized Amazon Bedrock usage" +regex = '''bedrock-api-key-YmVkcm9jay5hbWF6b25hd3MuY29t''' +entropy = 3 +keywords = ["bedrock-api-key-"] + +[[rules]] +id = "azure-ad-client-secret" +description = "Azure AD Client Secret" +regex = '''(?:^|[\\'"\x60\s>=:(,)])([a-zA-Z0-9_~.]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\\'"\x60\s<),])''' +entropy = 3 +keywords = ["q~"] + +[[rules]] +id = "beamer-api-token" +description = "Detected a Beamer API token, potentially compromising content management and exposing sensitive notifications and updates." +regex = '''(?i)[\w.-]{0,50}?(?:beamer)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(b_[a-z0-9=_\-]{44})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["beamer"] + +[[rules]] +id = "bitbucket-client-id" +description = "Discovered a potential Bitbucket Client ID, risking unauthorized repository access and potential codebase exposure." +regex = '''(?i)[\w.-]{0,50}?(?:bitbucket)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["bitbucket"] + +[[rules]] +id = "bitbucket-client-secret" +description = "Discovered a potential Bitbucket Client Secret, posing a risk of compromised code repositories and unauthorized access." +regex = '''(?i)[\w.-]{0,50}?(?:bitbucket)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["bitbucket"] + +[[rules]] +id = "bittrex-access-key" +description = "Identified a Bittrex Access Key, which could lead to unauthorized access to cryptocurrency trading accounts and financial loss." +regex = '''(?i)[\w.-]{0,50}?(?:bittrex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["bittrex"] + +[[rules]] +id = "bittrex-secret-key" +description = "Detected a Bittrex Secret Key, potentially compromising cryptocurrency transactions and financial security." +regex = '''(?i)[\w.-]{0,50}?(?:bittrex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["bittrex"] + +[[rules]] +id = "cisco-meraki-api-key" +description = "Cisco Meraki is a cloud-managed IT solution that provides networking, security, and device management through an easy-to-use interface." +regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:(?-i:[Mm]eraki|MERAKI))(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["meraki"] + +[[rules]] +id = "clickhouse-cloud-api-secret-key" +description = "Identified a pattern that may indicate clickhouse cloud API secret key, risking unauthorized clickhouse cloud api access and data breaches on ClickHouse Cloud platforms." +regex = '''\b(4b1d[A-Za-z0-9]{38})\b''' +entropy = 3 +keywords = ["4b1d"] + +[[rules]] +id = "clojars-api-token" +description = "Uncovered a possible Clojars API token, risking unauthorized access to Clojure libraries and potential code manipulation." +regex = '''(?i)CLOJARS_[a-z0-9]{60}''' +entropy = 2 +keywords = ["clojars_"] + +[[rules]] +id = "cloudflare-api-key" +description = "Detected a Cloudflare API Key, potentially compromising cloud application deployments and operational security." +regex = '''(?i)[\w.-]{0,50}?(?:cloudflare)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["cloudflare"] + +[[rules]] +id = "cloudflare-global-api-key" +description = "Detected a Cloudflare Global API Key, potentially compromising cloud application deployments and operational security." +regex = '''(?i)[\w.-]{0,50}?(?:cloudflare)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{37})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["cloudflare"] + +[[rules]] +id = "cloudflare-origin-ca-key" +description = "Detected a Cloudflare Origin CA Key, potentially compromising cloud application deployments and operational security." +regex = '''\b(v1\.0-[a-f0-9]{24}-[a-f0-9]{146})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = [ + "cloudflare", + "v1.0-", +] + +[[rules]] +id = "codecov-access-token" +description = "Found a pattern resembling a Codecov Access Token, posing a risk of unauthorized access to code coverage reports and sensitive data." +regex = '''(?i)[\w.-]{0,50}?(?:codecov)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["codecov"] + +[[rules]] +id = "cohere-api-token" +description = "Identified a Cohere Token, posing a risk of unauthorized access to AI services and data manipulation." +regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:cohere|CO_API_KEY)(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-zA-Z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = [ + "cohere", + "co_api_key", +] + +[[rules]] +id = "coinbase-access-token" +description = "Detected a Coinbase Access Token, posing a risk of unauthorized access to cryptocurrency accounts and financial transactions." +regex = '''(?i)[\w.-]{0,50}?(?:coinbase)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["coinbase"] + +[[rules]] +id = "confluent-access-token" +description = "Identified a Confluent Access Token, which could compromise access to streaming data platforms and sensitive data flow." +regex = '''(?i)[\w.-]{0,50}?(?:confluent)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["confluent"] + +[[rules]] +id = "confluent-secret-key" +description = "Found a Confluent Secret Key, potentially risking unauthorized operations and data access within Confluent services." +regex = '''(?i)[\w.-]{0,50}?(?:confluent)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["confluent"] + +[[rules]] +id = "contentful-delivery-api-token" +description = "Discovered a Contentful delivery API token, posing a risk to content management systems and data integrity." +regex = '''(?i)[\w.-]{0,50}?(?:contentful)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{43})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["contentful"] + +[[rules]] +id = "curl-auth-header" +description = "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource." +regex = '''\bcurl\b(?:.*?|.*?(?:[\r\n]{1,2}.*?){1,5})[ \t\n\r](?:-H|--header)(?:=|[ \t]{0,5})(?:"(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \t]{0,5}([\w=~@.+/-]{8,}))"|'(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \t]{0,5}([\w=~@.+/-]{8,}))')(?:\B|\s|\z)''' +entropy = 2.75 +keywords = ["curl"] + +[[rules]] +id = "curl-auth-user" +description = "Discovered a potential basic authorization token provided in a curl command, which could compromise the curl accessed resource." +regex = '''\bcurl\b(?:.*|.*(?:[\r\n]{1,2}.*){1,5})[ \t\n\r](?:-u|--user)(?:=|[ \t]{0,5})("(:[^"]{3,}|[^:"]{3,}:|[^:"]{3,}:[^"]{3,})"|'([^:']{3,}:[^']{3,})'|((?:"[^"]{3,}"|'[^']{3,}'|[\w$@.-]+):(?:"[^"]{3,}"|'[^']{3,}'|[\w${}@.-]+)))(?:\s|\z)''' +entropy = 2 +keywords = ["curl"] +[[rules.allowlists]] +regexes = [ + '''[^:]+:(?:change(?:it|me)|pass(?:word)?|pwd|test|token|\*+|x+)''', + '''['"]?<[^>]+>['"]?:['"]?<[^>]+>|<[^:]+:[^>]+>['"]?''', + '''[^:]+:\[[^]]+]''', + '''['"]?[^:]+['"]?:['"]?\$(?:\d|\w+|\{(?:\d|\w+)})['"]?''', + '''\$\([^)]+\):\$\([^)]+\)''', + '''['"]?\$?{{[^}]+}}['"]?:['"]?\$?{{[^}]+}}['"]?''', +] + +[[rules]] +id = "databricks-api-token" +description = "Uncovered a Databricks API token, which may compromise big data analytics platforms and sensitive data processing." +regex = '''\b(dapi[a-f0-9]{32}(?:-\d)?)(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["dapi"] + +[[rules]] +id = "datadog-access-token" +description = "Detected a Datadog Access Token, potentially risking monitoring and analytics data exposure and manipulation." +regex = '''(?i)[\w.-]{0,50}?(?:datadog)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["datadog"] + +[[rules]] +id = "defined-networking-api-token" +description = "Identified a Defined Networking API token, which could lead to unauthorized network operations and data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:dnkey)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(dnkey-[a-z0-9=_\-]{26}-[a-z0-9=_\-]{52})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["dnkey"] + +[[rules]] +id = "digitalocean-access-token" +description = "Found a DigitalOcean OAuth Access Token, risking unauthorized cloud resource access and data compromise." +regex = '''\b(doo_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["doo_v1_"] + +[[rules]] +id = "digitalocean-pat" +description = "Discovered a DigitalOcean Personal Access Token, posing a threat to cloud infrastructure security and data privacy." +regex = '''\b(dop_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["dop_v1_"] + +[[rules]] +id = "digitalocean-refresh-token" +description = "Uncovered a DigitalOcean OAuth Refresh Token, which could allow prolonged unauthorized access and resource manipulation." +regex = '''(?i)\b(dor_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["dor_v1_"] + +[[rules]] +id = "discord-api-token" +description = "Detected a Discord API key, potentially compromising communication channels and user data privacy on Discord." +regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["discord"] + +[[rules]] +id = "discord-client-id" +description = "Identified a Discord client ID, which may lead to unauthorized integrations and data exposure in Discord applications." +regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9]{18})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["discord"] + +[[rules]] +id = "discord-client-secret" +description = "Discovered a potential Discord client secret, risking compromised Discord bot integrations and data leaks." +regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["discord"] + +[[rules]] +id = "doppler-api-token" +description = "Discovered a Doppler API token, posing a risk to environment and secrets management security." +regex = '''dp\.pt\.(?i)[a-z0-9]{43}''' +entropy = 2 +keywords = ["dp.pt."] + +[[rules]] +id = "droneci-access-token" +description = "Detected a Droneci Access Token, potentially compromising continuous integration and deployment workflows." +regex = '''(?i)[\w.-]{0,50}?(?:droneci)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["droneci"] + +[[rules]] +id = "dropbox-api-token" +description = "Identified a Dropbox API secret, which could lead to unauthorized file access and data breaches in Dropbox storage." +regex = '''(?i)[\w.-]{0,50}?(?:dropbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{15})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["dropbox"] + +[[rules]] +id = "dropbox-long-lived-api-token" +description = "Found a Dropbox long-lived API token, risking prolonged unauthorized access to cloud storage and sensitive data." +regex = '''(?i)[\w.-]{0,50}?(?:dropbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{11}(AAAAAAAAAA)[a-z0-9\-_=]{43})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["dropbox"] + +[[rules]] +id = "dropbox-short-lived-api-token" +description = "Discovered a Dropbox short-lived API token, posing a risk of temporary but potentially harmful data access and manipulation." +regex = '''(?i)[\w.-]{0,50}?(?:dropbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(sl\.[a-z0-9\-=_]{135})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["dropbox"] + +[[rules]] +id = "duffel-api-token" +description = "Uncovered a Duffel API token, which may compromise travel platform integrations and sensitive customer data." +regex = '''duffel_(?:test|live)_(?i)[a-z0-9_\-=]{43}''' +entropy = 2 +keywords = ["duffel_"] + +[[rules]] +id = "dynatrace-api-token" +description = "Detected a Dynatrace API token, potentially risking application performance monitoring and data exposure." +regex = '''dt0c01\.(?i)[a-z0-9]{24}\.[a-z0-9]{64}''' +entropy = 4 +keywords = ["dt0c01."] + +[[rules]] +id = "easypost-api-token" +description = "Identified an EasyPost API token, which could lead to unauthorized postal and shipment service access and data exposure." +regex = '''\bEZAK(?i)[a-z0-9]{54}\b''' +entropy = 2 +keywords = ["ezak"] + +[[rules]] +id = "easypost-test-api-token" +description = "Detected an EasyPost test API token, risking exposure of test environments and potentially sensitive shipment data." +regex = '''\bEZTK(?i)[a-z0-9]{54}\b''' +entropy = 2 +keywords = ["eztk"] + +[[rules]] +id = "etsy-access-token" +description = "Found an Etsy Access Token, potentially compromising Etsy shop management and customer data." +regex = '''(?i)[\w.-]{0,50}?(?:(?-i:ETSY|[Ee]tsy))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{24})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["etsy"] + +[[rules]] +id = "facebook-access-token" +description = "Discovered a Facebook Access Token, posing a risk of unauthorized access to Facebook accounts and personal data exposure." +regex = '''(?i)\b(\d{15,16}(\||%)[0-9a-z\-_]{27,40})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["facebook"] + +[[rules]] +id = "facebook-page-access-token" +description = "Discovered a Facebook Page Access Token, posing a risk of unauthorized access to Facebook accounts and personal data exposure." +regex = '''\b(EAA[MC](?i)[a-z0-9]{100,})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = [ + "eaam", + "eaac", +] + +[[rules]] +id = "facebook-secret" +description = "Discovered a Facebook Application secret, posing a risk of unauthorized access to Facebook accounts and personal data exposure." +regex = '''(?i)[\w.-]{0,50}?(?:facebook)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["facebook"] + +[[rules]] +id = "fastly-api-token" +description = "Uncovered a Fastly API key, which may compromise CDN and edge cloud services, leading to content delivery and security issues." +regex = '''(?i)[\w.-]{0,50}?(?:fastly)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["fastly"] + +[[rules]] +id = "finicity-api-token" +description = "Detected a Finicity API token, potentially risking financial data access and unauthorized financial operations." +regex = '''(?i)[\w.-]{0,50}?(?:finicity)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["finicity"] + +[[rules]] +id = "finicity-client-secret" +description = "Identified a Finicity Client Secret, which could lead to compromised financial service integrations and data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:finicity)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["finicity"] + +[[rules]] +id = "finnhub-access-token" +description = "Found a Finnhub Access Token, risking unauthorized access to financial market data and analytics." +regex = '''(?i)[\w.-]{0,50}?(?:finnhub)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["finnhub"] + +[[rules]] +id = "flickr-access-token" +description = "Discovered a Flickr Access Token, posing a risk of unauthorized photo management and potential data leakage." +regex = '''(?i)[\w.-]{0,50}?(?:flickr)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["flickr"] + +[[rules]] +id = "flutterwave-encryption-key" +description = "Uncovered a Flutterwave Encryption Key, which may compromise payment processing and sensitive financial information." +regex = '''FLWSECK_TEST-(?i)[a-h0-9]{12}''' +entropy = 2 +keywords = ["flwseck_test"] + +[[rules]] +id = "flutterwave-public-key" +description = "Detected a Finicity Public Key, potentially exposing public cryptographic operations and integrations." +regex = '''FLWPUBK_TEST-(?i)[a-h0-9]{32}-X''' +entropy = 2 +keywords = ["flwpubk_test"] + +[[rules]] +id = "flutterwave-secret-key" +description = "Identified a Flutterwave Secret Key, risking unauthorized financial transactions and data breaches." +regex = '''FLWSECK_TEST-(?i)[a-h0-9]{32}-X''' +entropy = 2 +keywords = ["flwseck_test"] + +[[rules]] +id = "flyio-access-token" +description = "Uncovered a Fly.io API key" +regex = '''\b((?:fo1_[\w-]{43}|fm1[ar]_[a-zA-Z0-9+\/]{100,}={0,3}|fm2_[a-zA-Z0-9+\/]{100,}={0,3}))(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = [ + "fo1_", + "fm1", + "fm2_", +] + +[[rules]] +id = "frameio-api-token" +description = "Found a Frame.io API token, potentially compromising video collaboration and project management." +regex = '''fio-u-(?i)[a-z0-9\-_=]{64}''' +keywords = ["fio-u-"] + +[[rules]] +id = "freemius-secret-key" +description = "Detected a Freemius secret key, potentially exposing sensitive information." +regex = '''(?i)["']secret_key["']\s*=>\s*["'](sk_[\S]{29})["']''' +path = '''(?i)\.php$''' +keywords = ["secret_key"] + +[[rules]] +id = "freshbooks-access-token" +description = "Discovered a Freshbooks Access Token, posing a risk to accounting software access and sensitive financial data exposure." +regex = '''(?i)[\w.-]{0,50}?(?:freshbooks)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["freshbooks"] + +[[rules]] +id = "gcp-api-key" +description = "Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches." +regex = '''\b(AIza[\w-]{35})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = ["aiza"] +[[rules.allowlists]] +regexes = [ + '''AIzaSyabcdefghijklmnopqrstuvwxyz1234567''', + '''AIzaSyAnLA7NfeLquW1tJFpx_eQCxoX-oo6YyIs''', + '''AIzaSyCkEhVjf3pduRDt6d1yKOMitrUEke8agEM''', + '''AIzaSyDMAScliyLx7F0NPDEJi1QmyCgHIAODrlU''', + '''AIzaSyD3asb-2pEZVqMkmL6M9N6nHZRR_znhrh0''', + '''AIzayDNSXIbFmlXbIE6mCzDLQAqITYefhixbX4A''', + '''AIzaSyAdOS2zB6NCsk1pCdZ4-P6GBdi_UUPwX7c''', + '''AIzaSyASWm6HmTMdYWpgMnjRBjxcQ9CKctWmLd4''', + '''AIzaSyANUvH9H9BsUccjsu2pCmEkOPjjaXeDQgY''', + '''AIzaSyA5_iVawFQ8ABuTZNUdcwERLJv_a_p4wtM''', + '''AIzaSyA4UrcGxgwQFTfaI3no3t7Lt1sjmdnP5sQ''', + '''AIzaSyDSb51JiIcB6OJpwwMicseKRhhrOq1cS7g''', + '''AIzaSyBF2RrAIm4a0mO64EShQfqfd2AFnzAvvuU''', + '''AIzaSyBcE-OOIbhjyR83gm4r2MFCu4MJmprNXsw''', + '''AIzaSyB8qGxt4ec15vitgn44duC5ucxaOi4FmqE''', + '''AIzaSyA8vmApnrHNFE0bApF4hoZ11srVL_n0nvY''', +] + +[[rules]] +id = "generic-api-key" +description = "Detected a Generic API Key, potentially exposing access to various services and sensitive operations." +regex = '''(?i)[\w.-]{0,50}?(?:access|auth|(?-i:[Aa]pi|API)|credential|creds|key|passw(?:or)?d|secret|token)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([\w.=-]{10,150}|[a-z0-9][a-z0-9+/]{11,}={0,3})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3.5 +keywords = [ + "access", + "api", + "auth", + "key", + "credential", + "creds", + "passwd", + "password", + "secret", + "token", +] +[[rules.allowlists]] +regexes = [ + '''^[a-zA-Z_.-]+$''', +] +[[rules.allowlists]] +description = "Allowlist for Generic API Keys" +regexTarget = "match" +regexes = [ + '''(?i)(?:access(?:ibility|or)|access[_.-]?id|random[_.-]?access|api[_.-]?(?:id|name|version)|rapid|capital|[a-z0-9-]*?api[a-z0-9-]*?:jar:|author|X-MS-Exchange-Organization-Auth|Authentication-Results|(?:credentials?[_.-]?id|withCredentials)|(?:bucket|foreign|hot|idx|natural|primary|pub(?:lic)?|schema|sequence)[_.-]?key|(?:turkey)|key[_.-]?(?:alias|board|code|frame|id|length|mesh|name|pair|press(?:ed)?|ring|selector|signature|size|stone|storetype|word|up|down|left|right)|key[_.-]?vault[_.-]?(?:id|name)|keyVaultToStoreSecrets|key(?:store|tab)[_.-]?(?:file|path)|issuerkeyhash|(?-i:[DdMm]onkey|[DM]ONKEY)|keying|(?:secret)[_.-]?(?:length|name|size)|UserSecretsId|(?:csrf)[_.-]?token|(?:io\.jsonwebtoken[ \t]?:[ \t]?[\w-]+)|(?:api|credentials|token)[_.-]?(?:endpoint|ur[il])|public[_.-]?token|(?:key|token)[_.-]?file|(?-i:(?:[A-Z_]+=\n[A-Z_]+=|[a-z_]+=\n[a-z_]+=)(?:\n|\z))|(?-i:(?:[A-Z.]+=\n[A-Z.]+=|[a-z.]+=\n[a-z.]+=)(?:\n|\z)))''', +] +stopwords = [ + "000000", + "6fe4476ee5a1832882e326b506d14126", + "_ec2_", + "aaaaaa", + "about", + "abstract", + "academy", + "acces", + "account", + "act-", + "act.", + "act_", + "action", + "active", + "actively", + "activity", + "adapter", + "add-", + "add-on", + "add.", + "add_", + "addon", + "addres", + "admin", + "adobe", + "advanced", + "adventure", + "agent", + "agile", + "air-", + "air.", + "air_", + "ajax", + "akka", + "alert", + "alfred", + "algorithm", + "all-", + "all.", + "all_", + "alloy", + "alpha", + "amazon", + "amqp", + "analysi", + "analytic", + "analyzer", + "android", + "angular", + "angularj", + "animate", + "animation", + "another", + "ansible", + "answer", + "ant-", + "ant.", + "ant_", + "any-", + "any.", + "any_", + "apache", + "app-", + "app.", + "app_", + "apple", + "arch", + "archive", + "archived", + "arduino", + "array", + "art-", + "art.", + "art_", + "article", + "asp-", + "asp.", + "asp_", + "asset", + "async", + "atom", + "attention", + "audio", + "audit", + "aura", + "auth", + "author", + "authorize", + "auto", + "automated", + "automatic", + "awesome", + "aws_", + "azure", + "back", + "backbone", + "backend", + "backup", + "bar-", + "bar.", + "bar_", + "base", + "based", + "bash", + "basic", + "batch", + "been", + "beer", + "behavior", + "being", + "benchmark", + "best", + "beta", + "better", + "big-", + "big.", + "big_", + "binary", + "binding", + "bit-", + "bit.", + "bit_", + "bitcoin", + "block", + "blog", + "board", + "book", + "bookmark", + "boost", + "boot", + "bootstrap", + "bosh", + "bot-", + "bot.", + "bot_", + "bower", + "box-", + "box.", + "box_", + "boxen", + "bracket", + "branch", + "bridge", + "browser", + "brunch", + "buffer", + "bug-", + "bug.", + "bug_", + "build", + "builder", + "building", + "buildout", + "buildpack", + "built", + "bundle", + "busines", + "but-", + "but.", + "but_", + "button", + "cache", + "caching", + "cakephp", + "calendar", + "call", + "camera", + "campfire", + "can-", + "can.", + "can_", + "canva", + "captcha", + "capture", + "card", + "carousel", + "case", + "cassandra", + "cat-", + "cat.", + "cat_", + "category", + "center", + "cento", + "challenge", + "change", + "changelog", + "channel", + "chart", + "chat", + "cheat", + "check", + "checker", + "chef", + "ches", + "chinese", + "chosen", + "chrome", + "ckeditor", + "clas", + "classe", + "classic", + "clean", + "cli-", + "cli.", + "cli_", + "client", + "clojure", + "clone", + "closure", + "cloud", + "club", + "cluster", + "cms-", + "cms_", + "coco", + "code", + "coding", + "coffee", + "color", + "combination", + "combo", + "command", + "commander", + "comment", + "commit", + "common", + "community", + "compas", + "compiler", + "complete", + "component", + "composer", + "computer", + "computing", + "con-", + "con.", + "con_", + "concept", + "conf", + "config", + "connect", + "connector", + "console", + "contact", + "container", + "contao", + "content", + "contest", + "context", + "control", + "convert", + "converter", + "conway'", + "cookbook", + "cookie", + "cool", + "copy", + "cordova", + "core", + "couchbase", + "couchdb", + "countdown", + "counter", + "course", + "craft", + "crawler", + "create", + "creating", + "creator", + "credential", + "crm-", + "crm.", + "crm_", + "cros", + "crud", + "csv-", + "csv.", + "csv_", + "cube", + "cucumber", + "cuda", + "current", + "currently", + "custom", + "daemon", + "dark", + "dart", + "dash", + "dashboard", + "data", + "database", + "date", + "day-", + "day.", + "day_", + "dead", + "debian", + "debug", + "debugger", + "deck", + "define", + "del-", + "del.", + "del_", + "delete", + "demo", + "deploy", + "design", + "designer", + "desktop", + "detection", + "detector", + "dev-", + "dev.", + "dev_", + "develop", + "developer", + "device", + "devise", + "diff", + "digital", + "directive", + "directory", + "discovery", + "display", + "django", + "dns-", + "dns_", + "doc-", + "doc.", + "doc_", + "docker", + "docpad", + "doctrine", + "document", + "doe-", + "doe.", + "doe_", + "dojo", + "dom-", + "dom.", + "dom_", + "domain", + "don't", + "done", + "dot-", + "dot.", + "dot_", + "dotfile", + "download", + "draft", + "drag", + "drill", + "drive", + "driven", + "driver", + "drop", + "dropbox", + "drupal", + "dsl-", + "dsl.", + "dsl_", + "dynamic", + "easy", + "ecdsa", + "eclipse", + "edit", + "editing", + "edition", + "editor", + "element", + "emac", + "email", + "embed", + "embedded", + "ember", + "emitter", + "emulator", + "encoding", + "endpoint", + "engine", + "english", + "enhanced", + "entity", + "entry", + "env_", + "episode", + "erlang", + "error", + "espresso", + "event", + "evented", + "example", + "exchange", + "exercise", + "experiment", + "expire", + "exploit", + "explorer", + "export", + "exporter", + "expres", + "ext-", + "ext.", + "ext_", + "extended", + "extension", + "external", + "extra", + "extractor", + "fabric", + "facebook", + "factory", + "fake", + "fast", + "feature", + "feed", + "fewfwef", + "ffmpeg", + "field", + "file", + "filter", + "find", + "finder", + "firefox", + "firmware", + "first", + "fish", + "fix-", + "fix_", + "flash", + "flask", + "flat", + "flex", + "flexible", + "flickr", + "flow", + "fluent", + "fluentd", + "fluid", + "folder", + "font", + "force", + "foreman", + "fork", + "form", + "format", + "formatter", + "forum", + "foundry", + "framework", + "free", + "friend", + "friendly", + "front-end", + "frontend", + "ftp-", + "ftp.", + "ftp_", + "fuel", + "full", + "fun-", + "fun.", + "fun_", + "func", + "future", + "gaia", + "gallery", + "game", + "gateway", + "gem-", + "gem.", + "gem_", + "gen-", + "gen.", + "gen_", + "general", + "generator", + "generic", + "genetic", + "get-", + "get.", + "get_", + "getenv", + "getting", + "ghost", + "gist", + "git-", + "git.", + "git_", + "github", + "gitignore", + "gitlab", + "glas", + "gmail", + "gnome", + "gnu-", + "gnu.", + "gnu_", + "goal", + "golang", + "gollum", + "good", + "google", + "gpu-", + "gpu.", + "gpu_", + "gradle", + "grail", + "graph", + "graphic", + "great", + "grid", + "groovy", + "group", + "grunt", + "guard", + "gui-", + "gui.", + "gui_", + "guide", + "guideline", + "gulp", + "gwt-", + "gwt.", + "gwt_", + "hack", + "hackathon", + "hacker", + "hacking", + "hadoop", + "haml", + "handler", + "hardware", + "has-", + "has_", + "hash", + "haskell", + "have", + "haxe", + "hello", + "help", + "helper", + "here", + "hero", + "heroku", + "high", + "hipchat", + "history", + "home", + "homebrew", + "homepage", + "hook", + "host", + "hosting", + "hot-", + "hot.", + "hot_", + "house", + "how-", + "how.", + "how_", + "html", + "http", + "hub-", + "hub.", + "hub_", + "hubot", + "human", + "icon", + "ide-", + "ide.", + "ide_", + "idea", + "identity", + "idiomatic", + "image", + "impact", + "import", + "important", + "importer", + "impres", + "index", + "infinite", + "info", + "injection", + "inline", + "input", + "inside", + "inspector", + "instagram", + "install", + "installer", + "instant", + "intellij", + "interface", + "internet", + "interview", + "into", + "intro", + "ionic", + "iphone", + "ipython", + "irc-", + "irc_", + "iso-", + "iso.", + "iso_", + "issue", + "jade", + "jasmine", + "java", + "jbos", + "jekyll", + "jenkin", + "jetbrains", + "job-", + "job.", + "job_", + "joomla", + "jpa-", + "jpa.", + "jpa_", + "jquery", + "json", + "just", + "kafka", + "karma", + "kata", + "kernel", + "keyboard", + "kindle", + "kit-", + "kit.", + "kit_", + "kitchen", + "knife", + "koan", + "kohana", + "lab-", + "lab.", + "lab_", + "lambda", + "lamp", + "language", + "laravel", + "last", + "latest", + "latex", + "launcher", + "layer", + "layout", + "lazy", + "ldap", + "leaflet", + "league", + "learn", + "learning", + "led-", + "led.", + "led_", + "leetcode", + "les-", + "les.", + "les_", + "level", + "leveldb", + "lib-", + "lib.", + "lib_", + "librarie", + "library", + "license", + "life", + "liferay", + "light", + "lightbox", + "like", + "line", + "link", + "linked", + "linkedin", + "linux", + "lisp", + "list", + "lite", + "little", + "load", + "loader", + "local", + "location", + "lock", + "log-", + "log.", + "log_", + "logger", + "logging", + "logic", + "login", + "logstash", + "longer", + "look", + "love", + "lua-", + "lua.", + "lua_", + "mac-", + "mac.", + "mac_", + "machine", + "made", + "magento", + "magic", + "mail", + "make", + "maker", + "making", + "man-", + "man.", + "man_", + "manage", + "manager", + "manifest", + "manual", + "map-", + "map.", + "map_", + "mapper", + "mapping", + "markdown", + "markup", + "master", + "math", + "matrix", + "maven", + "md5", + "mean", + "media", + "mediawiki", + "meetup", + "memcached", + "memory", + "menu", + "merchant", + "message", + "messaging", + "meta", + "metadata", + "meteor", + "method", + "metric", + "micro", + "middleman", + "migration", + "minecraft", + "miner", + "mini", + "minimal", + "mirror", + "mit-", + "mit.", + "mit_", + "mobile", + "mocha", + "mock", + "mod-", + "mod.", + "mod_", + "mode", + "model", + "modern", + "modular", + "module", + "modx", + "money", + "mongo", + "mongodb", + "mongoid", + "mongoose", + "monitor", + "monkey", + "more", + "motion", + "moved", + "movie", + "mozilla", + "mqtt", + "mule", + "multi", + "multiple", + "music", + "mustache", + "mvc-", + "mvc.", + "mvc_", + "mysql", + "nagio", + "name", + "native", + "need", + "neo-", + "neo.", + "neo_", + "nest", + "nested", + "net-", + "net.", + "net_", + "nette", + "network", + "new-", + "new.", + "new_", + "next", + "nginx", + "ninja", + "nlp-", + "nlp.", + "nlp_", + "node", + "nodej", + "nosql", + "not-", + "not.", + "not_", + "note", + "notebook", + "notepad", + "notice", + "notifier", + "now-", + "now.", + "now_", + "number", + "oauth", + "object", + "objective", + "obsolete", + "ocaml", + "octopres", + "official", + "old-", + "old.", + "old_", + "onboard", + "online", + "only", + "open", + "opencv", + "opengl", + "openshift", + "openwrt", + "option", + "oracle", + "org-", + "org.", + "org_", + "origin", + "original", + "orm-", + "orm.", + "orm_", + "osx-", + "osx_", + "our-", + "our.", + "our_", + "out-", + "out.", + "out_", + "output", + "over", + "overview", + "own-", + "own.", + "own_", + "pack", + "package", + "packet", + "page", + "panel", + "paper", + "paperclip", + "para", + "parallax", + "parallel", + "parse", + "parser", + "parsing", + "particle", + "party", + "password", + "patch", + "path", + "pattern", + "payment", + "paypal", + "pdf-", + "pdf.", + "pdf_", + "pebble", + "people", + "perl", + "personal", + "phalcon", + "phoenix", + "phone", + "phonegap", + "photo", + "php-", + "php.", + "php_", + "physic", + "picker", + "pipeline", + "platform", + "play", + "player", + "please", + "plu-", + "plu.", + "plu_", + "plug-in", + "plugin", + "plupload", + "png-", + "png.", + "png_", + "poker", + "polyfill", + "polymer", + "pool", + "pop-", + "pop.", + "pop_", + "popcorn", + "popup", + "port", + "portable", + "portal", + "portfolio", + "post", + "power", + "powered", + "powerful", + "prelude", + "pretty", + "preview", + "principle", + "print", + "pro-", + "pro.", + "pro_", + "problem", + "proc", + "product", + "profile", + "profiler", + "program", + "progres", + "project", + "protocol", + "prototype", + "provider", + "proxy", + "public", + "pull", + "puppet", + "pure", + "purpose", + "push", + "pusher", + "pyramid", + "python", + "quality", + "query", + "queue", + "quick", + "rabbitmq", + "rack", + "radio", + "rail", + "railscast", + "random", + "range", + "raspberry", + "rdf-", + "rdf.", + "rdf_", + "react", + "reactive", + "read", + "reader", + "readme", + "ready", + "real", + "real-time", + "reality", + "realtime", + "recipe", + "recorder", + "red-", + "red.", + "red_", + "reddit", + "redi", + "redmine", + "reference", + "refinery", + "refresh", + "registry", + "related", + "release", + "remote", + "rendering", + "repo", + "report", + "request", + "require", + "required", + "requirej", + "research", + "resource", + "response", + "resque", + "rest", + "restful", + "resume", + "reveal", + "reverse", + "review", + "riak", + "rich", + "right", + "ring", + "robot", + "role", + "room", + "router", + "routing", + "rpc-", + "rpc.", + "rpc_", + "rpg-", + "rpg.", + "rpg_", + "rspec", + "ruby-", + "ruby.", + "ruby_", + "rule", + "run-", + "run.", + "run_", + "runner", + "running", + "runtime", + "rust", + "rvm-", + "rvm.", + "rvm_", + "salt", + "sample", + "sandbox", + "sas-", + "sas.", + "sas_", + "sbt-", + "sbt.", + "sbt_", + "scala", + "scalable", + "scanner", + "schema", + "scheme", + "school", + "science", + "scraper", + "scratch", + "screen", + "script", + "scroll", + "scs-", + "scs.", + "scs_", + "sdk-", + "sdk.", + "sdk_", + "sdl-", + "sdl.", + "sdl_", + "search", + "secure", + "security", + "see-", + "see.", + "see_", + "seed", + "select", + "selector", + "selenium", + "semantic", + "sencha", + "send", + "sentiment", + "serie", + "server", + "service", + "session", + "set-", + "set.", + "set_", + "setting", + "setup", + "sha1", + "sha2", + "sha256", + "share", + "shared", + "sharing", + "sheet", + "shell", + "shield", + "shipping", + "shop", + "shopify", + "shortener", + "should", + "show", + "showcase", + "side", + "silex", + "simple", + "simulator", + "single", + "site", + "skeleton", + "sketch", + "skin", + "slack", + "slide", + "slider", + "slim", + "small", + "smart", + "smtp", + "snake", + "snapshot", + "snippet", + "soap", + "social", + "socket", + "software", + "solarized", + "solr", + "solution", + "solver", + "some", + "soon", + "source", + "space", + "spark", + "spatial", + "spec", + "sphinx", + "spine", + "spotify", + "spree", + "spring", + "sprite", + "sql-", + "sql.", + "sql_", + "sqlite", + "ssh-", + "ssh.", + "ssh_", + "stack", + "staging", + "standard", + "stanford", + "start", + "started", + "starter", + "startup", + "stat", + "statamic", + "state", + "static", + "statistic", + "statsd", + "statu", + "steam", + "step", + "still", + "stm-", + "stm.", + "stm_", + "storage", + "store", + "storm", + "story", + "strategy", + "stream", + "streaming", + "string", + "stripe", + "structure", + "studio", + "study", + "stuff", + "style", + "sublime", + "sugar", + "suite", + "summary", + "super", + "support", + "supported", + "svg-", + "svg.", + "svg_", + "svn-", + "svn.", + "svn_", + "swagger", + "swift", + "switch", + "switcher", + "symfony", + "symphony", + "sync", + "synopsi", + "syntax", + "system", + "tab-", + "tab.", + "tab_", + "table", + "tag-", + "tag.", + "tag_", + "talk", + "target", + "task", + "tcp-", + "tcp.", + "tcp_", + "tdd-", + "tdd.", + "tdd_", + "team", + "tech", + "template", + "term", + "terminal", + "testing", + "tetri", + "text", + "textmate", + "theme", + "theory", + "three", + "thrift", + "time", + "timeline", + "timer", + "tiny", + "tinymce", + "tip-", + "tip.", + "tip_", + "title", + "todo", + "todomvc", + "token", + "tool", + "toolbox", + "toolkit", + "top-", + "top.", + "top_", + "tornado", + "touch", + "tower", + "tracker", + "tracking", + "traffic", + "training", + "transfer", + "translate", + "transport", + "tree", + "trello", + "try-", + "try.", + "try_", + "tumblr", + "tut-", + "tut.", + "tut_", + "tutorial", + "tweet", + "twig", + "twitter", + "type", + "typo", + "ubuntu", + "uiview", + "ultimate", + "under", + "unit", + "unity", + "universal", + "unix", + "update", + "updated", + "upgrade", + "upload", + "uploader", + "uri-", + "uri.", + "uri_", + "url-", + "url.", + "url_", + "usage", + "usb-", + "usb.", + "usb_", + "use-", + "use.", + "use_", + "used", + "useful", + "user", + "using", + "util", + "utilitie", + "utility", + "vagrant", + "validator", + "value", + "variou", + "varnish", + "version", + "via-", + "via.", + "via_", + "video", + "view", + "viewer", + "vim-", + "vim.", + "vim_", + "vimrc", + "virtual", + "vision", + "visual", + "vpn", + "want", + "warning", + "watch", + "watcher", + "wave", + "way-", + "way.", + "way_", + "weather", + "web-", + "web_", + "webapp", + "webgl", + "webhook", + "webkit", + "webrtc", + "website", + "websocket", + "welcome", + "what", + "what'", + "when", + "where", + "which", + "why-", + "why.", + "why_", + "widget", + "wifi", + "wiki", + "win-", + "win.", + "win_", + "window", + "wip-", + "wip.", + "wip_", + "within", + "without", + "wizard", + "word", + "wordpres", + "work", + "worker", + "workflow", + "working", + "workshop", + "world", + "wrapper", + "write", + "writer", + "writing", + "written", + "www-", + "www.", + "www_", + "xamarin", + "xcode", + "xml-", + "xml.", + "xml_", + "xmpp", + "xxxxxx", + "yahoo", + "yaml", + "yandex", + "yeoman", + "yet-", + "yet.", + "yet_", + "yii-", + "yii.", + "yii_", + "youtube", + "yui-", + "yui.", + "yui_", + "zend", + "zero", + "zip-", + "zip.", + "zip_", + "zsh-", + "zsh.", + "zsh_", +] +[[rules.allowlists]] +regexTarget = "line" +regexes = [ + '''--mount=type=secret,''', + '''import[ \t]+{[ \t\w,]+}[ \t]+from[ \t]+['"][^'"]+['"]''', +] +[[rules.allowlists]] +condition = "AND" +paths = [ + '''\.bb$''','''\.bbappend$''','''\.bbclass$''','''\.inc$''', +] +regexTarget = "line" +regexes = [ + '''LICENSE[^=]*=\s*"[^"]+''', + '''LIC_FILES_CHKSUM[^=]*=\s*"[^"]+''', + '''SRC[^=]*=\s*"[a-zA-Z0-9]+''', +] + +[[rules]] +id = "github-app-token" +description = "Identified a GitHub App Token, which may compromise GitHub application integrations and source code security." +regex = '''(?:ghu|ghs)_[0-9a-zA-Z]{36}''' +entropy = 3 +keywords = [ + "ghu_", + "ghs_", +] +[[rules.allowlists]] +paths = [ + '''(?:^|/)@octokit/auth-token/README\.md$''', +] + +[[rules]] +id = "github-fine-grained-pat" +description = "Found a GitHub Fine-Grained Personal Access Token, risking unauthorized repository access and code manipulation." +regex = '''github_pat_\w{82}''' +entropy = 3 +keywords = ["github_pat_"] + +[[rules]] +id = "github-oauth" +description = "Discovered a GitHub OAuth Access Token, posing a risk of compromised GitHub account integrations and data leaks." +regex = '''gho_[0-9a-zA-Z]{36}''' +entropy = 3 +keywords = ["gho_"] + +[[rules]] +id = "github-pat" +description = "Uncovered a GitHub Personal Access Token, potentially leading to unauthorized repository access and sensitive content exposure." +regex = '''ghp_[0-9a-zA-Z]{36}''' +entropy = 3 +keywords = ["ghp_"] +[[rules.allowlists]] +paths = [ + '''(?:^|/)@octokit/auth-token/README\.md$''', +] + +[[rules]] +id = "github-refresh-token" +description = "Detected a GitHub Refresh Token, which could allow prolonged unauthorized access to GitHub services." +regex = '''ghr_[0-9a-zA-Z]{36}''' +entropy = 3 +keywords = ["ghr_"] + +[[rules]] +id = "gitlab-cicd-job-token" +description = "Identified a GitLab CI/CD Job Token, potential access to projects and some APIs on behalf of a user while the CI job is running." +regex = '''glcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}''' +entropy = 3 +keywords = ["glcbt-"] + +[[rules]] +id = "gitlab-deploy-token" +description = "Identified a GitLab Deploy Token, risking access to repositories, packages and containers with write access." +regex = '''gldt-[0-9a-zA-Z_\-]{20}''' +entropy = 3 +keywords = ["gldt-"] + +[[rules]] +id = "gitlab-feature-flag-client-token" +description = "Identified a GitLab feature flag client token, risks exposing user lists and features flags used by an application." +regex = '''glffct-[0-9a-zA-Z_\-]{20}''' +entropy = 3 +keywords = ["glffct-"] + +[[rules]] +id = "gitlab-feed-token" +description = "Identified a GitLab feed token, risking exposure of user data." +regex = '''glft-[0-9a-zA-Z_\-]{20}''' +entropy = 3 +keywords = ["glft-"] + +[[rules]] +id = "gitlab-incoming-mail-token" +description = "Identified a GitLab incoming mail token, risking manipulation of data sent by mail." +regex = '''glimt-[0-9a-zA-Z_\-]{25}''' +entropy = 3 +keywords = ["glimt-"] + +[[rules]] +id = "gitlab-kubernetes-agent-token" +description = "Identified a GitLab Kubernetes Agent token, risking access to repos and registry of projects connected via agent." +regex = '''glagent-[0-9a-zA-Z_\-]{50}''' +entropy = 3 +keywords = ["glagent-"] + +[[rules]] +id = "gitlab-oauth-app-secret" +description = "Identified a GitLab OIDC Application Secret, risking access to apps using GitLab as authentication provider." +regex = '''gloas-[0-9a-zA-Z_\-]{64}''' +entropy = 3 +keywords = ["gloas-"] + +[[rules]] +id = "gitlab-pat" +description = "Identified a GitLab Personal Access Token, risking unauthorized access to GitLab repositories and codebase exposure." +regex = '''glpat-[\w-]{20}''' +entropy = 3 +keywords = ["glpat-"] + +[[rules]] +id = "gitlab-pat-routable" +description = "Identified a GitLab Personal Access Token (routable), risking unauthorized access to GitLab repositories and codebase exposure." +regex = '''\bglpat-[0-9a-zA-Z_-]{27,300}\.[0-9a-z]{2}[0-9a-z]{7}\b''' +entropy = 4 +keywords = ["glpat-"] + +[[rules]] +id = "gitlab-ptt" +description = "Found a GitLab Pipeline Trigger Token, potentially compromising continuous integration workflows and project security." +regex = '''glptt-[0-9a-f]{40}''' +entropy = 3 +keywords = ["glptt-"] + +[[rules]] +id = "gitlab-rrt" +description = "Discovered a GitLab Runner Registration Token, posing a risk to CI/CD pipeline integrity and unauthorized access." +regex = '''GR1348941[\w-]{20}''' +entropy = 3 +keywords = ["gr1348941"] + +[[rules]] +id = "gitlab-runner-authentication-token" +description = "Discovered a GitLab Runner Authentication Token, posing a risk to CI/CD pipeline integrity and unauthorized access." +regex = '''glrt-[0-9a-zA-Z_\-]{20}''' +entropy = 3 +keywords = ["glrt-"] + +[[rules]] +id = "gitlab-runner-authentication-token-routable" +description = "Discovered a GitLab Runner Authentication Token (Routable), posing a risk to CI/CD pipeline integrity and unauthorized access." +regex = '''\bglrt-t\d_[0-9a-zA-Z_\-]{27,300}\.[0-9a-z]{2}[0-9a-z]{7}\b''' +entropy = 4 +keywords = ["glrt-"] + +[[rules]] +id = "gitlab-scim-token" +description = "Discovered a GitLab SCIM Token, posing a risk to unauthorized access for a organization or instance." +regex = '''glsoat-[0-9a-zA-Z_\-]{20}''' +entropy = 3 +keywords = ["glsoat-"] + +[[rules]] +id = "gitlab-session-cookie" +description = "Discovered a GitLab Session Cookie, posing a risk to unauthorized access to a user account." +regex = '''_gitlab_session=[0-9a-z]{32}''' +entropy = 3 +keywords = ["_gitlab_session="] + +[[rules]] +id = "gitter-access-token" +description = "Uncovered a Gitter Access Token, which may lead to unauthorized access to chat and communication services." +regex = '''(?i)[\w.-]{0,50}?(?:gitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["gitter"] + +[[rules]] +id = "gocardless-api-token" +description = "Detected a GoCardless API token, potentially risking unauthorized direct debit payment operations and financial data exposure." +regex = '''(?i)[\w.-]{0,50}?(?:gocardless)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(live_(?i)[a-z0-9\-_=]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = [ + "live_", + "gocardless", +] + +[[rules]] +id = "grafana-api-key" +description = "Identified a Grafana API key, which could compromise monitoring dashboards and sensitive data analytics." +regex = '''(?i)\b(eyJrIjoi[A-Za-z0-9]{70,400}={0,3})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["eyjrijoi"] + +[[rules]] +id = "grafana-cloud-api-token" +description = "Found a Grafana cloud API token, risking unauthorized access to cloud-based monitoring services and data exposure." +regex = '''(?i)\b(glc_[A-Za-z0-9+/]{32,400}={0,3})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["glc_"] + +[[rules]] +id = "grafana-service-account-token" +description = "Discovered a Grafana service account token, posing a risk of compromised monitoring services and data integrity." +regex = '''(?i)\b(glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["glsa_"] + +[[rules]] +id = "harness-api-key" +description = "Identified a Harness Access Token (PAT or SAT), risking unauthorized access to a Harness account." +regex = '''(?:pat|sat)\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9]{24}\.[a-zA-Z0-9]{20}''' +keywords = [ + "pat.", + "sat.", +] + +[[rules]] +id = "hashicorp-tf-api-token" +description = "Uncovered a HashiCorp Terraform user/org API token, which may lead to unauthorized infrastructure management and security breaches." +regex = '''(?i)[a-z0-9]{14}\.(?-i:atlasv1)\.[a-z0-9\-_=]{60,70}''' +entropy = 3.5 +keywords = ["atlasv1"] + +[[rules]] +id = "hashicorp-tf-password" +description = "Identified a HashiCorp Terraform password field, risking unauthorized infrastructure configuration and security breaches." +regex = '''(?i)[\w.-]{0,50}?(?:administrator_login_password|password)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}("[a-z0-9=_\-]{8,20}")(?:[\x60'"\s;]|\\[nr]|$)''' +path = '''(?i)\.(?:tf|hcl)$''' +entropy = 2 +keywords = [ + "administrator_login_password", + "password", +] + +[[rules]] +id = "heroku-api-key" +description = "Detected a Heroku API Key, potentially compromising cloud application deployments and operational security." +regex = '''(?i)[\w.-]{0,50}?(?:heroku)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["heroku"] + +[[rules]] +id = "heroku-api-key-v2" +description = "Detected a Heroku API Key, potentially compromising cloud application deployments and operational security." +regex = '''\b((HRKU-AA[0-9a-zA-Z_-]{58}))(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = ["hrku-aa"] + +[[rules]] +id = "hubspot-api-key" +description = "Found a HubSpot API Token, posing a risk to CRM data integrity and unauthorized marketing operations." +regex = '''(?i)[\w.-]{0,50}?(?:hubspot)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["hubspot"] + +[[rules]] +id = "huggingface-access-token" +description = "Discovered a Hugging Face Access token, which could lead to unauthorized access to AI models and sensitive data." +regex = '''\b(hf_(?i:[a-z]{34}))(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["hf_"] + +[[rules]] +id = "huggingface-organization-api-token" +description = "Uncovered a Hugging Face Organization API token, potentially compromising AI organization accounts and associated data." +regex = '''\b(api_org_(?i:[a-z]{34}))(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["api_org_"] + +[[rules]] +id = "infracost-api-token" +description = "Detected an Infracost API Token, risking unauthorized access to cloud cost estimation tools and financial data." +regex = '''\b(ico-[a-zA-Z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["ico-"] + +[[rules]] +id = "intercom-api-key" +description = "Identified an Intercom API Token, which could compromise customer communication channels and data privacy." +regex = '''(?i)[\w.-]{0,50}?(?:intercom)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{60})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["intercom"] + +[[rules]] +id = "intra42-client-secret" +description = "Found a Intra42 client secret, which could lead to unauthorized access to the 42School API and sensitive data." +regex = '''\b(s-s4t2(?:ud|af)-(?i)[abcdef0123456789]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = [ + "intra", + "s-s4t2ud-", + "s-s4t2af-", +] + +[[rules]] +id = "jfrog-api-key" +description = "Found a JFrog API Key, posing a risk of unauthorized access to software artifact repositories and build pipelines." +regex = '''(?i)[\w.-]{0,50}?(?:jfrog|artifactory|bintray|xray)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{73})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = [ + "jfrog", + "artifactory", + "bintray", + "xray", +] + +[[rules]] +id = "jfrog-identity-token" +description = "Discovered a JFrog Identity Token, potentially compromising access to JFrog services and sensitive software artifacts." +regex = '''(?i)[\w.-]{0,50}?(?:jfrog|artifactory|bintray|xray)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = [ + "jfrog", + "artifactory", + "bintray", + "xray", +] + +[[rules]] +id = "jwt" +description = "Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data." +regex = '''\b(ey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/\\_-]{17,}\.(?:[a-zA-Z0-9\/\\_-]{10,}={0,2})?)(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["ey"] + +[[rules]] +id = "jwt-base64" +description = "Detected a Base64-encoded JSON Web Token, posing a risk of exposing encoded authentication and data exchange information." +regex = '''\bZXlK(?:(?PaGJHY2lPaU)|(?PaGNIVWlPaU)|(?PaGNIWWlPaU)|(?PaGRXUWlPaU)|(?PaU5qUWlP)|(?PamNtbDBJanBi)|(?PamRIa2lPaU)|(?PbGNHc2lPbn)|(?PbGJtTWlPaU)|(?PcWEzVWlPaU)|(?PcWQyc2lPb)|(?PcGMzTWlPaU)|(?PcGRpSTZJ)|(?PcmFXUWlP)|(?PclpYbGZiM0J6SWpwY)|(?PcmRIa2lPaUp)|(?PdWIyNWpaU0k2)|(?Pd01tTWlP)|(?Pd01uTWlPaU)|(?Pd2NIUWlPaU)|(?PemRXSWlPaU)|(?PemRuUWlP)|(?PMFlXY2lPaU)|(?PMGVYQWlPaUp)|(?PMWNtd2l)|(?PMWMyVWlPaUp)|(?PMlpYSWlPaU)|(?PMlpYSnphVzl1SWpv)|(?PNElqb2)|(?PNE5XTWlP)|(?PNE5YUWlPaU)|(?PNE5YUWpVekkxTmlJNkl)|(?PNE5YVWlPaU)|(?PNmFYQWlPaU))[a-zA-Z0-9\/\\_+\-\r\n]{40,}={0,2}''' +entropy = 2 +keywords = ["zxlk"] + +[[rules]] +id = "kraken-access-token" +description = "Identified a Kraken Access Token, potentially compromising cryptocurrency trading accounts and financial security." +regex = '''(?i)[\w.-]{0,50}?(?:kraken)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9\/=_\+\-]{80,90})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["kraken"] + +[[rules]] +id = "kubernetes-secret-yaml" +description = "Possible Kubernetes Secret detected, posing a risk of leaking credentials/tokens from your deployments" +regex = '''(?i)(?:\bkind:[ \t]*["']?\bsecret\b["']?(?s:.){0,200}?\bdata:(?s:.){0,100}?\s+([\w.-]+:(?:[ \t]*(?:\||>[-+]?)\s+)?[ \t]*(?:["']?[a-z0-9+/]{10,}={0,3}["']?|\{\{[ \t\w"|$:=,.-]+}}|""|''))|\bdata:(?s:.){0,100}?\s+([\w.-]+:(?:[ \t]*(?:\||>[-+]?)\s+)?[ \t]*(?:["']?[a-z0-9+/]{10,}={0,3}["']?|\{\{[ \t\w"|$:=,.-]+}}|""|''))(?s:.){0,200}?\bkind:[ \t]*["']?\bsecret\b["']?)''' +path = '''(?i)\.ya?ml$''' +keywords = ["secret"] +[[rules.allowlists]] +regexes = [ + '''[\w.-]+:(?:[ \t]*(?:\||>[-+]?)\s+)?[ \t]*(?:\{\{[ \t\w"|$:=,.-]+}}|""|'')''', +] +[[rules.allowlists]] +regexTarget = "match" +regexes = [ + '''(kind:(?s:.)+\n---\n(?s:.)+\bdata:|data:(?s:.)+\n---\n(?s:.)+\bkind:)''', +] + +[[rules]] +id = "kucoin-access-token" +description = "Found a Kucoin Access Token, risking unauthorized access to cryptocurrency exchange services and transactions." +regex = '''(?i)[\w.-]{0,50}?(?:kucoin)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{24})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["kucoin"] + +[[rules]] +id = "kucoin-secret-key" +description = "Discovered a Kucoin Secret Key, which could lead to compromised cryptocurrency operations and financial data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:kucoin)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["kucoin"] + +[[rules]] +id = "launchdarkly-access-token" +description = "Uncovered a Launchdarkly Access Token, potentially compromising feature flag management and application functionality." +regex = '''(?i)[\w.-]{0,50}?(?:launchdarkly)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["launchdarkly"] + +[[rules]] +id = "linear-api-key" +description = "Detected a Linear API Token, posing a risk to project management tools and sensitive task data." +regex = '''lin_api_(?i)[a-z0-9]{40}''' +entropy = 2 +keywords = ["lin_api_"] + +[[rules]] +id = "linear-client-secret" +description = "Identified a Linear Client Secret, which may compromise secure integrations and sensitive project management data." +regex = '''(?i)[\w.-]{0,50}?(?:linear)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["linear"] + +[[rules]] +id = "linkedin-client-id" +description = "Found a LinkedIn Client ID, risking unauthorized access to LinkedIn integrations and professional data exposure." +regex = '''(?i)[\w.-]{0,50}?(?:linked[_-]?in)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{14})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = [ + "linkedin", + "linked_in", + "linked-in", +] + +[[rules]] +id = "linkedin-client-secret" +description = "Discovered a LinkedIn Client secret, potentially compromising LinkedIn application integrations and user data." +regex = '''(?i)[\w.-]{0,50}?(?:linked[_-]?in)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = [ + "linkedin", + "linked_in", + "linked-in", +] + +[[rules]] +id = "lob-api-key" +description = "Uncovered a Lob API Key, which could lead to unauthorized access to mailing and address verification services." +regex = '''(?i)[\w.-]{0,50}?(?:lob)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}((live|test)_[a-f0-9]{35})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = [ + "test_", + "live_", +] + +[[rules]] +id = "lob-pub-api-key" +description = "Detected a Lob Publishable API Key, posing a risk of exposing mail and print service integrations." +regex = '''(?i)[\w.-]{0,50}?(?:lob)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}((test|live)_pub_[a-f0-9]{31})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = [ + "test_pub", + "live_pub", + "_pub", +] + +[[rules]] +id = "looker-client-id" +description = "Found a Looker Client ID, risking unauthorized access to a Looker account and exposing sensitive data." +regex = '''(?i)[\w.-]{0,50}?(?:looker)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["looker"] + +[[rules]] +id = "looker-client-secret" +description = "Found a Looker Client Secret, risking unauthorized access to a Looker account and exposing sensitive data." +regex = '''(?i)[\w.-]{0,50}?(?:looker)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{24})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["looker"] + +[[rules]] +id = "mailchimp-api-key" +description = "Identified a Mailchimp API key, potentially compromising email marketing campaigns and subscriber data." +regex = '''(?i)[\w.-]{0,50}?(?:MailchimpSDK.initialize|mailchimp)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32}-us\d\d)(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["mailchimp"] + +[[rules]] +id = "mailgun-private-api-token" +description = "Found a Mailgun private API token, risking unauthorized email service operations and data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:mailgun)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(key-[a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["mailgun"] + +[[rules]] +id = "mailgun-pub-key" +description = "Discovered a Mailgun public validation key, which could expose email verification processes and associated data." +regex = '''(?i)[\w.-]{0,50}?(?:mailgun)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(pubkey-[a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["mailgun"] + +[[rules]] +id = "mailgun-signing-key" +description = "Uncovered a Mailgun webhook signing key, potentially compromising email automation and data integrity." +regex = '''(?i)[\w.-]{0,50}?(?:mailgun)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-h0-9]{32}-[a-h0-9]{8}-[a-h0-9]{8})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["mailgun"] + +[[rules]] +id = "mapbox-api-token" +description = "Detected a MapBox API token, posing a risk to geospatial services and sensitive location data exposure." +regex = '''(?i)[\w.-]{0,50}?(?:mapbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(pk\.[a-z0-9]{60}\.[a-z0-9]{22})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["mapbox"] + +[[rules]] +id = "mattermost-access-token" +description = "Identified a Mattermost Access Token, which may compromise team communication channels and data privacy." +regex = '''(?i)[\w.-]{0,50}?(?:mattermost)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{26})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["mattermost"] + +[[rules]] +id = "maxmind-license-key" +description = "Discovered a potential MaxMind license key." +regex = '''\b([A-Za-z0-9]{6}_[A-Za-z0-9]{29}_mmk)(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = ["_mmk"] + +[[rules]] +id = "messagebird-api-token" +description = "Found a MessageBird API token, risking unauthorized access to communication platforms and message data." +regex = '''(?i)[\w.-]{0,50}?(?:message[_-]?bird)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{25})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = [ + "messagebird", + "message-bird", + "message_bird", +] + +[[rules]] +id = "messagebird-client-id" +description = "Discovered a MessageBird client ID, potentially compromising API integrations and sensitive communication data." +regex = '''(?i)[\w.-]{0,50}?(?:message[_-]?bird)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = [ + "messagebird", + "message-bird", + "message_bird", +] + +[[rules]] +id = "microsoft-teams-webhook" +description = "Uncovered a Microsoft Teams Webhook, which could lead to unauthorized access to team collaboration tools and data leaks." +regex = '''https://[a-z0-9]+\.webhook\.office\.com/webhookb2/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}@[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}/IncomingWebhook/[a-z0-9]{32}/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}''' +keywords = [ + "webhook.office.com", + "webhookb2", + "incomingwebhook", +] + +[[rules]] +id = "netlify-access-token" +description = "Detected a Netlify Access Token, potentially compromising web hosting services and site management." +regex = '''(?i)[\w.-]{0,50}?(?:netlify)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{40,46})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["netlify"] + +[[rules]] +id = "new-relic-browser-api-token" +description = "Identified a New Relic ingest browser API token, risking unauthorized access to application performance data and analytics." +regex = '''(?i)[\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(NRJS-[a-f0-9]{19})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["nrjs-"] + +[[rules]] +id = "new-relic-insert-key" +description = "Discovered a New Relic insight insert key, compromising data injection into the platform." +regex = '''(?i)[\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(NRII-[a-z0-9-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["nrii-"] + +[[rules]] +id = "new-relic-user-api-id" +description = "Found a New Relic user API ID, posing a risk to application monitoring services and data integrity." +regex = '''(?i)[\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = [ + "new-relic", + "newrelic", + "new_relic", +] + +[[rules]] +id = "new-relic-user-api-key" +description = "Discovered a New Relic user API Key, which could lead to compromised application insights and performance monitoring." +regex = '''(?i)[\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(NRAK-[a-z0-9]{27})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["nrak"] + +[[rules]] +id = "notion-api-token" +description = "Notion API token" +regex = '''\b(ntn_[0-9]{11}[A-Za-z0-9]{32}[A-Za-z0-9]{3})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = ["ntn_"] + +[[rules]] +id = "npm-access-token" +description = "Uncovered an npm access token, potentially compromising package management and code repository access." +regex = '''(?i)\b(npm_[a-z0-9]{36})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["npm_"] + +[[rules]] +id = "nuget-config-password" +description = "Identified a password within a Nuget config file, potentially compromising package management access." +regex = '''(?i)''' +path = '''(?i)nuget\.config$''' +entropy = 1 +keywords = ["|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = [ + "nytimes", + "new-york-times", + "newyorktimes", +] + +[[rules]] +id = "octopus-deploy-api-key" +description = "Discovered a potential Octopus Deploy API key, risking application deployments and operational security." +regex = '''\b(API-[A-Z0-9]{26})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["api-"] + +[[rules]] +id = "okta-access-token" +description = "Identified an Okta Access Token, which may compromise identity management services and user authentication data." +regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:(?-i:[Oo]kta|OKTA))(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(00[\w=\-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = ["okta"] + +[[rules]] +id = "openai-api-key" +description = "Found an OpenAI API Key, posing a risk of unauthorized access to AI services and data manipulation." +regex = '''\b(sk-(?:proj|svcacct|admin)-(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})T3BlbkFJ(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})\b|sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["t3blbkfj"] + +[[rules]] +id = "openshift-user-token" +description = "Found an OpenShift user token, potentially compromising an OpenShift/Kubernetes cluster." +regex = '''\b(sha256~[\w-]{43})(?:[^\w-]|\z)''' +entropy = 3.5 +keywords = ["sha256~"] + +[[rules]] +id = "perplexity-api-key" +description = "Detected a Perplexity API key, which could lead to unauthorized access to Perplexity AI services and data exposure." +regex = '''\b(pplx-[a-zA-Z0-9]{48})(?:[\x60'"\s;]|\\[nr]|$|\b)''' +entropy = 4 +keywords = ["pplx-"] + +[[rules]] +id = "pkcs12-file" +description = "Found a PKCS #12 file, which commonly contain bundled private keys." +path = '''(?i)(?:^|\/)[^\/]+\.p(?:12|fx)$''' + +[[rules]] +id = "plaid-api-token" +description = "Discovered a Plaid API Token, potentially compromising financial data aggregation and banking services." +regex = '''(?i)[\w.-]{0,50}?(?:plaid)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(access-(?:sandbox|development|production)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["plaid"] + +[[rules]] +id = "plaid-client-id" +description = "Uncovered a Plaid Client ID, which could lead to unauthorized financial service integrations and data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:plaid)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{24})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3.5 +keywords = ["plaid"] + +[[rules]] +id = "plaid-secret-key" +description = "Detected a Plaid Secret key, risking unauthorized access to financial accounts and sensitive transaction data." +regex = '''(?i)[\w.-]{0,50}?(?:plaid)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{30})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3.5 +keywords = ["plaid"] + +[[rules]] +id = "planetscale-api-token" +description = "Identified a PlanetScale API token, potentially compromising database management and operations." +regex = '''\b(pscale_tkn_(?i)[\w=\.-]{32,64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["pscale_tkn_"] + +[[rules]] +id = "planetscale-oauth-token" +description = "Found a PlanetScale OAuth token, posing a risk to database access control and sensitive data integrity." +regex = '''\b(pscale_oauth_[\w=\.-]{32,64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["pscale_oauth_"] + +[[rules]] +id = "planetscale-password" +description = "Discovered a PlanetScale password, which could lead to unauthorized database operations and data breaches." +regex = '''(?i)\b(pscale_pw_(?i)[\w=\.-]{32,64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["pscale_pw_"] + +[[rules]] +id = "postman-api-token" +description = "Uncovered a Postman API token, potentially compromising API testing and development workflows." +regex = '''\b(PMAK-(?i)[a-f0-9]{24}\-[a-f0-9]{34})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["pmak-"] + +[[rules]] +id = "prefect-api-token" +description = "Detected a Prefect API token, risking unauthorized access to workflow management and automation services." +regex = '''\b(pnu_[a-zA-Z0-9]{36})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["pnu_"] + +[[rules]] +id = "private-key" +description = "Identified a Private Key, which may compromise cryptographic security and sensitive data encryption." +regex = '''(?i)-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----[\s\S-]{64,}?KEY(?: BLOCK)?-----''' +keywords = ["-----begin"] + +[[rules]] +id = "privateai-api-token" +description = "Identified a PrivateAI Token, posing a risk of unauthorized access to AI services and data manipulation." +regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:private[_-]?ai)(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = [ + "privateai", + "private_ai", + "private-ai", +] + +[[rules]] +id = "pulumi-api-token" +description = "Found a Pulumi API token, posing a risk to infrastructure as code services and cloud resource management." +regex = '''\b(pul-[a-f0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["pul-"] + +[[rules]] +id = "pypi-upload-token" +description = "Discovered a PyPI upload token, potentially compromising Python package distribution and repository integrity." +regex = '''pypi-AgEIcHlwaS5vcmc[\w-]{50,1000}''' +entropy = 3 +keywords = ["pypi-ageichlwas5vcmc"] + +[[rules]] +id = "rapidapi-access-token" +description = "Uncovered a RapidAPI Access Token, which could lead to unauthorized access to various APIs and data services." +regex = '''(?i)[\w.-]{0,50}?(?:rapidapi)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{50})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["rapidapi"] + +[[rules]] +id = "readme-api-token" +description = "Detected a Readme API token, risking unauthorized documentation management and content exposure." +regex = '''\b(rdme_[a-z0-9]{70})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["rdme_"] + +[[rules]] +id = "rubygems-api-token" +description = "Identified a Rubygem API token, potentially compromising Ruby library distribution and package management." +regex = '''\b(rubygems_[a-f0-9]{48})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["rubygems_"] + +[[rules]] +id = "scalingo-api-token" +description = "Found a Scalingo API token, posing a risk to cloud platform services and application deployment security." +regex = '''\b(tk-us-[\w-]{48})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["tk-us-"] + +[[rules]] +id = "sendbird-access-id" +description = "Discovered a Sendbird Access ID, which could compromise chat and messaging platform integrations." +regex = '''(?i)[\w.-]{0,50}?(?:sendbird)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["sendbird"] + +[[rules]] +id = "sendbird-access-token" +description = "Uncovered a Sendbird Access Token, potentially risking unauthorized access to communication services and user data." +regex = '''(?i)[\w.-]{0,50}?(?:sendbird)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["sendbird"] + +[[rules]] +id = "sendgrid-api-token" +description = "Detected a SendGrid API token, posing a risk of unauthorized email service operations and data exposure." +regex = '''\b(SG\.(?i)[a-z0-9=_\-\.]{66})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["sg."] + +[[rules]] +id = "sendinblue-api-token" +description = "Identified a Sendinblue API token, which may compromise email marketing services and subscriber data privacy." +regex = '''\b(xkeysib-[a-f0-9]{64}\-(?i)[a-z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["xkeysib-"] + +[[rules]] +id = "sentry-access-token" +description = "Found a Sentry.io Access Token (old format), risking unauthorized access to error tracking services and sensitive application data." +regex = '''(?i)[\w.-]{0,50}?(?:sentry)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["sentry"] + +[[rules]] +id = "sentry-org-token" +description = "Found a Sentry.io Organization Token, risking unauthorized access to error tracking services and sensitive application data." +regex = '''\bsntrys_eyJpYXQiO[a-zA-Z0-9+/]{10,200}(?:LCJyZWdpb25fdXJs|InJlZ2lvbl91cmwi|cmVnaW9uX3VybCI6)[a-zA-Z0-9+/]{10,200}={0,2}_[a-zA-Z0-9+/]{43}(?:[^a-zA-Z0-9+/]|\z)''' +entropy = 4.5 +keywords = ["sntrys_eyjpyxqio"] + +[[rules]] +id = "sentry-user-token" +description = "Found a Sentry.io User Token, risking unauthorized access to error tracking services and sensitive application data." +regex = '''\b(sntryu_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3.5 +keywords = ["sntryu_"] + +[[rules]] +id = "settlemint-application-access-token" +description = "Found a Settlemint Application Access Token." +regex = '''\b(sm_aat_[a-zA-Z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["sm_aat"] + +[[rules]] +id = "settlemint-personal-access-token" +description = "Found a Settlemint Personal Access Token." +regex = '''\b(sm_pat_[a-zA-Z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["sm_pat"] + +[[rules]] +id = "settlemint-service-access-token" +description = "Found a Settlemint Service Access Token." +regex = '''\b(sm_sat_[a-zA-Z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["sm_sat"] + +[[rules]] +id = "shippo-api-token" +description = "Discovered a Shippo API token, potentially compromising shipping services and customer order data." +regex = '''\b(shippo_(?:live|test)_[a-fA-F0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["shippo_"] + +[[rules]] +id = "shopify-access-token" +description = "Uncovered a Shopify access token, which could lead to unauthorized e-commerce platform access and data breaches." +regex = '''shpat_[a-fA-F0-9]{32}''' +entropy = 2 +keywords = ["shpat_"] + +[[rules]] +id = "shopify-custom-access-token" +description = "Detected a Shopify custom access token, potentially compromising custom app integrations and e-commerce data security." +regex = '''shpca_[a-fA-F0-9]{32}''' +entropy = 2 +keywords = ["shpca_"] + +[[rules]] +id = "shopify-private-app-access-token" +description = "Identified a Shopify private app access token, risking unauthorized access to private app data and store operations." +regex = '''shppa_[a-fA-F0-9]{32}''' +entropy = 2 +keywords = ["shppa_"] + +[[rules]] +id = "shopify-shared-secret" +description = "Found a Shopify shared secret, posing a risk to application authentication and e-commerce platform security." +regex = '''shpss_[a-fA-F0-9]{32}''' +entropy = 2 +keywords = ["shpss_"] + +[[rules]] +id = "sidekiq-secret" +description = "Discovered a Sidekiq Secret, which could lead to compromised background job processing and application data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:BUNDLE_ENTERPRISE__CONTRIBSYS__COM|BUNDLE_GEMS__CONTRIBSYS__COM)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{8}:[a-f0-9]{8})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = [ + "bundle_enterprise__contribsys__com", + "bundle_gems__contribsys__com", +] + +[[rules]] +id = "sidekiq-sensitive-url" +description = "Uncovered a Sidekiq Sensitive URL, potentially exposing internal job queues and sensitive operation details." +regex = '''(?i)\bhttps?://([a-f0-9]{8}:[a-f0-9]{8})@(?:gems.contribsys.com|enterprise.contribsys.com)(?:[\/|\#|\?|:]|$)''' +keywords = [ + "gems.contribsys.com", + "enterprise.contribsys.com", +] + +[[rules]] +id = "slack-app-token" +description = "Detected a Slack App-level token, risking unauthorized access to Slack applications and workspace data." +regex = '''(?i)xapp-\d-[A-Z0-9]+-\d+-[a-z0-9]+''' +entropy = 2 +keywords = ["xapp"] + +[[rules]] +id = "slack-bot-token" +description = "Identified a Slack Bot token, which may compromise bot integrations and communication channel security." +regex = '''xoxb-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*''' +entropy = 3 +keywords = ["xoxb"] + +[[rules]] +id = "slack-config-access-token" +description = "Found a Slack Configuration access token, posing a risk to workspace configuration and sensitive data access." +regex = '''(?i)xoxe.xox[bp]-\d-[A-Z0-9]{163,166}''' +entropy = 2 +keywords = [ + "xoxe.xoxb-", + "xoxe.xoxp-", +] + +[[rules]] +id = "slack-config-refresh-token" +description = "Discovered a Slack Configuration refresh token, potentially allowing prolonged unauthorized access to configuration settings." +regex = '''(?i)xoxe-\d-[A-Z0-9]{146}''' +entropy = 2 +keywords = ["xoxe-"] + +[[rules]] +id = "slack-legacy-bot-token" +description = "Uncovered a Slack Legacy bot token, which could lead to compromised legacy bot operations and data exposure." +regex = '''xoxb-[0-9]{8,14}-[a-zA-Z0-9]{18,26}''' +entropy = 2 +keywords = ["xoxb"] + +[[rules]] +id = "slack-legacy-token" +description = "Detected a Slack Legacy token, risking unauthorized access to older Slack integrations and user data." +regex = '''xox[os]-\d+-\d+-\d+-[a-fA-F\d]+''' +entropy = 2 +keywords = [ + "xoxo", + "xoxs", +] + +[[rules]] +id = "slack-legacy-workspace-token" +description = "Identified a Slack Legacy Workspace token, potentially compromising access to workspace data and legacy features." +regex = '''xox[ar]-(?:\d-)?[0-9a-zA-Z]{8,48}''' +entropy = 2 +keywords = [ + "xoxa", + "xoxr", +] + +[[rules]] +id = "slack-user-token" +description = "Found a Slack User token, posing a risk of unauthorized user impersonation and data access within Slack workspaces." +regex = '''xox[pe](?:-[0-9]{10,13}){3}-[a-zA-Z0-9-]{28,34}''' +entropy = 2 +keywords = [ + "xoxp-", + "xoxe-", +] + +[[rules]] +id = "slack-webhook-url" +description = "Discovered a Slack Webhook, which could lead to unauthorized message posting and data leakage in Slack channels." +regex = '''(?:https?://)?hooks.slack.com/(?:services|workflows|triggers)/[A-Za-z0-9+/]{43,56}''' +keywords = ["hooks.slack.com"] + +[[rules]] +id = "snyk-api-token" +description = "Uncovered a Snyk API token, potentially compromising software vulnerability scanning and code security." +regex = '''(?i)[\w.-]{0,50}?(?:snyk[_.-]?(?:(?:api|oauth)[_.-]?)?(?:key|token))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["snyk"] + +[[rules]] +id = "sonar-api-token" +description = "Uncovered a Sonar API token, potentially compromising software vulnerability scanning and code security." +regex = '''(?i)[\w.-]{0,50}?(?:sonar[_.-]?(login|token))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}((?:squ_|sqp_|sqa_)?[a-z0-9=_\-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +secretGroup = 2 +keywords = ["sonar"] + +[[rules]] +id = "sourcegraph-access-token" +description = "Sourcegraph is a code search and navigation engine." +regex = '''(?i)\b(\b(sgp_(?:[a-fA-F0-9]{16}|local)_[a-fA-F0-9]{40}|sgp_[a-fA-F0-9]{40}|[a-fA-F0-9]{40})\b)(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = [ + "sgp_", + "sourcegraph", +] + +[[rules]] +id = "square-access-token" +description = "Detected a Square Access Token, risking unauthorized payment processing and financial transaction exposure." +regex = '''\b((?:EAAA|sq0atp-)[\w-]{22,60})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = [ + "sq0atp-", + "eaaa", +] + +[[rules]] +id = "squarespace-access-token" +description = "Identified a Squarespace Access Token, which may compromise website management and content control on Squarespace." +regex = '''(?i)[\w.-]{0,50}?(?:squarespace)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["squarespace"] + +[[rules]] +id = "stripe-access-token" +description = "Found a Stripe Access Token, posing a risk to payment processing services and sensitive financial data." +regex = '''\b((?:sk|rk)_(?:test|live|prod)_[a-zA-Z0-9]{10,99})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = [ + "sk_test", + "sk_live", + "sk_prod", + "rk_test", + "rk_live", + "rk_prod", +] + +[[rules]] +id = "sumologic-access-id" +description = "Discovered a SumoLogic Access ID, potentially compromising log management services and data analytics integrity." +regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:(?-i:[Ss]umo|SUMO))(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(su[a-zA-Z0-9]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["sumo"] + +[[rules]] +id = "sumologic-access-token" +description = "Uncovered a SumoLogic Access Token, which could lead to unauthorized access to log data and analytics insights." +regex = '''(?i)[\w.-]{0,50}?(?:(?-i:[Ss]umo|SUMO))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["sumo"] + +[[rules]] +id = "telegram-bot-api-token" +description = "Detected a Telegram Bot API Token, risking unauthorized bot operations and message interception on Telegram." +regex = '''(?i)[\w.-]{0,50}?(?:telegr)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9]{5,16}:(?-i:A)[a-z0-9_\-]{34})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["telegr"] + +[[rules]] +id = "travisci-access-token" +description = "Identified a Travis CI Access Token, potentially compromising continuous integration services and codebase security." +regex = '''(?i)[\w.-]{0,50}?(?:travis)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{22})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["travis"] + +[[rules]] +id = "twilio-api-key" +description = "Found a Twilio API Key, posing a risk to communication services and sensitive customer interaction data." +regex = '''SK[0-9a-fA-F]{32}''' +entropy = 3 +keywords = ["sk"] + +[[rules]] +id = "twitch-api-token" +description = "Discovered a Twitch API token, which could compromise streaming services and account integrations." +regex = '''(?i)[\w.-]{0,50}?(?:twitch)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{30})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["twitch"] + +[[rules]] +id = "twitter-access-secret" +description = "Uncovered a Twitter Access Secret, potentially risking unauthorized Twitter integrations and data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{45})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["twitter"] + +[[rules]] +id = "twitter-access-token" +description = "Detected a Twitter Access Token, posing a risk of unauthorized account operations and social media data exposure." +regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9]{15,25}-[a-zA-Z0-9]{20,40})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["twitter"] + +[[rules]] +id = "twitter-api-key" +description = "Identified a Twitter API Key, which may compromise Twitter application integrations and user data security." +regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{25})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["twitter"] + +[[rules]] +id = "twitter-api-secret" +description = "Found a Twitter API Secret, risking the security of Twitter app integrations and sensitive data access." +regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{50})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["twitter"] + +[[rules]] +id = "twitter-bearer-token" +description = "Discovered a Twitter Bearer Token, potentially compromising API access and data retrieval from Twitter." +regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(A{22}[a-zA-Z0-9%]{80,100})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["twitter"] + +[[rules]] +id = "typeform-api-token" +description = "Uncovered a Typeform API token, which could lead to unauthorized survey management and data collection." +regex = '''(?i)[\w.-]{0,50}?(?:typeform)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(tfp_[a-z0-9\-_\.=]{59})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["tfp_"] + +[[rules]] +id = "vault-batch-token" +description = "Detected a Vault Batch Token, risking unauthorized access to secret management services and sensitive data." +regex = '''\b(hvb\.[\w-]{138,300})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = ["hvb."] + +[[rules]] +id = "vault-service-token" +description = "Identified a Vault Service Token, potentially compromising infrastructure security and access to sensitive credentials." +regex = '''\b((?:hvs\.[\w-]{90,120}|s\.(?i:[a-z0-9]{24})))(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3.5 +keywords = [ + "hvs.", + "s.", +] +[[rules.allowlists]] +regexes = [ + '''s\.[A-Za-z]{24}''', +] + +[[rules]] +id = "yandex-access-token" +description = "Found a Yandex Access Token, posing a risk to Yandex service integrations and user data privacy." +regex = '''(?i)[\w.-]{0,50}?(?:yandex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(t1\.[A-Z0-9a-z_-]+[=]{0,2}\.[A-Z0-9a-z_-]{86}[=]{0,2})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["yandex"] + +[[rules]] +id = "yandex-api-key" +description = "Discovered a Yandex API Key, which could lead to unauthorized access to Yandex services and data manipulation." +regex = '''(?i)[\w.-]{0,50}?(?:yandex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(AQVN[A-Za-z0-9_\-]{35,38})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["yandex"] + +[[rules]] +id = "yandex-aws-access-token" +description = "Uncovered a Yandex AWS Access Token, potentially compromising cloud resource access and data security on Yandex Cloud." +regex = '''(?i)[\w.-]{0,50}?(?:yandex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(YC[a-zA-Z0-9_\-]{38})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["yandex"] + +[[rules]] +id = "zendesk-secret-key" +description = "Detected a Zendesk Secret Key, risking unauthorized access to customer support services and sensitive ticketing data." +regex = '''(?i)[\w.-]{0,50}?(?:zendesk)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["zendesk"] + diff --git a/config/rule.go b/config/rule.go new file mode 100644 index 0000000..3ed2244 --- /dev/null +++ b/config/rule.go @@ -0,0 +1,107 @@ +package config + +import ( + "errors" + "fmt" + "strings" + + "github.com/zricethezav/gitleaks/v8/regexp" +) + +// Rules contain information that define details on how to detect secrets +type Rule struct { + // RuleID is a unique identifier for this rule + RuleID string + + // Description is the description of the rule. + Description string + + // Entropy is a float representing the minimum shannon + // entropy a regex group must have to be considered a secret. + Entropy float64 + + // SecretGroup is an int used to extract secret from regex + // match and used as the group that will have its entropy + // checked if `entropy` is set. + SecretGroup int + + // Regex is a golang regular expression used to detect secrets. + Regex *regexp.Regexp + + // Path is a golang regular expression used to + // filter secrets by path + Path *regexp.Regexp + + // Tags is an array of strings used for metadata + // and reporting purposes. + Tags []string + + // Keywords are used for pre-regex check filtering. Rules that contain + // keywords will perform a quick string compare check to make sure the + // keyword(s) are in the content being scanned. + Keywords []string + + // Allowlists allows a rule to be ignored for specific commits, paths, regexes, and/or stopwords. + Allowlists []*Allowlist + + // validated is an internal flag to track whether `Validate()` has been called. + validated bool + + // If a rule has RequiredRules, it makes the rule dependent on the RequiredRules. + // In otherwords, this rule is now a composite rule. + RequiredRules []*Required + + SkipReport bool +} + +type Required struct { + RuleID string + WithinLines *int + WithinColumns *int +} + +// Validate guards against common misconfigurations. +func (r *Rule) Validate() error { + if r.validated { + return nil + } + + // Ensure |id| is present. + if strings.TrimSpace(r.RuleID) == "" { + // Try to provide helpful context, since |id| is empty. + var sb strings.Builder + if r.Description != "" { + sb.WriteString(", description: " + r.Description) + } + if r.Regex != nil { + sb.WriteString(", regex: " + r.Regex.String()) + } + if r.Path != nil { + sb.WriteString(", path: " + r.Path.String()) + } + return errors.New("rule |id| is missing or empty" + sb.String()) + } + + // Ensure the rule actually matches something. + if r.Regex == nil && r.Path == nil { + return errors.New(r.RuleID + ": both |regex| and |path| are empty, this rule will have no effect") + } + + // Ensure |secretGroup| works. + if r.Regex != nil && r.SecretGroup > r.Regex.NumSubexp() { + return fmt.Errorf("%s: invalid regex secret group %d, max regex secret group %d", r.RuleID, r.SecretGroup, r.Regex.NumSubexp()) + } + + for _, allowlist := range r.Allowlists { + // This will probably never happen. + if allowlist == nil { + continue + } + if err := allowlist.Validate(); err != nil { + return fmt.Errorf("%s: %w", r.RuleID, err) + } + } + + r.validated = true + return nil +} diff --git a/config/utils.go b/config/utils.go new file mode 100644 index 0000000..e9b2783 --- /dev/null +++ b/config/utils.go @@ -0,0 +1,40 @@ +package config + +import ( + "strings" + + "github.com/zricethezav/gitleaks/v8/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 +} + +// joinRegexOr combines multiple |patterns| into a single *regexp.Regexp. +func joinRegexOr(patterns []*regexp.Regexp) *regexp.Regexp { + var sb strings.Builder + sb.WriteString("(?:") + for i, pat := range patterns { + sb.WriteString(pat.String()) + if i != len(patterns)-1 { + sb.WriteString("|") + } + } + sb.WriteString(")") + return regexp.MustCompile(sb.String()) +} diff --git a/detect/baseline.go b/detect/baseline.go new file mode 100644 index 0000000..d0520fd --- /dev/null +++ b/detect/baseline.go @@ -0,0 +1,81 @@ +package detect + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/zricethezav/gitleaks/v8/report" +) + +func IsNew(finding report.Finding, redact uint, baseline []report.Finding) bool { + // Explicitly testing each property as it gives significantly better performance in comparison to cmp.Equal(). Drawback is that + // the code requires maintenance if/when the Finding struct changes + for _, b := range baseline { + if finding.RuleID == b.RuleID && + finding.Description == b.Description && + finding.StartLine == b.StartLine && + finding.EndLine == b.EndLine && + finding.StartColumn == b.StartColumn && + finding.EndColumn == b.EndColumn && + (redact > 0 || (finding.Match == b.Match && finding.Secret == b.Secret)) && + finding.File == b.File && + finding.Commit == b.Commit && + finding.Author == b.Author && + finding.Email == b.Email && + finding.Date == b.Date && + finding.Message == b.Message && + // Omit checking finding.Fingerprint - if the format of the fingerprint changes, the users will see unexpected behaviour + finding.Entropy == b.Entropy { + return false + } + } + return true +} + +func LoadBaseline(baselinePath string) ([]report.Finding, error) { + bytes, err := os.ReadFile(baselinePath) + if err != nil { + return nil, fmt.Errorf("could not open %s", baselinePath) + } + + var previousFindings []report.Finding + err = json.Unmarshal(bytes, &previousFindings) + if err != nil { + return nil, fmt.Errorf("the format of the file %s is not supported", baselinePath) + } + + return previousFindings, nil +} + +func (d *Detector) AddBaseline(baselinePath string, source string) error { + if baselinePath != "" { + absoluteSource, err := filepath.Abs(source) + if err != nil { + return err + } + + absoluteBaseline, err := filepath.Abs(baselinePath) + if err != nil { + return err + } + + relativeBaseline, err := filepath.Rel(absoluteSource, absoluteBaseline) + if err != nil { + return err + } + + baseline, err := LoadBaseline(baselinePath) + if err != nil { + return err + } + + d.baseline = baseline + baselinePath = relativeBaseline + + } + + d.baselinePath = baselinePath + return nil +} diff --git a/detect/baseline_test.go b/detect/baseline_test.go new file mode 100644 index 0000000..96a8402 --- /dev/null +++ b/detect/baseline_test.go @@ -0,0 +1,232 @@ +package detect + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zricethezav/gitleaks/v8/report" +) + +func TestIsNew(t *testing.T) { + t.Parallel() + tests := map[string]struct { + findings report.Finding + redact uint + baseline []report.Finding + expect bool + }{ + // new + "new - commit doesn't match baseline": { + findings: report.Finding{ + Commit: "0000", + Author: "a", + }, + baseline: []report.Finding{ + { + Commit: "0002", + Author: "a", + }, + }, + expect: true, + }, + "new - redacted, different baseline": { + findings: report.Finding{ + RuleID: "private-key", + Description: "Identified a Private Key, which may compromise cryptographic security and sensitive data encryption.", + StartLine: 1, + EndLine: 15, + StartColumn: 1, + EndColumn: 30, + Match: "REDACTED", + Secret: "REDACTED", + File: "key.txt", + Commit: "6d3ba1f7653822c0f8ac9a9af56daaa2cd8bbcad", + Entropy: 5.9834013, + Author: "James Bond", + Email: "jbond@gov.co.uk", + Date: "2025-03-02T15:10:40Z", + Message: "init", + Fingerprint: "6d3ba1f7653822c0f8ac9a9af56daaa2cd8bbcad:key.txt:private-key:1", + }, + baseline: []report.Finding{ + { + RuleID: "private-key", + Description: "Identified a Private Key, which may compromise cryptographic security and sensitive data encryption.", + StartLine: 1, + EndLine: 15, + StartColumn: 1, + EndColumn: 30, + Match: "-----BEGIN RSA PRIVATE KEY-----\nMIICWgIBAAKBgFIckgeuo80H6skLd1FKYfJC75/tnmtDWO4Rf2AFqrYZdu71VKGR\noGfEVl7AmvxTd9u6tnPtWjAeu9k2VMQcOtXEwgU0A6H09EBcS1EVN/I8pcNw1qjO\nkJ7ZA8AhZk/OpVAK7665CEny7ISRNZnx1nPaHjlb8lebPzlWOvxX9wjbAgMBAAEC\ngYBN6wqv+4s4juC/cwAAxeL4L4iQbL497yS+lSAYEIiUUMnJrEhpIXXjwi5rr73i\n35oHisCEdaF1tFRxpNr/VgKFsM1KqQUZvCVRE9Rokfe23QkQDvcxh9CI/Ah9Eofp\nx/m5DjSsRKrbIpOOAC3J3B/s02HRmxy8tRYnQVqWXzAH8QJBAJdBgXi62KI1eytU\n7l3Q8ymkS1OHzSOGBEYPpZZQ7WRpZlv/06cKfJBT/dGgA4z9i9ySs8cWUoh+FGYX\nlkDB4c0CQQCK+TwfAFvrkSWorZ9Gjb6y2LZQPUufTzJNhzhK5XObCDbwyMXEM/Vs\newiyUFljlI/A9PjcrmkgrDLUMD4+og1HAkAs2t01W1uhBvEm0YH6yltCDxnThKM+\nFKEx0bQOVqN/so4LXFt83uw/tNjBkI1dA1e1qr+rm6AQICuWdwo03ApFAkBktes4\nuCTk2GHHFFM5aN0KdHviOBlGULkub9B+jjsx3UkbQxP2dITlYV/TAOFWhcGLXru+\nCPKMR93p4TAqaXtfAkA+ZZDb0mA9rtaetJlSoo6XgwI/+kqltADch9dcyqYBHwjr\nAEkzUKvmCxNAK4GEPA79FZFp30kDx+buysyeX9qY\n-----END RSA PRIVATE KEY-----", + Secret: "-----BEGIN RSA PRIVATE KEY-----\nMIICWgIBAAKBgFIckgeuo80H6skLd1FKYfJC75/tnmtDWO4Rf2AFqrYZdu71VKGR\noGfEVl7AmvxTd9u6tnPtWjAeu9k2VMQcOtXEwgU0A6H09EBcS1EVN/I8pcNw1qjO\nkJ7ZA8AhZk/OpVAK7665CEny7ISRNZnx1nPaHjlb8lebPzlWOvxX9wjbAgMBAAEC\ngYBN6wqv+4s4juC/cwAAxeL4L4iQbL497yS+lSAYEIiUUMnJrEhpIXXjwi5rr73i\n35oHisCEdaF1tFRxpNr/VgKFsM1KqQUZvCVRE9Rokfe23QkQDvcxh9CI/Ah9Eofp\nx/m5DjSsRKrbIpOOAC3J3B/s02HRmxy8tRYnQVqWXzAH8QJBAJdBgXi62KI1eytU\n7l3Q8ymkS1OHzSOGBEYPpZZQ7WRpZlv/06cKfJBT/dGgA4z9i9ySs8cWUoh+FGYX\nlkDB4c0CQQCK+TwfAFvrkSWorZ9Gjb6y2LZQPUufTzJNhzhK5XObCDbwyMXEM/Vs\newiyUFljlI/A9PjcrmkgrDLUMD4+og1HAkAs2t01W1uhBvEm0YH6yltCDxnThKM+\nFKEx0bQOVqN/so4LXFt83uw/tNjBkI1dA1e1qr+rm6AQICuWdwo03ApFAkBktes4\nuCTk2GHHFFM5aN0KdHviOBlGULkub9B+jjsx3UkbQxP2dITlYV/TAOFWhcGLXru+\nCPKMR93p4TAqaXtfAkA+ZZDb0mA9rtaetJlSoo6XgwI/+kqltADch9dcyqYBHwjr\nAEkzUKvmCxNAK4GEPA79FZFp30kDx+buysyeX9qY\n-----END RSA PRIVATE KEY-----", + File: "key.txt", + Commit: "e55e00ca1690a6b5b612d28b3d9ada3fd1775ac4", + Entropy: 5.9834013, + Author: "James Bond", + Email: "jbond@gov.co.uk", + Date: "2025-02-02T17:45:30Z", + Message: "init", + Fingerprint: "e55e00ca1690a6b5b612d28b3d9ada3fd1775ac4:key.txt:private-key:1", + }, + }, + expect: true, + }, + + // not new + "not new - commit+author matches": { + findings: report.Finding{ + Commit: "0000", + Author: "a", + }, + baseline: []report.Finding{ + { + Commit: "0000", + Author: "a", + }, + }, + expect: false, + }, + "not new - commit+author matches, tags ignored": { + findings: report.Finding{ + Commit: "0000", + Author: "a", + Tags: []string{"a", "b"}, + }, + baseline: []report.Finding{ + { + Commit: "0000", + Author: "a", + Tags: []string{"a", "c"}, + }, + }, + expect: false, // Updated tags doesn't make it a new finding + }, + "not new - redacted, everything else matches": { + findings: report.Finding{ + RuleID: "private-key", + Description: "Identified a Private Key, which may compromise cryptographic security and sensitive data encryption.", + StartLine: 1, + EndLine: 15, + StartColumn: 1, + EndColumn: 30, + Match: "REDACTED", + Secret: "REDACTED", + File: "key.txt", + Commit: "e55e00ca1690a6b5b612d28b3d9ada3fd1775ac4", + Entropy: 5.9834013, + Author: "James Bond", + Email: "jbond@gov.co.uk", + Date: "2025-02-02T17:45:30Z", + Message: "init", + Fingerprint: "e55e00ca1690a6b5b612d28b3d9ada3fd1775ac4:key.txt:private-key:1", + }, + redact: 100, + baseline: []report.Finding{ + { + RuleID: "private-key", + Description: "Identified a Private Key, which may compromise cryptographic security and sensitive data encryption.", + StartLine: 1, + EndLine: 15, + StartColumn: 1, + EndColumn: 30, + Match: "-----BEGIN RSA PRIVATE KEY-----\nMIICWgIBAAKBgFIckgeuo80H6skLd1FKYfJC75/tnmtDWO4Rf2AFqrYZdu71VKGR\noGfEVl7AmvxTd9u6tnPtWjAeu9k2VMQcOtXEwgU0A6H09EBcS1EVN/I8pcNw1qjO\nkJ7ZA8AhZk/OpVAK7665CEny7ISRNZnx1nPaHjlb8lebPzlWOvxX9wjbAgMBAAEC\ngYBN6wqv+4s4juC/cwAAxeL4L4iQbL497yS+lSAYEIiUUMnJrEhpIXXjwi5rr73i\n35oHisCEdaF1tFRxpNr/VgKFsM1KqQUZvCVRE9Rokfe23QkQDvcxh9CI/Ah9Eofp\nx/m5DjSsRKrbIpOOAC3J3B/s02HRmxy8tRYnQVqWXzAH8QJBAJdBgXi62KI1eytU\n7l3Q8ymkS1OHzSOGBEYPpZZQ7WRpZlv/06cKfJBT/dGgA4z9i9ySs8cWUoh+FGYX\nlkDB4c0CQQCK+TwfAFvrkSWorZ9Gjb6y2LZQPUufTzJNhzhK5XObCDbwyMXEM/Vs\newiyUFljlI/A9PjcrmkgrDLUMD4+og1HAkAs2t01W1uhBvEm0YH6yltCDxnThKM+\nFKEx0bQOVqN/so4LXFt83uw/tNjBkI1dA1e1qr+rm6AQICuWdwo03ApFAkBktes4\nuCTk2GHHFFM5aN0KdHviOBlGULkub9B+jjsx3UkbQxP2dITlYV/TAOFWhcGLXru+\nCPKMR93p4TAqaXtfAkA+ZZDb0mA9rtaetJlSoo6XgwI/+kqltADch9dcyqYBHwjr\nAEkzUKvmCxNAK4GEPA79FZFp30kDx+buysyeX9qY\n-----END RSA PRIVATE KEY-----", + Secret: "-----BEGIN RSA PRIVATE KEY-----\nMIICWgIBAAKBgFIckgeuo80H6skLd1FKYfJC75/tnmtDWO4Rf2AFqrYZdu71VKGR\noGfEVl7AmvxTd9u6tnPtWjAeu9k2VMQcOtXEwgU0A6H09EBcS1EVN/I8pcNw1qjO\nkJ7ZA8AhZk/OpVAK7665CEny7ISRNZnx1nPaHjlb8lebPzlWOvxX9wjbAgMBAAEC\ngYBN6wqv+4s4juC/cwAAxeL4L4iQbL497yS+lSAYEIiUUMnJrEhpIXXjwi5rr73i\n35oHisCEdaF1tFRxpNr/VgKFsM1KqQUZvCVRE9Rokfe23QkQDvcxh9CI/Ah9Eofp\nx/m5DjSsRKrbIpOOAC3J3B/s02HRmxy8tRYnQVqWXzAH8QJBAJdBgXi62KI1eytU\n7l3Q8ymkS1OHzSOGBEYPpZZQ7WRpZlv/06cKfJBT/dGgA4z9i9ySs8cWUoh+FGYX\nlkDB4c0CQQCK+TwfAFvrkSWorZ9Gjb6y2LZQPUufTzJNhzhK5XObCDbwyMXEM/Vs\newiyUFljlI/A9PjcrmkgrDLUMD4+og1HAkAs2t01W1uhBvEm0YH6yltCDxnThKM+\nFKEx0bQOVqN/so4LXFt83uw/tNjBkI1dA1e1qr+rm6AQICuWdwo03ApFAkBktes4\nuCTk2GHHFFM5aN0KdHviOBlGULkub9B+jjsx3UkbQxP2dITlYV/TAOFWhcGLXru+\nCPKMR93p4TAqaXtfAkA+ZZDb0mA9rtaetJlSoo6XgwI/+kqltADch9dcyqYBHwjr\nAEkzUKvmCxNAK4GEPA79FZFp30kDx+buysyeX9qY\n-----END RSA PRIVATE KEY-----", + File: "key.txt", + Commit: "e55e00ca1690a6b5b612d28b3d9ada3fd1775ac4", + Entropy: 5.9834013, + Author: "James Bond", + Email: "jbond@gov.co.uk", + Date: "2025-02-02T17:45:30Z", + Message: "init", + Fingerprint: "e55e00ca1690a6b5b612d28b3d9ada3fd1775ac4:key.txt:private-key:1", + }, + }, + expect: false, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, test.expect, IsNew(test.findings, test.redact, test.baseline)) + }) + } +} + +func TestFileLoadBaseline(t *testing.T) { + t.Parallel() + tests := []struct { + Filename string + ExpectedError error + }{ + { + Filename: "../testdata/baseline/baseline.csv", + ExpectedError: errors.New("the format of the file ../testdata/baseline/baseline.csv is not supported"), + }, + { + Filename: "../testdata/baseline/baseline.sarif", + ExpectedError: errors.New("the format of the file ../testdata/baseline/baseline.sarif is not supported"), + }, + { + Filename: "../testdata/baseline/notfound.json", + ExpectedError: errors.New("could not open ../testdata/baseline/notfound.json"), + }, + } + + for _, test := range tests { + _, err := LoadBaseline(test.Filename) + assert.Equal(t, test.ExpectedError, err) + } +} + +func TestIgnoreIssuesInBaseline(t *testing.T) { + t.Parallel() + tests := []struct { + findings []report.Finding + baseline []report.Finding + expectCount int + }{ + { + findings: []report.Finding{ + { + Author: "a", + Commit: "5", + }, + }, + baseline: []report.Finding{ + { + Author: "a", + Commit: "5", + }, + }, + expectCount: 0, + }, + { + findings: []report.Finding{ + { + Author: "a", + Commit: "5", + Fingerprint: "a", + }, + }, + baseline: []report.Finding{ + { + Author: "a", + Commit: "5", + Fingerprint: "b", + }, + }, + expectCount: 0, + }, + } + + for _, test := range tests { + d, err := NewDetectorDefaultConfig() + require.NoError(t, err) + d.baseline = test.baseline + for _, finding := range test.findings { + d.AddFinding(finding) + } + assert.Len(t, d.findings, test.expectCount) + } +} diff --git a/detect/codec/ascii.go b/detect/codec/ascii.go new file mode 100644 index 0000000..2ea2ca2 --- /dev/null +++ b/detect/codec/ascii.go @@ -0,0 +1,34 @@ +package codec + +var printableASCII [256]bool + +func init() { + for b := 0; b < len(printableASCII); b++ { + if '\x08' < b && b < '\x7f' { + printableASCII[b] = true + } + } +} + +// isPrintableASCII returns true if all bytes are printable ASCII +func isPrintableASCII(b []byte) bool { + for _, c := range b { + if !printableASCII[c] { + return false + } + } + + return true +} + +// hasByte can be used to check if a string has at least one of the provided +// bytes. Note: make sure byteset is long enough to handle the largest byte in +// the string. +func hasByte(data string, byteset []bool) bool { + for i := 0; i < len(data); i++ { + if byteset[data[i]] { + return true + } + } + return false +} diff --git a/detect/codec/base64.go b/detect/codec/base64.go new file mode 100644 index 0000000..8655dc9 --- /dev/null +++ b/detect/codec/base64.go @@ -0,0 +1,39 @@ +package codec + +import ( + "encoding/base64" +) + +// likelyBase64Chars is a set of characters that you would expect to find at +// least one of in base64 encoded data. This risks missing about 1% of +// base64 encoded data that doesn't contain these characters, but gives you +// the performance gain of not trying to decode a lot of long symbols in code. +var likelyBase64Chars = make([]bool, 256) + +func init() { + for _, c := range `0123456789+/-_` { + likelyBase64Chars[c] = true + } +} + +// decodeBase64 decodes base64 encoded printable ASCII characters +func decodeBase64(encodedValue string) string { + // Exit early if it doesn't seem like base64 + if !hasByte(encodedValue, likelyBase64Chars) { + return "" + } + + // Try standard base64 decoding + decodedValue, err := base64.StdEncoding.DecodeString(encodedValue) + if err == nil && isPrintableASCII(decodedValue) { + return string(decodedValue) + } + + // Try base64url decoding + decodedValue, err = base64.RawURLEncoding.DecodeString(encodedValue) + if err == nil && isPrintableASCII(decodedValue) { + return string(decodedValue) + } + + return "" +} diff --git a/detect/codec/decoder.go b/detect/codec/decoder.go new file mode 100644 index 0000000..1a68d4f --- /dev/null +++ b/detect/codec/decoder.go @@ -0,0 +1,105 @@ +package codec + +import ( + "bytes" + + "github.com/zricethezav/gitleaks/v8/logging" +) + +// Decoder decodes various types of data in place +type Decoder struct { + decodedMap map[string]string +} + +// NewDecoder creates a default decoder struct +func NewDecoder() *Decoder { + return &Decoder{ + decodedMap: make(map[string]string), + } +} + +// Decode returns the data with the values decoded in place along with the +// encoded segment meta data for the next pass of decoding +func (d *Decoder) Decode(data string, predecessors []*EncodedSegment) (string, []*EncodedSegment) { + segments := d.findEncodedSegments(data, predecessors) + + if len(segments) > 0 { + result := bytes.NewBuffer(make([]byte, 0, len(data))) + encodedStart := 0 + for _, segment := range segments { + result.WriteString(data[encodedStart:segment.encoded.start]) + result.WriteString(segment.decodedValue) + encodedStart = segment.encoded.end + } + + result.WriteString(data[encodedStart:]) + return result.String(), segments + } + + return data, segments +} + +// findEncodedSegments finds the encoded segments in the data +func (d *Decoder) findEncodedSegments(data string, predecessors []*EncodedSegment) []*EncodedSegment { + if len(data) == 0 { + return []*EncodedSegment{} + } + + decodedShift := 0 + encodingMatches := findEncodingMatches(data) + segments := make([]*EncodedSegment, 0, len(encodingMatches)) + for _, m := range encodingMatches { + encodedValue := data[m.start:m.end] + decodedValue, alreadyDecoded := d.decodedMap[encodedValue] + + if !alreadyDecoded { + decodedValue = m.encoding.decode(encodedValue) + d.decodedMap[encodedValue] = decodedValue + } + + if len(decodedValue) == 0 { + continue + } + + segment := &EncodedSegment{ + predecessors: predecessors, + original: toOriginal(predecessors, m.startEnd), + encoded: m.startEnd, + decoded: startEnd{ + m.start + decodedShift, + m.start + decodedShift + len(decodedValue), + }, + decodedValue: decodedValue, + encodings: m.encoding.kind, + depth: 1, + } + + // Shift decoded start and ends based on size changes + decodedShift += len(decodedValue) - len(encodedValue) + + // Adjust depth and encoding if applicable + if len(segment.predecessors) != 0 { + // Set the depth based on the predecessors' depth in the previous pass + segment.depth = 1 + segment.predecessors[0].depth + // Adjust encodings + for _, p := range segment.predecessors { + if segment.encoded.overlaps(p.decoded) { + segment.encodings |= p.encodings + } + } + } + + segments = append(segments, segment) + logging.Debug(). + Str("decoder", m.encoding.kind.String()). + Msgf( + "segment found: original=%s pos=%s: %q -> %q", + segment.original, + segment.encoded, + encodedValue, + segment.decodedValue, + ) + } + + return segments +} diff --git a/detect/codec/decoder_test.go b/detect/codec/decoder_test.go new file mode 100644 index 0000000..e6cd593 --- /dev/null +++ b/detect/codec/decoder_test.go @@ -0,0 +1,144 @@ +package codec + +import ( + "encoding/hex" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecode(t *testing.T) { + tests := []struct { + chunk string + expected string + name string + }{ + { + name: "only b64 chunk", + chunk: `bG9uZ2VyLWVuY29kZWQtc2VjcmV0LXRlc3Q=`, + expected: `longer-encoded-secret-test`, + }, + { + name: "mixed content", + chunk: `token: bG9uZ2VyLWVuY29kZWQtc2VjcmV0LXRlc3Q=`, + expected: `token: longer-encoded-secret-test`, + }, + { + name: "no chunk", + chunk: ``, + expected: ``, + }, + { + name: "env var (looks like all b64 decodable but has `=` in the middle)", + chunk: `some-encoded-secret=dGVzdC1zZWNyZXQtdmFsdWU=`, + expected: `some-encoded-secret=test-secret-value`, + }, + { + name: "has longer b64 inside", + chunk: `some-encoded-secret="bG9uZ2VyLWVuY29kZWQtc2VjcmV0LXRlc3Q="`, + expected: `some-encoded-secret="longer-encoded-secret-test"`, + }, + { + name: "many possible i := 0substrings", + chunk: `Many substrings in this slack message could be base64 decoded + but only dGhpcyBlbmNhcHN1bGF0ZWQgc2VjcmV0 should be decoded.`, + expected: `Many substrings in this slack message could be base64 decoded + but only this encapsulated secret should be decoded.`, + }, + { + name: "b64-url-safe: only b64 chunk", + chunk: `bG9uZ2VyLWVuY29kZWQtc2VjcmV0LXRlc3Q`, + expected: `longer-encoded-secret-test`, + }, + { + name: "b64-url-safe: mixed content", + chunk: `token: bG9uZ2VyLWVuY29kZWQtc2VjcmV0LXRlc3Q`, + expected: `token: longer-encoded-secret-test`, + }, + { + name: "b64-url-safe: env var (looks like all b64 decodable but has `=` in the middle)", + chunk: `some-encoded-secret=dGVzdC1zZWNyZXQtdmFsdWU=`, + expected: `some-encoded-secret=test-secret-value`, + }, + { + name: "b64-url-safe: has longer b64 inside", + chunk: `some-encoded-secret="bG9uZ2VyLWVuY29kZWQtc2VjcmV0LXRlc3Q"`, + expected: `some-encoded-secret="longer-encoded-secret-test"`, + }, + { + name: "b64-url-safe: hyphen url b64", + chunk: `Z2l0bGVha3M-PmZpbmRzLXNlY3JldHM`, + expected: `gitleaks>>finds-secrets`, + }, + { + name: "b64-url-safe: underscore url b64", + chunk: `YjY0dXJsc2FmZS10ZXN0LXNlY3JldC11bmRlcnNjb3Jlcz8_`, + expected: `b64urlsafe-test-secret-underscores??`, + }, + { + name: "invalid base64 string", + chunk: `a3d3fa7c2bb99e469ba55e5834ce79ee4853a8a3`, + expected: `a3d3fa7c2bb99e469ba55e5834ce79ee4853a8a3`, + }, + { + name: "url encoded value", + chunk: `secret%3D%22q%24%21%40%23%24%25%5E%26%2A%28%20asdf%22`, + expected: `secret="q$!@#$%^&*( asdf"`, + }, + { + name: "hex encoded value", + chunk: `secret="466973684D617048756E6B79212121363334"`, + expected: `secret="FishMapHunky!!!634"`, + }, + { + name: "unicode encoded value", + chunk: `secret=U+0061 U+0062 U+0063 U+0064 U+0065 U+0066`, + expected: "secret=abcdef", + }, + { + name: "unicode encoded value backslashed", + chunk: `secret=\\u0068\\u0065\\u006c\\u006c\\u006f\\u0020\\u0077\\u006f\\u0072\\u006c\\u0064\\u0020\\u0064\\u0075\\u0064\\u0065`, + expected: "secret=hello world dude", + }, + { + name: "unicode encoded value backslashed mixed w/ hex", + chunk: `secret=\u0068\u0065\u006c\u006c\u006f\u0020\u0077\u006f\u0072\u006c\u0064 6C6F76656C792070656F706C65206F66206561727468`, + expected: "secret=hello world lovely people of earth", + }, + } + + decoder := NewDecoder() + fullDecode := func(data string) string { + segments := []*EncodedSegment{} + for { + data, segments = decoder.Decode(data, segments) + if len(segments) == 0 { + return data + } + } + } + + // Test value decoding + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, fullDecode(tt.chunk)) + }) + } + + // Percent encode the values to test percent decoding + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encodedChunk := url.PathEscape(tt.chunk) + assert.Equal(t, tt.expected, fullDecode(encodedChunk)) + }) + } + + // Hex encode the values to test hex decoding + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encodedChunk := hex.EncodeToString([]byte(tt.chunk)) + assert.Equal(t, tt.expected, fullDecode(encodedChunk)) + }) + } +} diff --git a/detect/codec/encodings.go b/detect/codec/encodings.go new file mode 100644 index 0000000..e97d153 --- /dev/null +++ b/detect/codec/encodings.go @@ -0,0 +1,161 @@ +package codec + +import ( + "fmt" + "math" + "strings" + + "github.com/zricethezav/gitleaks/v8/regexp" +) + +var ( + // encodingsRe is a regex built by combining all the encoding patterns + // into named capture groups so that a single pass can detect multiple + // encodings + encodingsRe *regexp.Regexp + // encodings contains all the encoding configurations for the detector. + // The precedence is important. You want more specific encodings to + // have a higher precedence or encodings that partially encode the + // values (e.g. percent) unlike encodings that fully encode the string + // (e.g. base64). If two encoding matches overlap the decoder will use + // this order to determine which encoding should wait till the next pass. + encodings = []*encoding{ + { + kind: percentKind, + pattern: `%[0-9A-Fa-f]{2}(?:.*%[0-9A-Fa-f]{2})?`, + decode: decodePercent, + }, + { + kind: unicodeKind, + pattern: `(?:(?:U\+[a-fA-F0-9]{4}(?:\s|$))+|(?i)(?:\\{1,2}u[a-fA-F0-9]{4})+)`, + decode: decodeUnicode, + }, + { + kind: hexKind, + pattern: `[0-9A-Fa-f]{32,}`, + decode: decodeHex, + }, + { + kind: base64Kind, + pattern: `[\w\/+-]{16,}={0,2}`, + decode: decodeBase64, + }, + } +) + +// encodingNames is used to map the encodingKinds to their name +var encodingNames = []string{ + "percent", + "unicode", + "hex", + "base64", +} + +// encodingKind can be or'd together to capture all of the unique encodings +// that were present in a segment +type encodingKind int + +var ( + // make sure these go up by powers of 2 + percentKind = encodingKind(1) + unicodeKind = encodingKind(2) + hexKind = encodingKind(4) + base64Kind = encodingKind(8) +) + +func (e encodingKind) String() string { + i := int(math.Log2(float64(e))) + if i >= len(encodingNames) { + return "" + } + return encodingNames[i] +} + +// kinds returns a list of encodingKinds combined in this one +func (e encodingKind) kinds() []encodingKind { + kinds := []encodingKind{} + + for i := 0; i < len(encodingNames); i++ { + if kind := int(e) & int(math.Pow(2, float64(i))); kind != 0 { + kinds = append(kinds, encodingKind(kind)) + } + } + + return kinds +} + +// encodingMatch represents a match of an encoding in the text +type encodingMatch struct { + encoding *encoding + startEnd +} + +// encoding represent a type of coding supported by the decoder. +type encoding struct { + // the kind of decoding (e.g. base64, etc) + kind encodingKind + // the regex pattern that matches the encoding format + pattern string + // take the match and return the decoded value + decode func(string) string + // determine which encoding should win out when two overlap + precedence int +} + +func init() { + count := len(encodings) + namedPatterns := make([]string, count) + for i, encoding := range encodings { + encoding.precedence = count - i + namedPatterns[i] = fmt.Sprintf( + "(?P<%s>%s)", + encoding.kind, + encoding.pattern, + ) + } + encodingsRe = regexp.MustCompile(strings.Join(namedPatterns, "|")) +} + +// findEncodingMatches finds as many encodings as it can for this pass +func findEncodingMatches(data string) []encodingMatch { + var all []encodingMatch + for _, matchIndex := range encodingsRe.FindAllStringSubmatchIndex(data, -1) { + // Add the encodingMatch with its proper encoding + for i, j := 2, 0; i < len(matchIndex); i, j = i+2, j+1 { + if matchIndex[i] > -1 { + all = append(all, encodingMatch{ + encoding: encodings[j], + startEnd: startEnd{ + start: matchIndex[i], + end: matchIndex[i+1], + }, + }) + } + } + } + + totalMatches := len(all) + if totalMatches == 1 { + return all + } + + // filter out lower precedence ones that overlap their neigbors + filtered := make([]encodingMatch, 0, len(all)) + for i, m := range all { + if i > 0 { + prev := all[i-1] + if m.overlaps(prev.startEnd) && prev.encoding.precedence > m.encoding.precedence { + continue // skip this one + } + } + if i+1 < totalMatches { + next := all[i+1] + if m.overlaps(next.startEnd) && next.encoding.precedence > m.encoding.precedence { + continue // skip this one + } + } + filtered = append(filtered, m) + } + + return filtered +} diff --git a/detect/codec/hex.go b/detect/codec/hex.go new file mode 100644 index 0000000..dcf6e1b --- /dev/null +++ b/detect/codec/hex.go @@ -0,0 +1,60 @@ +package codec + +// hexMap is a precalculated map of hex nibbles +const hexMap = "" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\xff\xff\xff\xff\xff\xff" + + "\xff\x0a\x0b\x0c\x0d\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\x0a\x0b\x0c\x0d\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + +// likelyHexChars is a set of characters that you would expect to find at +// least one of in hex encoded data. This risks missing some hex data that +// doesn't contain these characters, but gives you the performance gain of not +// trying to decode a lot of long symbols in code. +var likelyHexChars = make([]bool, 256) + +func init() { + for _, c := range `0123456789` { + likelyHexChars[c] = true + } +} + +// decodeHex decodes hex data +func decodeHex(encodedValue string) string { + size := len(encodedValue) + // hex should have two characters per byte + if size%2 != 0 { + return "" + } + if !hasByte(encodedValue, likelyHexChars) { + return "" + } + + decodedValue := make([]byte, size/2) + for i := 0; i < size; i += 2 { + n1 := hexMap[encodedValue[i]] + n2 := hexMap[encodedValue[i+1]] + if n1|n2 == '\xff' { + return "" + } + b := n1<<4 | n2 + if !printableASCII[b] { + return "" + } + decodedValue[i/2] = b + } + + return string(decodedValue) +} diff --git a/detect/codec/percent.go b/detect/codec/percent.go new file mode 100644 index 0000000..bc10249 --- /dev/null +++ b/detect/codec/percent.go @@ -0,0 +1,34 @@ +package codec + +// decodePercent decodes percent encoded strings +func decodePercent(encodedValue string) string { + encLen := len(encodedValue) + decodedValue := make([]byte, encLen) + decIndex := 0 + encIndex := 0 + + for encIndex < encLen { + if encodedValue[encIndex] == '%' && encIndex+2 < encLen { + n1 := hexMap[encodedValue[encIndex+1]] + n2 := hexMap[encodedValue[encIndex+2]] + // Make sure they're hex characters + if n1|n2 != '\xff' { + b := n1<<4 | n2 + if !printableASCII[b] { + return "" + } + + decodedValue[decIndex] = b + encIndex += 3 + decIndex += 1 + continue + } + } + + decodedValue[decIndex] = encodedValue[encIndex] + encIndex += 1 + decIndex += 1 + } + + return string(decodedValue[:decIndex]) +} diff --git a/detect/codec/segment.go b/detect/codec/segment.go new file mode 100644 index 0000000..8b24b3e --- /dev/null +++ b/detect/codec/segment.go @@ -0,0 +1,173 @@ +package codec + +import ( + "fmt" +) + +// EncodedSegment represents a portion of text that is encoded in some way. +type EncodedSegment struct { + // predecessors are all of the segments from the previous decoding pass + predecessors []*EncodedSegment + + // original start/end indices before decoding + original startEnd + + // encoded start/end indices relative to the previous decoding pass. + // If it's a top level segment, original and encoded will be the + // same. + encoded startEnd + + // decoded start/end indices in this pass after decoding + decoded startEnd + + // decodedValue contains the decoded string for this segment + decodedValue string + + // encodings is the encodings that make up this segment. encodingKind + // can be or'd together to hold multiple encodings + encodings encodingKind + + // depth is how many decoding passes it took to decode this segment + depth int +} + +// Tags returns additional meta data tags related to the types of segments +func Tags(segments []*EncodedSegment) []string { + // Return an empty list if we don't have any segments + if len(segments) == 0 { + return []string{} + } + + // Since decoding is done in passes, the depth of all the segments + // should be the same + depth := segments[0].depth + + // Collect the encodings from the segments + encodings := segments[0].encodings + for i := 1; i < len(segments); i++ { + encodings |= segments[i].encodings + } + + kinds := encodings.kinds() + tags := make([]string, len(kinds)+1) + + tags[len(tags)-1] = fmt.Sprintf("decode-depth:%d", depth) + for i, kind := range kinds { + tags[i] = fmt.Sprintf("decoded:%s", kind) + } + + return tags +} + +// CurrentLine returns from the start of the line containing the segments +// to the end of the line where the segment ends. +func CurrentLine(segments []*EncodedSegment, currentRaw string) string { + // Return the whole thing if no segments are provided + if len(segments) == 0 { + return currentRaw + } + + start := 0 + end := len(currentRaw) + + // Merge the ranges together into a single decoded value + decoded := segments[0].decoded + for i := 1; i < len(segments); i++ { + decoded = decoded.merge(segments[i].decoded) + } + + // Find the start of the range + for i := decoded.start; i > -1; i-- { + c := currentRaw[i] + if c == '\n' { + start = i + break + } + } + + // Find the end of the range + for i := decoded.end; i < end; i++ { + c := currentRaw[i] + if c == '\n' { + end = i + break + } + } + + return currentRaw[start:end] +} + +// AdjustMatchIndex maps a match index from the current decode pass back to +// its location in the original text +func AdjustMatchIndex(segments []*EncodedSegment, matchIndex []int) []int { + // Don't adjust if we're not provided any segments + if len(segments) == 0 { + return matchIndex + } + + // Map the match to the location in the original text + match := startEnd{matchIndex[0], matchIndex[1]} + + // Map the match to its orignal location + adjusted := toOriginal(segments, match) + + // Return the adjusted match index + return []int{ + adjusted.start, + adjusted.end, + } +} + +// SegmentsWithDecodedOverlap the segments where the start and end overlap its +// decoded range +func SegmentsWithDecodedOverlap(segments []*EncodedSegment, start, end int) []*EncodedSegment { + se := startEnd{start, end} + overlaps := []*EncodedSegment{} + + for _, segment := range segments { + if segment.decoded.overlaps(se) { + overlaps = append(overlaps, segment) + } + } + + return overlaps +} + +// toOriginal maps a start/end to its start/end in the original text +// the provided start/end should be relative to the segment's decoded value +func toOriginal(predecessors []*EncodedSegment, decoded startEnd) startEnd { + if len(predecessors) == 0 { + return decoded + } + + // Map the decoded value one level up where it was encoded + encoded := startEnd{} + + for _, p := range predecessors { + if !p.decoded.overlaps(decoded) { + continue // Not in scope + } + + // If fully contained, return the segments original start/end + if p.decoded.contains(decoded) { + return p.original + } + + // Map the value to be relative to the predecessors's decoded values + if encoded.end == 0 { + encoded = p.encoded.add(p.decoded.overflow(decoded)) + } else { + encoded = encoded.merge(p.encoded.add(p.decoded.overflow(decoded))) + } + } + + // Should only get here if the thing passed in wasn't in a decoded + // value. This shouldn't be the case + if encoded.end == 0 { + return decoded + } + + // Climb up another level + // (NOTE: each segment references all the predecessors) + return toOriginal(predecessors[0].predecessors, encoded) +} diff --git a/detect/codec/start_end.go b/detect/codec/start_end.go new file mode 100644 index 0000000..e220991 --- /dev/null +++ b/detect/codec/start_end.go @@ -0,0 +1,57 @@ +package codec + +import ( + "fmt" +) + +// startEnd represents the start and end of some data. It mainly exists as a +// helper when referencing the values +type startEnd struct { + start int + end int +} + +// sub subtracts the values of two startEnds +func (s startEnd) sub(o startEnd) startEnd { + return startEnd{ + s.start - o.start, + s.end - o.end, + } +} + +// add adds the values of two startEnds +func (s startEnd) add(o startEnd) startEnd { + return startEnd{ + s.start + o.start, + s.end + o.end, + } +} + +// overlaps returns true if two startEnds overlap +func (s startEnd) overlaps(o startEnd) bool { + return o.start <= s.end && o.end >= s.start +} + +// contains returns true if the other is fully contained within this one +func (s startEnd) contains(o startEnd) bool { + return s.start <= o.start && o.end <= s.end +} + +// overflow returns a startEnd that tells how much the other goes outside the +// bounds of this one +func (s startEnd) overflow(o startEnd) startEnd { + return s.merge(o).sub(s) +} + +// merge takes two start/ends and returns a single one that encompasses both +func (s startEnd) merge(o startEnd) startEnd { + return startEnd{ + min(s.start, o.start), + max(s.end, o.end), + } +} + +// String returns a string representation for clearer debugging +func (s startEnd) String() string { + return fmt.Sprintf("[%d,%d]", s.start, s.end) +} diff --git a/detect/codec/unicode.go b/detect/codec/unicode.go new file mode 100644 index 0000000..5738e32 --- /dev/null +++ b/detect/codec/unicode.go @@ -0,0 +1,261 @@ +package codec + +import ( + "bytes" + "strconv" + "strings" + "unicode/utf8" + + "github.com/zricethezav/gitleaks/v8/regexp" +) + +var ( + // Standard Unicode notation (e.g., U+1234) + unicodeCodePointPat = regexp.MustCompile(`U\+([a-fA-F0-9]{4}).?`) + + // Multiple code points pattern - used for continuous sequences like "U+0074 U+006F U+006B..." + unicodeMultiCodePointPat = regexp.MustCompile(`(?:U\+[a-fA-F0-9]{4}(?:\s|$))+`) + + // Common escape sequence used in programming languages (e.g., \u1234) + unicodeEscapePat = regexp.MustCompile(`(?i)\\{1,2}u([a-fA-F0-9]{4})`) + + // Multiple escape sequences pattern - used for continuous sequences like "\u0074\u006F\u006B..." + unicodeMultiEscapePat = regexp.MustCompile(`(?i)(?:\\{1,2}u[a-fA-F0-9]{4})+`) +) + +// Unicode characters are encoded as 1 to 4 bytes per rune. +const maxBytesPerRune = 4 + +// decodeUnicode decodes Unicode escape sequences in the given string +func decodeUnicode(encodedValue string) string { + // First, check if we have a continuous sequence of Unicode code points + if matches := unicodeMultiCodePointPat.FindAllString(encodedValue, -1); len(matches) > 0 { + // For each detected sequence of code points + for _, match := range matches { + // Decode the entire sequence at once + decodedSequence := decodeMultiCodePoint(match) + + // If we successfully decoded something, replace it in the original string + if decodedSequence != "" && decodedSequence != match { + encodedValue = strings.Replace(encodedValue, match, decodedSequence, 1) + } + } + return encodedValue + } + + // Next, check if we have a continuous sequence of escape sequences + if matches := unicodeMultiEscapePat.FindAllString(encodedValue, -1); len(matches) > 0 { + // For each detected sequence of escape sequences + for _, match := range matches { + // Decode the entire sequence at once + decodedSequence := decodeMultiEscape(match) + + // If we successfully decoded something, replace it in the original string + if decodedSequence != "" && decodedSequence != match { + encodedValue = strings.Replace(encodedValue, match, decodedSequence, 1) + } + } + return encodedValue + } + + // If no multi-patterns were matched, fall back to the original implementation + // for individual code points and escape sequences + + // Create a copy of the input to work with + data := []byte(encodedValue) + + // Store the result + var result []byte + + // Check and decode Unicode code points (U+1234 format) + if unicodeCodePointPat.Match(data) { + result = decodeIndividualCodePoints(data) + } + + // If no code points were found or we have a mix of formats, + // also check for Unicode escape sequences (\u1234 format) + if len(result) == 0 || unicodeEscapePat.Match(data) { + // If we already have some result from code point decoding, + // continue decoding escape sequences on that result + if len(result) > 0 { + result = decodeIndividualEscapes(result) + } else { + result = decodeIndividualEscapes(data) + } + } + + // If nothing was decoded, return original string + if len(result) == 0 || bytes.Equal(result, data) { + return encodedValue + } + + return string(result) +} + +// decodeMultiCodePoint decodes a continuous sequence of Unicode code points (U+XXXX format) +func decodeMultiCodePoint(sequence string) string { + // If the sequence is empty, return empty string + if sequence == "" { + return "" + } + + // Split the sequence by whitespace to get individual code points + codePoints := strings.Fields(sequence) + if len(codePoints) == 0 { + return sequence + } + + // Decode each code point and build the result + var decodedRunes []rune + for _, cp := range codePoints { + // Check if it follows the U+XXXX pattern + if !strings.HasPrefix(cp, "U+") || len(cp) < 6 { + continue + } + + // Extract the hexadecimal value + hexValue := cp[2:] + + // Parse the hexadecimal value to an integer + unicodeInt, err := strconv.ParseInt(hexValue, 16, 32) + if err != nil { + continue + } + + // Convert to rune and add to result + decodedRunes = append(decodedRunes, rune(unicodeInt)) + } + + // If we didn't decode anything, return the original sequence + if len(decodedRunes) == 0 { + return sequence + } + + // Return the decoded string + return string(decodedRunes) +} + +// decodeMultiEscape decodes a continuous sequence of Unicode escape sequences (\uXXXX format) +func decodeMultiEscape(sequence string) string { + // If the sequence is empty, return empty string + if sequence == "" { + return "" + } + + // Find all escape sequences + escapes := unicodeEscapePat.FindAllStringSubmatch(sequence, -1) + if len(escapes) == 0 { + return sequence + } + + // Decode each escape sequence and build the result + var decodedRunes []rune + for _, esc := range escapes { + // Extract the hexadecimal value + hexValue := esc[1] + + // Parse the hexadecimal value to an integer + unicodeInt, err := strconv.ParseInt(hexValue, 16, 32) + if err != nil { + continue + } + + // Convert to rune and add to result + decodedRunes = append(decodedRunes, rune(unicodeInt)) + } + + // If we didn't decode anything, return the original sequence + if len(decodedRunes) == 0 { + return sequence + } + + // Return the decoded string + return string(decodedRunes) +} + +// decodeIndividualCodePoints decodes individual Unicode code points (U+1234 format) +// This is a fallback for when we don't have a continuous sequence of code points +func decodeIndividualCodePoints(input []byte) []byte { + // Find all Unicode code point sequences in the input byte slice + indices := unicodeCodePointPat.FindAllSubmatchIndex(input, -1) + + // If none found, return original input + if len(indices) == 0 { + return input + } + + // Iterate over found indices in reverse order to avoid modifying the slice length + utf8Bytes := make([]byte, maxBytesPerRune) + for i := len(indices) - 1; i >= 0; i-- { + matches := indices[i] + + startIndex := matches[0] + endIndex := matches[1] + hexStartIndex := matches[2] + hexEndIndex := matches[3] + + // If the input is like `U+1234 U+5678` we should replace `U+1234 `. + // Otherwise, we should only replace `U+1234`. + if endIndex != hexEndIndex && endIndex < len(input) && input[endIndex-1] == ' ' { + endIndex = endIndex - 1 + } + + // Extract the hexadecimal value from the escape sequence + hexValue := string(input[hexStartIndex:hexEndIndex]) + + // Parse the hexadecimal value to an integer + unicodeInt, err := strconv.ParseInt(hexValue, 16, 32) + if err != nil { + // If there's an error, continue to the next escape sequence + continue + } + + // Convert the Unicode code point to a UTF-8 representation + utf8Len := utf8.EncodeRune(utf8Bytes, rune(unicodeInt)) + + // Replace the escape sequence with the UTF-8 representation + input = append(input[:startIndex], append(utf8Bytes[:utf8Len], input[endIndex:]...)...) + } + + return input +} + +// decodeIndividualEscapes decodes individual Unicode escape sequences (\u1234 format) +// This is a fallback for when we don't have a continuous sequence of escape sequences +func decodeIndividualEscapes(input []byte) []byte { + // Find all Unicode escape sequences in the input byte slice + indices := unicodeEscapePat.FindAllSubmatchIndex(input, -1) + + // If none found, return original input + if len(indices) == 0 { + return input + } + + // Iterate over found indices in reverse order to avoid modifying the slice length + utf8Bytes := make([]byte, maxBytesPerRune) + for i := len(indices) - 1; i >= 0; i-- { + matches := indices[i] + + startIndex := matches[0] + hexStartIndex := matches[2] + endIndex := matches[3] + + // Extract the hexadecimal value from the escape sequence + hexValue := string(input[hexStartIndex:endIndex]) + + // Parse the hexadecimal value to an integer + unicodeInt, err := strconv.ParseInt(hexValue, 16, 32) + if err != nil { + // If there's an error, continue to the next escape sequence + continue + } + + // Convert the Unicode code point to a UTF-8 representation + utf8Len := utf8.EncodeRune(utf8Bytes, rune(unicodeInt)) + + // Replace the escape sequence with the UTF-8 representation + input = append(input[:startIndex], append(utf8Bytes[:utf8Len], input[endIndex:]...)...) + } + + return input +} diff --git a/detect/detect.go b/detect/detect.go new file mode 100644 index 0000000..ea3bf41 --- /dev/null +++ b/detect/detect.go @@ -0,0 +1,900 @@ +package detect + +import ( + "bufio" + "context" + "fmt" + "os" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/detect/codec" + "github.com/zricethezav/gitleaks/v8/logging" + "github.com/zricethezav/gitleaks/v8/regexp" + "github.com/zricethezav/gitleaks/v8/report" + "github.com/zricethezav/gitleaks/v8/sources" + + ahocorasick "github.com/BobuSumisu/aho-corasick" + "github.com/fatih/semgroup" + "github.com/rs/zerolog" + "github.com/spf13/viper" + "golang.org/x/exp/maps" +) + +const ( + gitleaksAllowSignature = "gitleaks:allow" + // SlowWarningThreshold is the amount of time to wait before logging that a file is slow. + // This is useful for identifying problematic files and tuning the allowlist. + SlowWarningThreshold = 5 * time.Second +) + +var ( + newLineRegexp = regexp.MustCompile("\n") +) + +// Detector is the main detector struct +type Detector struct { + // Config is the configuration for the detector + Config config.Config + + // Redact is a flag to redact findings. This is exported + // so users using gitleaks as a library can set this flag + // without calling `detector.Start(cmd *cobra.Command)` + Redact uint + + // verbose is a flag to print findings + Verbose bool + + // MaxDecodeDepths limits how many recursive decoding passes are allowed + MaxDecodeDepth int + + // MaxArchiveDepth limits how deep the sources will explore nested archives + MaxArchiveDepth int + + // files larger than this will be skipped + MaxTargetMegaBytes int + + // followSymlinks is a flag to enable scanning symlink files + FollowSymlinks bool + + // NoColor is a flag to disable color output + NoColor bool + + // IgnoreGitleaksAllow is a flag to ignore gitleaks:allow comments. + IgnoreGitleaksAllow bool + + // commitMutex is to prevent concurrent access to the + // commit map when adding commits + commitMutex *sync.Mutex + + // commitMap is used to keep track of commits that have been scanned. + // This is only used for logging purposes and git scans. + commitMap map[string]bool + + // findingMutex is to prevent concurrent access to the + // findings slice when adding findings. + findingMutex *sync.Mutex + + // findings is a slice of report.Findings. This is the result + // of the detector's scan which can then be used to generate a + // report. + findings []report.Finding + + // prefilter is a ahocorasick struct used for doing efficient string + // matching given a set of words (keywords from the rules in the config) + prefilter ahocorasick.Trie + + // a list of known findings that should be ignored + baseline []report.Finding + + // path to baseline + baselinePath string + + // gitleaksIgnore + gitleaksIgnore map[string]struct{} + + // Sema (https://github.com/fatih/semgroup) controls the concurrency + Sema *semgroup.Group + + // report-related settings. + ReportPath string + Reporter report.Reporter + + TotalBytes atomic.Uint64 +} + +// Fragment is an alias for sources.Fragment for backwards compatibility +// +// Deprecated: This will be replaced with sources.Fragment in v9 +type Fragment sources.Fragment + +// NewDetector creates a new detector with the given config +func NewDetector(cfg config.Config) *Detector { + return NewDetectorContext(context.Background(), cfg) +} + +// NewDetectorContext is the same as NewDetector but supports passing in a +// context to use for timeouts +func NewDetectorContext(ctx context.Context, cfg config.Config) *Detector { + return &Detector{ + commitMap: make(map[string]bool), + gitleaksIgnore: make(map[string]struct{}), + findingMutex: &sync.Mutex{}, + commitMutex: &sync.Mutex{}, + findings: make([]report.Finding, 0), + Config: cfg, + prefilter: *ahocorasick.NewTrieBuilder().AddStrings(maps.Keys(cfg.Keywords)).Build(), + Sema: semgroup.NewGroup(ctx, 40), + } +} + +// NewDetectorDefaultConfig creates a new detector with the default config +func NewDetectorDefaultConfig() (*Detector, error) { + viper.SetConfigType("toml") + err := viper.ReadConfig(strings.NewReader(config.DefaultConfig)) + if err != nil { + return nil, err + } + var vc config.ViperConfig + err = viper.Unmarshal(&vc) + if err != nil { + return nil, err + } + cfg, err := vc.Translate() + if err != nil { + return nil, err + } + return NewDetector(cfg), nil +} + +func (d *Detector) AddGitleaksIgnore(gitleaksIgnorePath string) error { + logging.Debug().Str("path", gitleaksIgnorePath).Msgf("found .gitleaksignore file") + file, err := os.Open(gitleaksIgnorePath) + if err != nil { + return err + } + defer func() { + // https://github.com/securego/gosec/issues/512 + if err := file.Close(); err != nil { + logging.Warn().Err(err).Msgf("Error closing .gitleaksignore file") + } + }() + + scanner := bufio.NewScanner(file) + replacer := strings.NewReplacer("\\", "/") + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + // Skip lines that start with a comment + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + // Normalize the path. + // TODO: Make this a breaking change in v9. + s := strings.Split(line, ":") + switch len(s) { + case 3: + // Global fingerprint. + // `file:rule-id:start-line` + s[0] = replacer.Replace(s[0]) + case 4: + // Commit fingerprint. + // `commit:file:rule-id:start-line` + s[1] = replacer.Replace(s[1]) + default: + logging.Warn().Str("fingerprint", line).Msg("Invalid .gitleaksignore entry") + } + d.gitleaksIgnore[strings.Join(s, ":")] = struct{}{} + } + return nil +} + +// DetectBytes scans the given bytes and returns a list of findings +func (d *Detector) DetectBytes(content []byte) []report.Finding { + return d.DetectString(string(content)) +} + +// DetectString scans the given string and returns a list of findings +func (d *Detector) DetectString(content string) []report.Finding { + return d.Detect(Fragment{ + Raw: content, + }) +} + +// DetectSource scans the given source and returns a list of findings +func (d *Detector) DetectSource(ctx context.Context, source sources.Source) ([]report.Finding, error) { + err := source.Fragments(ctx, func(fragment sources.Fragment, err error) error { + logContext := logging.With() + + if len(fragment.FilePath) > 0 { + logContext = logContext.Str("path", fragment.FilePath) + } + + if len(fragment.CommitSHA) > 6 { + logContext = logContext.Str("commit", fragment.CommitSHA[:7]) + d.addCommit(fragment.CommitSHA) + } else if len(fragment.CommitSHA) > 0 { + logContext = logContext.Str("commit", fragment.CommitSHA) + d.addCommit(fragment.CommitSHA) + logger := logContext.Logger() + logger.Warn().Msg("commit SHAs should be >= 7 characters long") + } + + logger := logContext.Logger() + + if err != nil { + // Log the error and move on to the next fragment + logger.Error().Err(err).Send() + return nil + } + + // both the fragment's content and path should be empty for it to be + // considered empty at this point because of path based matches + if len(fragment.Raw) == 0 && len(fragment.FilePath) == 0 { + logger.Trace().Msg("skipping empty fragment") + return nil + } + + var timer *time.Timer + // Only start the timer in debug mode + if logger.GetLevel() <= zerolog.DebugLevel { + timer = time.AfterFunc(SlowWarningThreshold, func() { + logger.Debug().Msgf("Taking longer than %s to inspect fragment", SlowWarningThreshold.String()) + }) + } + + for _, finding := range d.DetectContext(ctx, Fragment(fragment)) { + d.AddFinding(finding) + } + + // Stop the timer if it was created + if timer != nil { + timer.Stop() + } + + return nil + }) + + if _, isGit := source.(*sources.Git); isGit { + logging.Info().Msgf("%d commits scanned.", len(d.commitMap)) + logging.Debug().Msg("Note: this number might be smaller than expected due to commits with no additions") + } + + return d.Findings(), err +} + +// Detect scans the given fragment and returns a list of findings +func (d *Detector) Detect(fragment Fragment) []report.Finding { + return d.DetectContext(context.Background(), fragment) +} + +// DetectContext is the same as Detect but supports passing in a +// context to use for timeouts +func (d *Detector) DetectContext(ctx context.Context, fragment Fragment) []report.Finding { + if fragment.Bytes == nil { + d.TotalBytes.Add(uint64(len(fragment.Raw))) + } + d.TotalBytes.Add(uint64(len(fragment.Bytes))) + + var ( + findings []report.Finding + logger = func() zerolog.Logger { + l := logging.With().Str("path", fragment.FilePath) + if fragment.CommitSHA != "" { + l = l.Str("commit", fragment.CommitSHA) + } + return l.Logger() + }() + ) + + // check if filepath is allowed + if fragment.FilePath != "" { + // is the path our config or baseline file? + if fragment.FilePath == d.Config.Path || (d.baselinePath != "" && fragment.FilePath == d.baselinePath) { + logging.Trace().Msg("skipping file: matches config or baseline path") + return findings + } + } + // check if commit or filepath is allowed. + if isAllowed, event := checkCommitOrPathAllowed(logger, fragment, d.Config.Allowlists); isAllowed { + event.Msg("skipping file: global allowlist") + return findings + } + + // setup variables to handle different decoding passes + currentRaw := fragment.Raw + encodedSegments := []*codec.EncodedSegment{} + currentDecodeDepth := 0 + decoder := codec.NewDecoder() + +ScanLoop: + for { + select { + case <-ctx.Done(): + break ScanLoop + default: + // build keyword map for prefiltering rules + keywords := make(map[string]bool) + normalizedRaw := strings.ToLower(currentRaw) + matches := d.prefilter.MatchString(normalizedRaw) + for _, m := range matches { + keywords[normalizedRaw[m.Pos():int(m.Pos())+len(m.Match())]] = true + } + + for _, rule := range d.Config.Rules { + select { + case <-ctx.Done(): + break ScanLoop + default: + if len(rule.Keywords) == 0 { + // if no keywords are associated with the rule always scan the + // fragment using the rule + findings = append(findings, d.detectRule(fragment, currentRaw, rule, encodedSegments)...) + continue + } + + // check if keywords are in the fragment + for _, k := range rule.Keywords { + if _, ok := keywords[strings.ToLower(k)]; ok { + findings = append(findings, d.detectRule(fragment, currentRaw, rule, encodedSegments)...) + break + } + } + } + } + + // increment the depth by 1 as we start our decoding pass + currentDecodeDepth++ + + // stop the loop if we've hit our max decoding depth + if currentDecodeDepth > d.MaxDecodeDepth { + break ScanLoop + } + + // decode the currentRaw for the next pass + currentRaw, encodedSegments = decoder.Decode(currentRaw, encodedSegments) + + // stop the loop when there's nothing else to decode + if len(encodedSegments) == 0 { + break ScanLoop + } + } + } + + return filter(findings, d.Redact) +} + +// detectRule scans the given fragment for the given rule and returns a list of findings +func (d *Detector) detectRule(fragment Fragment, currentRaw string, r config.Rule, encodedSegments []*codec.EncodedSegment) []report.Finding { + var ( + findings []report.Finding + logger = func() zerolog.Logger { + l := logging.With().Str("rule-id", r.RuleID).Str("path", fragment.FilePath) + if fragment.CommitSHA != "" { + l = l.Str("commit", fragment.CommitSHA) + } + return l.Logger() + }() + ) + + if r.SkipReport && !fragment.InheritedFromFinding { + return findings + } + + // check if commit or file is allowed for this rule. + if isAllowed, event := checkCommitOrPathAllowed(logger, fragment, r.Allowlists); isAllowed { + event.Msg("skipping file: rule allowlist") + return findings + } + + if r.Path != nil { + if r.Regex == nil && len(encodedSegments) == 0 { + // Path _only_ rule + if r.Path.MatchString(fragment.FilePath) || (fragment.WindowsFilePath != "" && r.Path.MatchString(fragment.WindowsFilePath)) { + finding := report.Finding{ + Commit: fragment.CommitSHA, + RuleID: r.RuleID, + Description: r.Description, + File: fragment.FilePath, + SymlinkFile: fragment.SymlinkFile, + Match: "file detected: " + fragment.FilePath, + Tags: r.Tags, + } + if fragment.CommitInfo != nil { + finding.Author = fragment.CommitInfo.AuthorName + finding.Date = fragment.CommitInfo.Date + finding.Email = fragment.CommitInfo.AuthorEmail + finding.Link = createScmLink(fragment.CommitInfo.Remote, finding) + finding.Message = fragment.CommitInfo.Message + } + return append(findings, finding) + } + } else { + // if path is set _and_ a regex is set, then we need to check both + // so if the path does not match, then we should return early and not + // consider the regex + if !(r.Path.MatchString(fragment.FilePath) || (fragment.WindowsFilePath != "" && r.Path.MatchString(fragment.WindowsFilePath))) { + return findings + } + } + } + + // if path only rule, skip content checks + if r.Regex == nil { + return findings + } + + // if flag configure and raw data size bigger then the flag + if d.MaxTargetMegaBytes > 0 { + rawLength := len(currentRaw) / 1_000_000 + if rawLength > d.MaxTargetMegaBytes { + logger.Debug(). + Int("size", rawLength). + Int("max-size", d.MaxTargetMegaBytes). + Msg("skipping fragment: size") + return findings + } + } + + matches := r.Regex.FindAllStringIndex(currentRaw, -1) + if len(matches) == 0 { + return findings + } + + // TODO profile this, probably should replace with something more efficient + newlineIndices := newLineRegexp.FindAllStringIndex(fragment.Raw, -1) + + // use currentRaw instead of fragment.Raw since this represents the current + // decoding pass on the text + for _, matchIndex := range r.Regex.FindAllStringIndex(currentRaw, -1) { + // Extract secret from match + secret := strings.Trim(currentRaw[matchIndex[0]:matchIndex[1]], "\n") + + // For any meta data from decoding + var metaTags []string + currentLine := "" + + // Check if the decoded portions of the segment overlap with the match + // to see if its potentially a new match + if len(encodedSegments) > 0 { + segments := codec.SegmentsWithDecodedOverlap(encodedSegments, matchIndex[0], matchIndex[1]) + if len(segments) == 0 { + // This item has already been added to a finding + continue + } + + matchIndex = codec.AdjustMatchIndex(segments, matchIndex) + metaTags = append(metaTags, codec.Tags(segments)...) + currentLine = codec.CurrentLine(segments, currentRaw) + } else { + // Fixes: https://github.com/gitleaks/gitleaks/issues/1352 + // removes the incorrectly following line that was detected by regex expression '\n' + matchIndex[1] = matchIndex[0] + len(secret) + } + + // determine location of match. Note that the location + // in the finding will be the line/column numbers of the _match_ + // not the _secret_, which will be different if the secretGroup + // value is set for this rule + loc := location(newlineIndices, fragment.Raw, matchIndex) + + if matchIndex[1] > loc.endLineIndex { + loc.endLineIndex = matchIndex[1] + } + + finding := report.Finding{ + Commit: fragment.CommitSHA, + RuleID: r.RuleID, + Description: r.Description, + StartLine: fragment.StartLine + loc.startLine, + EndLine: fragment.StartLine + loc.endLine, + StartColumn: loc.startColumn, + EndColumn: loc.endColumn, + Line: fragment.Raw[loc.startLineIndex:loc.endLineIndex], + Match: secret, + Secret: secret, + File: fragment.FilePath, + SymlinkFile: fragment.SymlinkFile, + Tags: append(r.Tags, metaTags...), + } + if fragment.CommitInfo != nil { + finding.Author = fragment.CommitInfo.AuthorName + finding.Date = fragment.CommitInfo.Date + finding.Email = fragment.CommitInfo.AuthorEmail + finding.Link = createScmLink(fragment.CommitInfo.Remote, finding) + finding.Message = fragment.CommitInfo.Message + } + if !d.IgnoreGitleaksAllow && strings.Contains(finding.Line, gitleaksAllowSignature) { + logger.Trace(). + Str("finding", finding.Secret). + Msg("skipping finding: 'gitleaks:allow' signature") + continue + } + + if currentLine == "" { + currentLine = finding.Line + } + + // Set the value of |secret|, if the pattern contains at least one capture group. + // (The first element is the full match, hence we check >= 2.) + groups := r.Regex.FindStringSubmatch(finding.Secret) + if len(groups) >= 2 { + if r.SecretGroup > 0 { + if len(groups) <= r.SecretGroup { + // Config validation should prevent this + continue + } + finding.Secret = groups[r.SecretGroup] + } else { + // If |secretGroup| is not set, we will use the first suitable capture group. + for _, s := range groups[1:] { + if len(s) > 0 { + finding.Secret = s + break + } + } + } + } + + // check entropy + entropy := shannonEntropy(finding.Secret) + finding.Entropy = float32(entropy) + if r.Entropy != 0.0 { + // entropy is too low, skip this finding + if entropy <= r.Entropy { + logger.Trace(). + Str("finding", finding.Secret). + Float32("entropy", finding.Entropy). + Msg("skipping finding: low entropy") + continue + } + } + + // check if the result matches any of the global allowlists. + if isAllowed, event := checkFindingAllowed(logger, finding, fragment, currentLine, d.Config.Allowlists); isAllowed { + event.Msg("skipping finding: global allowlist") + continue + } + + // check if the result matches any of the rule allowlists. + if isAllowed, event := checkFindingAllowed(logger, finding, fragment, currentLine, r.Allowlists); isAllowed { + event.Msg("skipping finding: rule allowlist") + continue + } + findings = append(findings, finding) + } + + // Handle required rules (multi-part rules) + if fragment.InheritedFromFinding || len(r.RequiredRules) == 0 { + return findings + } + + // Process required rules and create findings with auxiliary findings + return d.processRequiredRules(fragment, currentRaw, r, encodedSegments, findings, logger) +} + +// processRequiredRules handles the logic for multi-part rules with auxiliary findings +func (d *Detector) processRequiredRules(fragment Fragment, currentRaw string, r config.Rule, encodedSegments []*codec.EncodedSegment, primaryFindings []report.Finding, logger zerolog.Logger) []report.Finding { + if len(primaryFindings) == 0 { + logger.Debug().Msg("no primary findings to process for required rules") + return primaryFindings + } + + // Pre-collect all required rule findings once + allRequiredFindings := make(map[string][]report.Finding) + + for _, requiredRule := range r.RequiredRules { + rule, ok := d.Config.Rules[requiredRule.RuleID] + if !ok { + logger.Error().Str("rule-id", requiredRule.RuleID).Msg("required rule not found in config") + continue + } + + // Mark fragment as inherited to prevent infinite recursion + inheritedFragment := fragment + inheritedFragment.InheritedFromFinding = true + + // Call detectRule once for each required rule + requiredFindings := d.detectRule(inheritedFragment, currentRaw, rule, encodedSegments) + allRequiredFindings[requiredRule.RuleID] = requiredFindings + + logger.Debug(). + Str("rule-id", requiredRule.RuleID). + Int("findings", len(requiredFindings)). + Msg("collected required rule findings") + } + + var finalFindings []report.Finding + + // Now process each primary finding against the pre-collected required findings + for _, primaryFinding := range primaryFindings { + var requiredFindings []*report.RequiredFinding + + for _, requiredRule := range r.RequiredRules { + foundRequiredFindings, exists := allRequiredFindings[requiredRule.RuleID] + if !exists { + continue // Rule wasn't found earlier, skip + } + + // Filter findings that are within proximity of the primary finding + for _, requiredFinding := range foundRequiredFindings { + if d.withinProximity(primaryFinding, requiredFinding, requiredRule) { + req := &report.RequiredFinding{ + RuleID: requiredFinding.RuleID, + StartLine: requiredFinding.StartLine, + EndLine: requiredFinding.EndLine, + StartColumn: requiredFinding.StartColumn, + EndColumn: requiredFinding.EndColumn, + Line: requiredFinding.Line, + Match: requiredFinding.Match, + Secret: requiredFinding.Secret, + } + requiredFindings = append(requiredFindings, req) + } + } + } + + // Check if we have at least one auxiliary finding for each required rule + if len(requiredFindings) > 0 && d.hasAllRequiredRules(requiredFindings, r.RequiredRules) { + // Create a finding with auxiliary findings + newFinding := primaryFinding // Copy the primary finding + newFinding.AddRequiredFindings(requiredFindings) + finalFindings = append(finalFindings, newFinding) + + logger.Debug(). + Str("primary-rule", r.RuleID). + Int("primary-line", primaryFinding.StartLine). + Int("auxiliary-count", len(requiredFindings)). + Msg("multi-part rule satisfied") + } + } + + return finalFindings +} + +// hasAllRequiredRules checks if we have at least one auxiliary finding for each required rule +func (d *Detector) hasAllRequiredRules(auxiliaryFindings []*report.RequiredFinding, requiredRules []*config.Required) bool { + foundRules := make(map[string]bool) + // AuxiliaryFinding + for _, aux := range auxiliaryFindings { + foundRules[aux.RuleID] = true + } + + for _, required := range requiredRules { + if !foundRules[required.RuleID] { + return false + } + } + + return true +} + +func (d *Detector) withinProximity(primary, required report.Finding, requiredRule *config.Required) bool { + // fmt.Println(requiredRule.WithinLines) + // If neither within_lines nor within_columns is set, findings just need to be in the same fragment + if requiredRule.WithinLines == nil && requiredRule.WithinColumns == nil { + return true + } + + // Check line proximity (vertical distance) + if requiredRule.WithinLines != nil { + lineDiff := abs(primary.StartLine - required.StartLine) + if lineDiff > *requiredRule.WithinLines { + return false + } + } + + // Check column proximity (horizontal distance) + if requiredRule.WithinColumns != nil { + // Use the start column of each finding for proximity calculation + colDiff := abs(primary.StartColumn - required.StartColumn) + if colDiff > *requiredRule.WithinColumns { + return false + } + } + + return true +} + +// abs returns the absolute value of an integer +func abs(x int) int { + if x < 0 { + return -x + } + return x +} + +// AddFinding synchronously adds a finding to the findings slice +func (d *Detector) AddFinding(finding report.Finding) { + globalFingerprint := fmt.Sprintf("%s:%s:%d", finding.File, finding.RuleID, finding.StartLine) + if finding.Commit != "" { + finding.Fingerprint = fmt.Sprintf("%s:%s:%s:%d", finding.Commit, finding.File, finding.RuleID, finding.StartLine) + } else { + finding.Fingerprint = globalFingerprint + } + + // check if we should ignore this finding + logger := logging.With().Str("finding", finding.Secret).Logger() + if _, ok := d.gitleaksIgnore[globalFingerprint]; ok { + logger.Debug(). + Str("fingerprint", globalFingerprint). + Msg("skipping finding: global fingerprint") + return + } else if finding.Commit != "" { + // Awkward nested if because I'm not sure how to chain these two conditions. + if _, ok := d.gitleaksIgnore[finding.Fingerprint]; ok { + logger.Debug(). + Str("fingerprint", finding.Fingerprint). + Msgf("skipping finding: fingerprint") + return + } + } + + if d.baseline != nil && !IsNew(finding, d.Redact, d.baseline) { + logger.Debug(). + Str("fingerprint", finding.Fingerprint). + Msgf("skipping finding: baseline") + return + } + + d.findingMutex.Lock() + d.findings = append(d.findings, finding) + if d.Verbose { + printFinding(finding, d.NoColor) + } + d.findingMutex.Unlock() +} + +// Findings returns the findings added to the detector +func (d *Detector) Findings() []report.Finding { + return d.findings +} + +// AddCommit synchronously adds a commit to the commit slice +func (d *Detector) addCommit(commit string) { + d.commitMutex.Lock() + d.commitMap[commit] = true + d.commitMutex.Unlock() +} + +// checkCommitOrPathAllowed evaluates |fragment| against all provided |allowlists|. +// +// If the match condition is "OR", only commit and path are checked. +// Otherwise, if regexes or stopwords are defined this will fail. +func checkCommitOrPathAllowed( + logger zerolog.Logger, + fragment Fragment, + allowlists []*config.Allowlist, +) (bool, *zerolog.Event) { + if fragment.FilePath == "" && fragment.CommitSHA == "" { + return false, nil + } + + for _, a := range allowlists { + var ( + isAllowed bool + allowlistChecks []bool + commitAllowed, _ = a.CommitAllowed(fragment.CommitSHA) + pathAllowed = a.PathAllowed(fragment.FilePath) || (fragment.WindowsFilePath != "" && a.PathAllowed(fragment.WindowsFilePath)) + ) + // If the condition is "AND" we need to check all conditions. + if a.MatchCondition == config.AllowlistMatchAnd { + if len(a.Commits) > 0 { + allowlistChecks = append(allowlistChecks, commitAllowed) + } + if len(a.Paths) > 0 { + allowlistChecks = append(allowlistChecks, pathAllowed) + } + // These will be checked later. + if len(a.Regexes) > 0 { + continue + } + if len(a.StopWords) > 0 { + continue + } + + isAllowed = allTrue(allowlistChecks) + } else { + isAllowed = commitAllowed || pathAllowed + } + if isAllowed { + event := logger.Trace().Str("condition", a.MatchCondition.String()) + if commitAllowed { + event.Bool("allowed-commit", commitAllowed) + } + if pathAllowed { + event.Bool("allowed-path", pathAllowed) + } + return true, event + } + } + return false, nil +} + +// checkFindingAllowed evaluates |finding| against all provided |allowlists|. +// +// If the match condition is "OR", only regex and stopwords are run. (Commit and path should be handled separately). +// Otherwise, all conditions are checked. +// +// TODO: The method signature is awkward. I can't think of a better way to log helpful info. +func checkFindingAllowed( + logger zerolog.Logger, + finding report.Finding, + fragment Fragment, + currentLine string, + allowlists []*config.Allowlist, +) (bool, *zerolog.Event) { + for _, a := range allowlists { + allowlistTarget := finding.Secret + switch a.RegexTarget { + case "match": + allowlistTarget = finding.Match + case "line": + allowlistTarget = currentLine + } + + var ( + checks []bool + isAllowed bool + commitAllowed bool + commit string + pathAllowed bool + regexAllowed = a.RegexAllowed(allowlistTarget) + containsStopword, word = a.ContainsStopWord(finding.Secret) + ) + // If the condition is "AND" we need to check all conditions. + if a.MatchCondition == config.AllowlistMatchAnd { + // Determine applicable checks. + if len(a.Commits) > 0 { + commitAllowed, commit = a.CommitAllowed(fragment.CommitSHA) + checks = append(checks, commitAllowed) + } + if len(a.Paths) > 0 { + pathAllowed = a.PathAllowed(fragment.FilePath) || (fragment.WindowsFilePath != "" && a.PathAllowed(fragment.WindowsFilePath)) + checks = append(checks, pathAllowed) + } + if len(a.Regexes) > 0 { + checks = append(checks, regexAllowed) + } + if len(a.StopWords) > 0 { + checks = append(checks, containsStopword) + } + + isAllowed = allTrue(checks) + } else { + isAllowed = regexAllowed || containsStopword + } + + if isAllowed { + event := logger.Trace(). + Str("finding", finding.Secret). + Str("condition", a.MatchCondition.String()) + if commitAllowed { + event.Str("allowed-commit", commit) + } + if pathAllowed { + event.Bool("allowed-path", pathAllowed) + } + if regexAllowed { + event.Bool("allowed-regex", regexAllowed) + } + if containsStopword { + event.Str("allowed-stopword", word) + } + return true, event + } + } + return false, nil +} + +func allTrue(bools []bool) bool { + for _, check := range bools { + if !check { + return false + } + } + return true +} diff --git a/detect/detect_test.go b/detect/detect_test.go new file mode 100644 index 0000000..ce5910e --- /dev/null +++ b/detect/detect_test.go @@ -0,0 +1,2773 @@ +package detect + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/rs/zerolog" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + + "github.com/zricethezav/gitleaks/v8/cmd/scm" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/detect/codec" + "github.com/zricethezav/gitleaks/v8/logging" + "github.com/zricethezav/gitleaks/v8/regexp" + "github.com/zricethezav/gitleaks/v8/report" + "github.com/zricethezav/gitleaks/v8/sources" +) + +const maxDecodeDepth = 8 +const configPath = "../testdata/config/" +const repoBasePath = "../testdata/repos/" +const archivesBasePath = "../testdata/archives/" +const encodedTestValues = ` +# Decoded +-----BEGIN PRIVATE KEY----- +135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb +u+QDkg0spw== +-----END PRIVATE KEY----- + +# Encoded +private_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K' + +# Double Encoded: b64 encoded aws config inside a jwt +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA + +# A small secret at the end to make sure that as the other ones above shrink +# when decoded, the positions are taken into consideration for overlaps +c21hbGwtc2VjcmV0 + +# This tests how it handles when the match bounds go outside the decoded value +secret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA== +# The above encoded again +c2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0= + +# Confirm you can ignore on the decoded value +password="bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw==" + +# This tests that it can do hex encoded data +secret=6465636F6465642D7365637265742D76616C756576484558 + +# This tests that it can do percent encoded data +## partial encoded data +secret=decoded-%73%65%63%72%65%74-valuev2 +## scattered encoded +secret=%64%65coded-%73%65%63%72%65%74-valuev3 + +# Test multi levels of encoding where the source is a partal encoding +# it is important that the bounds of the predecessors are properly +# considered +## single percent encoding in the middle of multi layer b64 +c2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09 +## single percent encoding at the beginning of hex +secret%3d6465636F6465642D7365637265742D76616C75657635 +## multiple percent encodings in a single layer base64 +secret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY= # ends in x86 +## base64 encoded partially percent encoded value +secret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU= +## one of the lines above that went through... a lot +## and there's surrounding text around it +Look at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy? +## Multi percent encode two random characters close to the bounds of the base64 +## encoded data to make sure that the bounds are still correctly calculated +secret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU= +## The similar to the above but also touching the edge of the base64 +secret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34 +## The similar to the above but also touching and overlapping the base64 +secret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34 +` + +var multili = ` +username = "admin" + + + + password = "secret123" +` + +func compare(t *testing.T, a, b []report.Finding) { + if diff := cmp.Diff(a, b, + cmpopts.SortSlices(func(a, b report.Finding) bool { + if a.File != b.File { + return a.File < b.File + } + if a.StartLine != b.StartLine { + return a.StartLine < b.StartLine + } + if a.StartColumn != b.StartColumn { + return a.StartColumn < b.StartColumn + } + if a.EndLine != b.EndLine { + return a.EndLine < b.EndLine + } + if a.EndColumn != b.EndColumn { + return a.EndColumn < b.EndColumn + } + if a.RuleID != b.RuleID { + return a.RuleID < b.RuleID + } + return a.Secret < b.Secret + }), + cmpopts.IgnoreFields(report.Finding{}, + "Fingerprint", "Author", "Email", "Date", "Message", "Commit", "requiredFindings"), + cmpopts.EquateApprox(0.0001, 0), // For floating point Entropy comparison + ); diff != "" { + t.Errorf("findings mismatch (-want +got):\n%s", diff) + } + +} + +func TestDetect(t *testing.T) { + logging.Logger = logging.Logger.Level(zerolog.TraceLevel) + tests := map[string]struct { + cfgName string + baselinePath string + fragment Fragment + // NOTE: for expected findings, all line numbers will be 0 + // because line deltas are added _after_ the finding is created. + // I.e., if the finding is from a --no-git file, the line number will be + // increase by 1 in DetectFromFiles(). If the finding is from git, + // the line number will be increased by the patch delta. + expectedFindings []report.Finding + wantError error + expectedAuxOutput string + }{ + // General + "valid allow comment (1)": { + cfgName: "simple", + fragment: Fragment{ + Raw: `awsToken := \"AKIALALEMEL33243OKIA\ // gitleaks:allow"`, + FilePath: "tmp.go", + }, + }, + "valid allow comment (2)": { + cfgName: "simple", + fragment: Fragment{ + Raw: `awsToken := \ + + \"AKIALALEMEL33243OKIA\ // gitleaks:allow" + + `, + FilePath: "tmp.go", + }, + }, + "invalid allow comment": { + cfgName: "simple", + fragment: Fragment{ + Raw: `awsToken := \"AKIALALEMEL33243OKIA\" + + // gitleaks:allow" + + `, + FilePath: "tmp.go", + }, + expectedFindings: []report.Finding{ + { + Description: "AWS Access Key", + Secret: "AKIALALEMEL33243OKIA", + Match: "AKIALALEMEL33243OKIA", + File: "tmp.go", + Line: `awsToken := \"AKIALALEMEL33243OKIA\"`, + RuleID: "aws-access-key", + Tags: []string{"key", "AWS"}, + StartLine: 0, + EndLine: 0, + StartColumn: 15, + EndColumn: 34, + Entropy: 3.1464393, + }, + }, + }, + "detect finding - aws": { + cfgName: "simple", + fragment: Fragment{ + Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`, + FilePath: "tmp.go", + }, + expectedFindings: []report.Finding{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + File: "tmp.go", + Line: `awsToken := \"AKIALALEMEL33243OLIA\"`, + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + Entropy: 3.0841837, + StartLine: 0, + EndLine: 0, + StartColumn: 15, + EndColumn: 34, + Tags: []string{"key", "AWS"}, + }, + }, + }, + "detect finding - sidekiq env var": { + cfgName: "simple", + fragment: Fragment{ + Raw: `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`, + FilePath: "tmp.sh", + }, + expectedFindings: []report.Finding{ + { + RuleID: "sidekiq-secret", + Description: "Sidekiq Secret", + File: "tmp.sh", + Line: `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`, + Match: "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;", + Secret: "cafebabe:deadbeef", + Entropy: 2.6098502, + StartLine: 0, + EndLine: 0, + StartColumn: 8, + EndColumn: 60, + Tags: []string{}, + }, + }, + }, + "detect finding - sidekiq env var, semicolon": { + cfgName: "simple", + fragment: Fragment{ + Raw: `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`, + FilePath: "tmp.sh", + }, + expectedFindings: []report.Finding{ + { + RuleID: "sidekiq-secret", + Description: "Sidekiq Secret", + File: "tmp.sh", + Line: `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`, + Match: "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=\"cafebabe:deadbeef\"", + Secret: "cafebabe:deadbeef", + Entropy: 2.6098502, + StartLine: 0, + EndLine: 0, + StartColumn: 21, + EndColumn: 74, + Tags: []string{}, + }, + }, + }, + "detect finding - sidekiq url": { + cfgName: "simple", + fragment: Fragment{ + Raw: `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true¶m2=false#heading1"`, + FilePath: "tmp.sh", + }, + expectedFindings: []report.Finding{ + { + RuleID: "sidekiq-sensitive-url", + Description: "Sidekiq Sensitive URL", + File: "tmp.sh", + Line: `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true¶m2=false#heading1"`, + Match: "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:", + Secret: "cafeb4b3:d3adb33f", + Entropy: 2.984234, + StartLine: 0, + EndLine: 0, + StartColumn: 8, + EndColumn: 58, + Tags: []string{}, + }, + }, + }, + "ignore finding - our config file": { + cfgName: "simple", + fragment: Fragment{ + Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`, + FilePath: filepath.Join(configPath, "simple.toml"), + }, + }, + "ignore finding - doesn't match path": { + cfgName: "generic_with_py_path", + fragment: Fragment{ + Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`, + FilePath: "tmp.go", + }, + }, + "detect finding - matches path,regex,entropy": { + cfgName: "generic_with_py_path", + fragment: Fragment{ + Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`, + FilePath: "tmp.py", + }, + expectedFindings: []report.Finding{ + { + RuleID: "generic-api-key", + Description: "Generic API Key", + File: "tmp.py", + Line: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`, + Match: "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"", + Secret: "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5", + Entropy: 3.7906237, + StartLine: 0, + EndLine: 0, + StartColumn: 22, + EndColumn: 93, + Tags: []string{}, + }, + }, + }, + "ignore finding - allowlist regex": { + cfgName: "generic_with_py_path", + fragment: Fragment{ + Raw: `const Discord_Public_Key = "load2523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`, + FilePath: "tmp.py", + }, + }, + + // Rule + "rule - ignore path": { + cfgName: "valid/rule_path_only", + baselinePath: ".baseline.json", + fragment: Fragment{ + Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`, + FilePath: ".baseline.json", + }, + }, + "rule - detect path ": { + cfgName: "valid/rule_path_only", + fragment: Fragment{ + Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`, + FilePath: "tmp.py", + }, + expectedFindings: []report.Finding{ + { + Description: "Python Files", + Match: "file detected: tmp.py", + File: "tmp.py", + RuleID: "python-files-only", + Tags: []string{}, + }, + }, + }, + "rule - match based on entropy": { + cfgName: "valid/rule_entropy_group", + fragment: Fragment{ + Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5" +//const Discord_Public_Key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +`, + FilePath: "tmp.go", + }, + expectedFindings: []report.Finding{ + { + RuleID: "discord-api-key", + Description: "Discord API key", + File: "tmp.go", + Line: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`, + Match: "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"", + Secret: "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5", + Entropy: 3.7906237, + StartLine: 0, + EndLine: 0, + StartColumn: 7, + EndColumn: 93, + Tags: []string{}, + }, + }, + }, + + // Allowlists + "global allowlist - ignore regex": { + cfgName: "valid/allowlist_global_regex", + fragment: Fragment{ + Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`, + FilePath: "tmp.go", + }, + }, + "global allowlist - detect, doesn't match all conditions": { + cfgName: "valid/allowlist_global_multiple", + fragment: Fragment{ + Raw: ` +const token = "mockSecret"; +// const token = "changeit";`, + FilePath: "config.txt", + }, + expectedFindings: []report.Finding{ + { + RuleID: "test", + File: "config.txt", + Line: "\nconst token = \"mockSecret\";", + Match: `token = "mockSecret"`, + Secret: "mockSecret", + Entropy: 2.9219282, + StartLine: 1, + EndLine: 1, + StartColumn: 8, + EndColumn: 27, + Tags: []string{}, + }, + }, + }, + "global allowlist - ignore, matches all conditions": { + cfgName: "valid/allowlist_global_multiple", + fragment: Fragment{ + Raw: `token := "mockSecret";`, + FilePath: "node_modules/config.txt", + }, + }, + "global allowlist - detect path, doesn't match all conditions": { + cfgName: "valid/allowlist_global_multiple", + fragment: Fragment{ + Raw: `var token = "fakeSecret";`, + FilePath: "node_modules/config.txt", + }, + expectedFindings: []report.Finding{ + { + RuleID: "test", + File: "node_modules/config.txt", + Line: "var token = \"fakeSecret\";", + Match: `token = "fakeSecret"`, + Secret: "fakeSecret", + Entropy: 2.8464394, + StartLine: 0, + EndLine: 0, + StartColumn: 5, + EndColumn: 24, + Tags: []string{}, + }, + }, + }, + "allowlist - ignore commit": { + cfgName: "valid/allowlist_rule_commit", + fragment: Fragment{ + Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`, + FilePath: "tmp.go", + CommitSHA: "allowthiscommit", + }, + }, + "allowlist - ignore path": { + cfgName: "valid/allowlist_rule_path", + fragment: Fragment{ + Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`, + FilePath: "tmp.go", + }, + }, + "allowlist - ignore path when extending": { + cfgName: "valid/allowlist_rule_extend_default", + fragment: Fragment{ + Raw: `token = "aebfab88-7596-481d-82e8-c60c8f7de0c0"`, + FilePath: "path/to/your/problematic/file.js", + }, + }, + "allowlist - ignore regex": { + cfgName: "valid/allowlist_rule_regex", + fragment: Fragment{ + Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`, + FilePath: "tmp.go", + }, + }, + "fragment level composite": { + cfgName: "composite", + fragment: Fragment{ + Raw: multili, + }, + expectedFindings: []report.Finding{ + { + Description: "Primary rule", + RuleID: "primary-rule", + StartLine: 5, + EndLine: 5, + StartColumn: 5, + EndColumn: 26, + Line: "\n\t\t\tpassword = \"secret123\"", + Match: `password = "secret123"`, + Secret: "secret123", + Entropy: 2.9477028846740723, + Tags: []string{}, + }, + }, + expectedAuxOutput: "Required: username-rule:1:admin\n", + }, + // Decoding + "detect encoded": { + cfgName: "encoded", + fragment: Fragment{ + Raw: encodedTestValues, + FilePath: "tmp.go", + }, + expectedFindings: []report.Finding{ + { // Plain text key captured by normal rule + Description: "Private Key", + Secret: "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----", + Match: "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----", + File: "tmp.go", + Line: "\n-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----", + RuleID: "private-key", + Tags: []string{"key", "private"}, + StartLine: 2, + EndLine: 5, + StartColumn: 2, + EndColumn: 26, + Entropy: 5.350665, + }, + { // Encoded key captured by custom b64 regex rule + Description: "Private Key", + Secret: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K", + Match: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K", + File: "tmp.go", + Line: "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'", + RuleID: "b64-encoded-private-key", + Tags: []string{"key", "private"}, + StartLine: 8, + EndLine: 8, + StartColumn: 16, + EndColumn: 207, + Entropy: 5.3861146, + }, + { // Encoded key captured by plain text rule using the decoder + Description: "Private Key", + Secret: "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----", + Match: "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----", + File: "tmp.go", + Line: "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'", + RuleID: "private-key", + Tags: []string{"key", "private", "decoded:base64", "decode-depth:1"}, + StartLine: 8, + EndLine: 8, + StartColumn: 16, + EndColumn: 207, + Entropy: 5.350665, + }, + { // Encoded Small secret at the end to make sure it's picked up by the decoding + Description: "Small Secret", + Secret: "small-secret", + Match: "small-secret", + File: "tmp.go", + Line: "\nc21hbGwtc2VjcmV0", + RuleID: "small-secret", + Tags: []string{"small", "secret", "decoded:base64", "decode-depth:1"}, + StartLine: 15, + EndLine: 15, + StartColumn: 2, + EndColumn: 17, + Entropy: 3.0849626, + }, + { // Secret where the decoded match goes outside the encoded value + Description: "Overlapping", + Secret: "decoded-secret-value00", + Match: "secret=decoded-secret-value00", + File: "tmp.go", + Line: "\nsecret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==", + RuleID: "overlapping", + Tags: []string{"overlapping", "decoded:base64", "decode-depth:1"}, + StartLine: 18, + EndLine: 18, + StartColumn: 2, + EndColumn: 40, + Entropy: 3.4428623, + }, + { // This just confirms that with no allowlist the pattern is detected (i.e. the regex is good) + Description: "Make sure this would be detected with no allowlist", + Secret: "lRqBK-z5kf4-please-ignore-me-X-XIJM2Pddw", + Match: "password=\"lRqBK-z5kf4-please-ignore-me-X-XIJM2Pddw\"", + File: "tmp.go", + Line: "\npassword=\"bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw==\"", + RuleID: "decoded-password-dont-ignore", + Tags: []string{"decode-ignore", "decoded:base64", "decode-depth:1"}, + StartLine: 23, + EndLine: 23, + StartColumn: 2, + EndColumn: 68, + Entropy: 4.5841837, + }, + { // Hex encoded data check + Description: "Overlapping", + Secret: "decoded-secret-valuevHEX", + Match: "secret=decoded-secret-valuevHEX", + File: "tmp.go", + Line: "\nsecret=6465636F6465642D7365637265742D76616C756576484558", + RuleID: "overlapping", + Tags: []string{"overlapping", "decoded:hex", "decode-depth:1"}, + StartLine: 26, + EndLine: 26, + StartColumn: 2, + EndColumn: 56, + Entropy: 3.6531072, + }, + { // handle partial encoded percent data + Description: "Overlapping", + Secret: "decoded-secret-valuev2", + Match: "secret=decoded-secret-valuev2", + File: "tmp.go", + Line: "\nsecret=decoded-%73%65%63%72%65%74-valuev2", + RuleID: "overlapping", + Tags: []string{"overlapping", "decoded:percent", "decode-depth:1"}, + StartLine: 30, + EndLine: 30, + StartColumn: 2, + EndColumn: 42, + Entropy: 3.4428623, + }, + { // handle partial encoded percent data + Description: "Overlapping", + Secret: "decoded-secret-valuev3", + Match: "secret=decoded-secret-valuev3", + File: "tmp.go", + Line: "\nsecret=%64%65coded-%73%65%63%72%65%74-valuev3", + RuleID: "overlapping", + Tags: []string{"overlapping", "decoded:percent", "decode-depth:1"}, + StartLine: 32, + EndLine: 32, + StartColumn: 2, + EndColumn: 46, + Entropy: 3.4428623, + }, + { // Encoded AWS config with a access key id inside a JWT + Description: "AWS IAM Unique Identifier", + Secret: "ASIAIOSFODNN7LXM10JI", + Match: " ASIAIOSFODNN7LXM10JI", + File: "tmp.go", + Line: "\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA", + RuleID: "aws-iam-unique-identifier", + Tags: []string{"aws", "identifier", "decoded:base64", "decode-depth:2"}, + StartLine: 11, + EndLine: 11, + StartColumn: 39, + EndColumn: 344, + Entropy: 3.6841838, + }, + { // Encoded AWS config with a secret access key inside a JWT + Description: "AWS Secret Access Key", + Secret: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEFUCDlEX1A", + Match: "aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEFUCDlEX1A", + File: "tmp.go", + Line: "\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA", + RuleID: "aws-secret-access-key", + Tags: []string{"aws", "secret", "decoded:base64", "decode-depth:2"}, + StartLine: 11, + EndLine: 11, + StartColumn: 39, + EndColumn: 344, + Entropy: 4.721928, + }, + { // Secret where the decoded match goes outside the encoded value and then encoded again + Description: "Overlapping", + Secret: "decoded-secret-value", + Match: "secret=decoded-secret-value", + File: "tmp.go", + Line: "\nc2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=", + RuleID: "overlapping", + Tags: []string{"overlapping", "decoded:base64", "decode-depth:2"}, + StartLine: 20, + EndLine: 20, + StartColumn: 2, + EndColumn: 49, + Entropy: 3.3037016, + }, + { // handle encodings that touch eachother + Description: "Overlapping", + Secret: "decoded-secret-valuev5", + Match: "secret=decoded-secret-valuev5", + File: "tmp.go", + Line: "\nsecret%3d6465636F6465642D7365637265742D76616C75657635", + RuleID: "overlapping", + Tags: []string{"overlapping", "decoded:percent", "decoded:hex", "decode-depth:2"}, + StartLine: 40, + EndLine: 40, + StartColumn: 2, + EndColumn: 54, + Entropy: 3.4428623, + }, + { // handle partial encoded percent data465642D7365637265742D76616C75657635 + Description: "Overlapping", + Secret: "decoded-secret-valuev4", + Match: "secret=decoded-secret-valuev4", + File: "tmp.go", + Line: "\nc2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09", + RuleID: "overlapping", + Tags: []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:3"}, + StartLine: 38, + EndLine: 38, + StartColumn: 2, + EndColumn: 55, + Entropy: 3.4428623, + }, + { // multiple percent encodings in a single layer base64 + Description: "Overlapping", + Secret: "decoded-secret-valuex86", + Match: "secret=decoded-secret-valuex86", + File: "tmp.go", + Line: "\nsecret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY= # ends in x86", + RuleID: "overlapping", + Tags: []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:2"}, + StartLine: 42, + EndLine: 42, + StartColumn: 2, + EndColumn: 44, + Entropy: 3.6381476, + }, + { // base64 encoded partially percent encoded value + Description: "Overlapping", + Secret: "decoded-secret-value", + Match: "secret=decoded-secret-value", + File: "tmp.go", + Line: "\nsecret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU=", + RuleID: "overlapping", + Tags: []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:2"}, + StartLine: 44, + EndLine: 44, + StartColumn: 2, + EndColumn: 52, + Entropy: 3.3037016, + }, + { // one of the lines above that went through... a lot + Description: "Overlapping", + Secret: "decoded-secret-value", + Match: "secret=decoded-secret-value", + File: "tmp.go", + Line: "\nLook at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy?", + RuleID: "overlapping", + Tags: []string{"overlapping", "decoded:percent", "decoded:hex", "decoded:base64", "decode-depth:7"}, + StartLine: 47, + EndLine: 47, + StartColumn: 22, + EndColumn: 177, + Entropy: 3.3037016, + }, + { // Multi percent encode two random characters close to the bounds of the base64 + Description: "Overlapping", + Secret: "decoded-secret-value", + Match: "secret=decoded-secret-value", + File: "tmp.go", + Line: "\nsecret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU=", + RuleID: "overlapping", + Tags: []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:5"}, + StartLine: 50, + EndLine: 50, + StartColumn: 2, + EndColumn: 300, + Entropy: 3.3037016, + }, + { // The similar to the above but also touching the edge of the base64 + Description: "Overlapping", + Secret: "decoded-secret-value", + Match: "secret=decoded-secret-value", + File: "tmp.go", + Line: "\nsecret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34", + RuleID: "overlapping", + Tags: []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:4"}, + StartLine: 52, + EndLine: 52, + StartColumn: 2, + EndColumn: 86, + Entropy: 3.3037016, + }, + { // The similar to the above but also touching and overlapping the base64 + Description: "Overlapping", + Secret: "decoded-secret-value", + Match: "secret=decoded-secret-value", + File: "tmp.go", + Line: "\nsecret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34", + RuleID: "overlapping", + Tags: []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:4"}, + StartLine: 54, + EndLine: 54, + StartColumn: 2, + EndColumn: 88, + Entropy: 3.3037016, + }, + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + viper.Reset() + viper.AddConfigPath(configPath) + viper.SetConfigName(tt.cfgName) + viper.SetConfigType("toml") + err := viper.ReadInConfig() + require.NoError(t, err) + + var vc config.ViperConfig + err = viper.Unmarshal(&vc) + require.NoError(t, err) + cfg, err := vc.Translate() + cfg.Path = filepath.Join(configPath, tt.cfgName+".toml") + assert.Equal(t, tt.wantError, err) + d := NewDetector(cfg) + d.MaxDecodeDepth = maxDecodeDepth + d.baselinePath = tt.baselinePath + + findings := d.Detect(tt.fragment) + + compare(t, findings, tt.expectedFindings) + + // extremely goofy way to test auxiliary findings + // capture stdout and print that sonabitch + // TODO + if tt.expectedAuxOutput != "" { + capturedOutput := captureStdout(func() { + for _, finding := range findings { + finding.PrintRequiredFindings() + } + }) + + // Clean up the output for comparison (remove ANSI color codes) + cleanOutput := stripANSI(capturedOutput) + expectedClean := stripANSI(tt.expectedAuxOutput) + + assert.Equal(t, expectedClean, cleanOutput, "Auxiliary output should match") + } + + }) + } +} + +func stripANSI(s string) string { + ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`) + return ansiRegex.ReplaceAllString(s, "") +} + +func captureStdout(f func()) string { + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + f() + + w.Close() + os.Stdout = oldStdout + + var buf bytes.Buffer + io.Copy(&buf, r) + return buf.String() +} + +// TestFromGit tests the FromGit function +func TestFromGit(t *testing.T) { + // TODO: Fix this test on windows. + if runtime.GOOS == "windows" { + t.Skipf("TODO: this fails on Windows: [git] fatal: bad object refs/remotes/origin/main?") + return + } + tests := []struct { + cfgName string + source string + logOpts string + expectedFindings []report.Finding + }{ + { + source: filepath.Join(repoBasePath, "small"), + cfgName: "simple", // the remote url is `git@github.com:gitleaks/test.git` + expectedFindings: []report.Finding{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 19, + EndColumn: 38, + Line: "\n awsToken := \"AKIALALEMEL33243OLIA\"", + Secret: "AKIALALEMEL33243OLIA", + Match: "AKIALALEMEL33243OLIA", + Entropy: 3.0841837, + File: "main.go", + Date: "2021-11-02T23:37:53Z", + Commit: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587", + Author: "Zachary Rice", + Email: "zricer@protonmail.com", + Message: "Accidentally add a secret", + Tags: []string{"key", "AWS"}, + Fingerprint: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587:main.go:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/1b6da43b82b22e4eaa10bcf8ee591e91abbfc587/main.go#L20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 9, + EndLine: 9, + StartColumn: 17, + EndColumn: 36, + Secret: "AKIALALEMEL33243OLIA", + Match: "AKIALALEMEL33243OLIA", + Line: "\n\taws_token := \"AKIALALEMEL33243OLIA\"", + File: "foo/foo.go", + Date: "2021-11-02T23:48:06Z", + Commit: "491504d5a31946ce75e22554cc34203d8e5ff3ca", + Author: "Zach Rice", + Email: "zricer@protonmail.com", + Message: "adding foo package with secret", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9", + Link: "https://github.com/gitleaks/test/blob/491504d5a31946ce75e22554cc34203d8e5ff3ca/foo/foo.go#L9", + }, + }, + }, + { + source: filepath.Join(repoBasePath, "small"), + logOpts: "--all foo...", + cfgName: "simple", + expectedFindings: []report.Finding{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 9, + EndLine: 9, + StartColumn: 17, + EndColumn: 36, + Secret: "AKIALALEMEL33243OLIA", + Line: "\n\taws_token := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Date: "2021-11-02T23:48:06Z", + File: "foo/foo.go", + Commit: "491504d5a31946ce75e22554cc34203d8e5ff3ca", + Author: "Zach Rice", + Email: "zricer@protonmail.com", + Message: "adding foo package with secret", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9", + Link: "https://github.com/gitleaks/test/blob/491504d5a31946ce75e22554cc34203d8e5ff3ca/foo/foo.go#L9", + }, + }, + }, + { + source: filepath.Join(repoBasePath, "archives"), + cfgName: "archives", + expectedFindings: []report.Finding{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "main.go.zst", + Commit: "db8789716fc664dbce0ed2d492570e92abf717a5", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:10:39Z", + Message: "Add main.go.zst", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "db8789716fc664dbce0ed2d492570e92abf717a5:main.go.zst:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/db8789716fc664dbce0ed2d492570e92abf717a5/main.go.zst#L20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files.tar!files/api.go", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar!files/api.go:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files.tar!files/main.go", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar!files/main.go:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files.zip!files/api.go", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.zip!files/api.go:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files.zip!files/main.go", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.zip!files/main.go:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files.7z!files/api.go", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.7z!files/api.go:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files.7z!files/main.go", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.7z!files/main.go:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files.tar.zst!files/api.go", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.zst!files/api.go:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files.tar.zst!files/main.go", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.zst!files/main.go:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files/api.go", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/api.go:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files/main.go", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files/main.go.xz", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.xz:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files/main.go.zst", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.zst:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files/main.go.gz", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.gz:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files.tar.xz!files/api.go", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.xz!files/api.go:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "nested.tar.gz!archives/files.tar.xz!files/main.go", + Commit: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68", + Author: "Test User", + Email: "user@example.com", + Date: "2025-05-27T05:08:50Z", + Message: "Add nested.tar.gz", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.xz!files/main.go:aws-access-key:20", + Link: "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz", + }, + }, + }, + } + + moveDotGit(t, "dotGit", ".git") + defer moveDotGit(t, ".git", "dotGit") + + for _, tt := range tests { + t.Run(strings.Join([]string{tt.cfgName, tt.source, tt.logOpts}, "/"), func(t *testing.T) { + viper.AddConfigPath(configPath) + viper.SetConfigName("simple") + viper.SetConfigType("toml") + err := viper.ReadInConfig() + require.NoError(t, err) + + var vc config.ViperConfig + err = viper.Unmarshal(&vc) + require.NoError(t, err) + cfg, err := vc.Translate() + require.NoError(t, err) + detector := NewDetector(cfg) + detector.MaxArchiveDepth = 8 + + var ignorePath string + info, err := os.Stat(tt.source) + require.NoError(t, err) + + if info.IsDir() { + ignorePath = filepath.Join(tt.source, ".gitleaksignore") + } else { + ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore") + } + err = detector.AddGitleaksIgnore(ignorePath) + require.NoError(t, err) + + gitCmd, err := sources.NewGitLogCmd(tt.source, tt.logOpts) + require.NoError(t, err) + + remote := NewRemoteInfo(scm.UnknownPlatform, tt.source) + findings, err := detector.DetectGit(gitCmd, remote) + require.NoError(t, err) + + for _, f := range findings { + f.Match = "" // remove lines cause copying and pasting them has some wack formatting + } + assert.ElementsMatch(t, tt.expectedFindings, findings) + }) + } +} + +func TestFromGitStaged(t *testing.T) { + tests := []struct { + cfgName string + source string + logOpts string + expectedFindings []report.Finding + }{ + { + source: filepath.Join(repoBasePath, "staged"), + cfgName: "simple", + expectedFindings: []report.Finding{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 7, + EndLine: 7, + StartColumn: 18, + EndColumn: 37, + Line: "\n\taws_token2 := \"AKIALALEMEL33243OLIA\" // this one is not", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "api/api.go", + SymlinkFile: "", + Commit: "", + Entropy: 3.0841837, + Author: "", + Email: "", + Date: "0001-01-01T00:00:00Z", + Message: "", + Tags: []string{ + "key", + "AWS", + }, + Fingerprint: "api/api.go:aws-access-key:7", + Link: "", + }, + }, + }, + } + + moveDotGit(t, "dotGit", ".git") + defer moveDotGit(t, ".git", "dotGit") + for _, tt := range tests { + + viper.AddConfigPath(configPath) + viper.SetConfigName("simple") + viper.SetConfigType("toml") + err := viper.ReadInConfig() + require.NoError(t, err) + + var vc config.ViperConfig + err = viper.Unmarshal(&vc) + require.NoError(t, err) + cfg, err := vc.Translate() + require.NoError(t, err) + detector := NewDetector(cfg) + err = detector.AddGitleaksIgnore(filepath.Join(tt.source, ".gitleaksignore")) + require.NoError(t, err) + gitCmd, err := sources.NewGitDiffCmd(tt.source, true) + require.NoError(t, err) + remote := NewRemoteInfo(scm.UnknownPlatform, tt.source) + findings, err := detector.DetectGit(gitCmd, remote) + require.NoError(t, err) + + for _, f := range findings { + f.Match = "" // remove lines cause copying and pasting them has some wack formatting + } + assert.ElementsMatch(t, tt.expectedFindings, findings) + } +} + +// TestFromFiles tests the FromFiles function +func TestFromFiles(t *testing.T) { + tests := []struct { + cfgName string + source string + expectedFindings []report.Finding + }{ + { + source: filepath.Join(repoBasePath, "nogit"), + cfgName: "simple", + expectedFindings: []report.Finding{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/repos/nogit/main.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20", + }, + }, + }, + { + source: filepath.Join(repoBasePath, "nogit", "main.go"), + cfgName: "simple", + expectedFindings: []report.Finding{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/repos/nogit/main.go", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20", + }, + }, + }, + { + source: filepath.Join(repoBasePath, "nogit", "api.go"), + cfgName: "simple", + expectedFindings: []report.Finding{}, + }, + { + source: filepath.Join(repoBasePath, "nogit", ".env.prod"), + cfgName: "generic", + expectedFindings: []report.Finding{ + { + RuleID: "generic-api-key", + Description: "Generic API Key", + StartLine: 4, + EndLine: 4, + StartColumn: 5, + EndColumn: 35, + Line: "\nDB_PASSWORD=8ae31cacf141669ddfb5da", + Match: "PASSWORD=8ae31cacf141669ddfb5da", + Secret: "8ae31cacf141669ddfb5da", + File: "../testdata/repos/nogit/.env.prod", + Tags: []string{}, + Entropy: 3.5383105, + Fingerprint: "../testdata/repos/nogit/.env.prod:generic-api-key:4", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) { + viper.AddConfigPath(configPath) + viper.SetConfigName(tt.cfgName) + viper.SetConfigType("toml") + err := viper.ReadInConfig() + require.NoError(t, err) + + var vc config.ViperConfig + err = viper.Unmarshal(&vc) + require.NoError(t, err) + + cfg, _ := vc.Translate() + detector := NewDetector(cfg) + + info, err := os.Stat(tt.source) + require.NoError(t, err) + + var ignorePath string + if info.IsDir() { + ignorePath = filepath.Join(tt.source, ".gitleaksignore") + } else { + ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore") + } + err = detector.AddGitleaksIgnore(ignorePath) + require.NoError(t, err) + + detector.FollowSymlinks = true + paths, err := sources.DirectoryTargets(tt.source, detector.Sema, true, cfg.Allowlists) + require.NoError(t, err) + + findings, err := detector.DetectFiles(paths) + require.NoError(t, err) + + // TODO: Temporary mitigation. + // https://github.com/gitleaks/gitleaks/issues/1641 + normalizedFindings := make([]report.Finding, len(findings)) + for i, f := range findings { + if strings.HasSuffix(f.Line, "\r") { + f.Line = strings.ReplaceAll(f.Line, "\r", "") + } + if strings.HasSuffix(f.Match, "\r") { + f.EndColumn = f.EndColumn - 1 + f.Match = strings.ReplaceAll(f.Match, "\r", "") + } + normalizedFindings[i] = f + } + assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings) + }) + } +} + +func TestDetectWithArchives(t *testing.T) { + tests := []struct { + cfgName string + source string + expireContext bool + expectedFindings []report.Finding + }{ + { + source: filepath.Join(archivesBasePath, "this-path-does-not-exist"), + cfgName: "archives", + expectedFindings: []report.Finding{}, + }, + { + source: filepath.Join(archivesBasePath, "files"), + cfgName: "archives", + expectedFindings: []report.Finding{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files/api.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files/api.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files/main.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files/main.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files/main.go.gz", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files/main.go.gz:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files/main.go.xz", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files/main.go.xz:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files/main.go.zst", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files/main.go.zst:aws-access-key:20", + }, + }, + }, + { + source: filepath.Join(archivesBasePath, "files.7z"), + cfgName: "archives", + expectedFindings: []report.Finding{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files.7z!files/api.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files.7z!files/api.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files.7z!files/main.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files.7z!files/main.go:aws-access-key:20", + }, + }, + }, + { + source: filepath.Join(archivesBasePath, "files.tar"), + cfgName: "archives", + expectedFindings: []report.Finding{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files.tar!files/api.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files.tar!files/api.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files.tar!files/main.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files.tar!files/main.go:aws-access-key:20", + }, + }, + }, + { + source: filepath.Join(archivesBasePath, "files.tar.xz"), + cfgName: "archives", + expectedFindings: []report.Finding{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files.tar.xz!files/api.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files.tar.xz!files/api.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files.tar.xz!files/main.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files.tar.xz!files/main.go:aws-access-key:20", + }, + }, + }, + { + source: filepath.Join(archivesBasePath, "files.tar.zst"), + cfgName: "archives", + expectedFindings: []report.Finding{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files.tar.zst!files/api.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files.tar.zst!files/api.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files.tar.zst!files/main.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files.tar.zst!files/main.go:aws-access-key:20", + }, + }, + }, + { + source: filepath.Join(archivesBasePath, "files.zip"), + cfgName: "archives", + expectedFindings: []report.Finding{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files.zip!files/api.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files.zip!files/api.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/files.zip!files/main.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/files.zip!files/main.go:aws-access-key:20", + }, + }, + }, + { + source: filepath.Join(archivesBasePath, "nested.tar.gz"), + cfgName: "archives", + expectedFindings: []report.Finding{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files.tar!files/api.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar!files/api.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files.tar!files/main.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar!files/main.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files.zip!files/api.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.zip!files/api.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files.zip!files/main.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.zip!files/main.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files.7z!files/api.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.7z!files/api.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files.7z!files/main.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.7z!files/main.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files.tar.zst!files/api.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.zst!files/api.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files.tar.zst!files/main.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.zst!files/main.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files/api.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/api.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files/main.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/main.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files/main.go.xz", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/main.go.xz:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files/main.go.zst", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/main.go.zst:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files/main.go.gz", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/main.go.gz:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/api.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/api.go:aws-access-key:20", + }, + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + StartLine: 20, + EndLine: 20, + StartColumn: 16, + EndColumn: 35, + Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"", + Match: "AKIALALEMEL33243OLIA", + Secret: "AKIALALEMEL33243OLIA", + File: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/main.go", + SymlinkFile: "", + Tags: []string{"key", "AWS"}, + Entropy: 3.0841837, + Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/main.go:aws-access-key:20", + }, + }, + }, + { + source: filepath.Join(archivesBasePath, "nested.tar.gz"), + cfgName: "archives", + expireContext: true, + expectedFindings: []report.Finding{}, + }, + } + + for _, tt := range tests { + t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) { + viper.AddConfigPath(configPath) + viper.SetConfigName(tt.cfgName) + viper.SetConfigType("toml") + err := viper.ReadInConfig() + require.NoError(t, err) + + var vc config.ViperConfig + err = viper.Unmarshal(&vc) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + if tt.expireContext { + cancel() + } + + cfg, _ := vc.Translate() + detector := NewDetectorContext(ctx, cfg) + detector.MaxArchiveDepth = 8 + + findings, err := detector.DetectSource( + ctx, &sources.Files{ + Path: tt.source, + Sema: detector.Sema, + Config: &cfg, + MaxArchiveDepth: detector.MaxArchiveDepth, + }, + ) + + if tt.expireContext { + require.EqualError(t, err, "context canceled") + } else { + cancel() + require.NoError(t, err) + } + + // TODO: Temporary mitigation. + // https://github.com/gitleaks/gitleaks/issues/1641 + normalizedFindings := make([]report.Finding, len(findings)) + for i, f := range findings { + if strings.HasSuffix(f.Line, "\r") { + f.Line = strings.ReplaceAll(f.Line, "\r", "") + } + if strings.HasSuffix(f.Match, "\r") { + f.EndColumn = f.EndColumn - 1 + f.Match = strings.ReplaceAll(f.Match, "\r", "") + } + normalizedFindings[i] = f + } + assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings) + }) + } + +} + +func TestDetectWithSymlinks(t *testing.T) { + // TODO: Fix this test on windows. + if runtime.GOOS == "windows" { + t.Skipf("TODO: this returns no results on windows, I'm not sure why.") + return + } + + tests := []struct { + cfgName string + source string + expectedFindings []report.Finding + }{ + { + source: filepath.Join(repoBasePath, "symlinks/file_symlink"), + cfgName: "simple", + expectedFindings: []report.Finding{ + { + RuleID: "apkey", + Description: "Asymmetric Private Key", + StartLine: 1, + EndLine: 1, + StartColumn: 1, + EndColumn: 35, + Match: "-----BEGIN OPENSSH PRIVATE KEY-----", + Secret: "-----BEGIN OPENSSH PRIVATE KEY-----", + Line: "-----BEGIN OPENSSH PRIVATE KEY-----", + File: "../testdata/repos/symlinks/source_file/id_ed25519", + SymlinkFile: "../testdata/repos/symlinks/file_symlink/symlinked_id_ed25519", + Tags: []string{"key", "AsymmetricPrivateKey"}, + Entropy: 3.587164, + Fingerprint: "../testdata/repos/symlinks/source_file/id_ed25519:apkey:1", + }, + }, + }, + } + + for _, tt := range tests { + viper.AddConfigPath(configPath) + viper.SetConfigName("simple") + viper.SetConfigType("toml") + err := viper.ReadInConfig() + require.NoError(t, err) + + var vc config.ViperConfig + err = viper.Unmarshal(&vc) + require.NoError(t, err) + + cfg, _ := vc.Translate() + detector := NewDetector(cfg) + detector.FollowSymlinks = true + paths, err := sources.DirectoryTargets(tt.source, detector.Sema, true, cfg.Allowlists) + require.NoError(t, err) + + findings, err := detector.DetectFiles(paths) + require.NoError(t, err) + assert.ElementsMatch(t, tt.expectedFindings, findings) + } +} + +func TestDetectRuleAllowlist(t *testing.T) { + cases := map[string]struct { + fragment Fragment + allowlist *config.Allowlist + expected []report.Finding + }{ + // Commit / path + "commit allowed": { + fragment: Fragment{ + CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", + }, + allowlist: &config.Allowlist{ + Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"}, + }, + }, + "path allowed": { + fragment: Fragment{ + FilePath: "package-lock.json", + }, + allowlist: &config.Allowlist{ + Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)}, + }, + }, + "commit AND path allowed": { + fragment: Fragment{ + CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", + FilePath: "package-lock.json", + }, + allowlist: &config.Allowlist{ + MatchCondition: config.AllowlistMatchAnd, + Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"}, + Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)}, + }, + }, + "commit AND path NOT allowed": { + fragment: Fragment{ + CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", + FilePath: "package.json", + }, + allowlist: &config.Allowlist{ + MatchCondition: config.AllowlistMatchAnd, + Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"}, + Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)}, + }, + expected: []report.Finding{ + { + StartLine: 1, + EndLine: 1, + StartColumn: 18, + EndColumn: 28, + Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';", + Match: "Summer2024!", + Secret: "Summer2024!", + File: "package.json", + Commit: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", + Entropy: 3.095795154571533, + RuleID: "test-rule", + }, + }, + }, + "commit AND path NOT allowed - other conditions": { + fragment: Fragment{ + CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", + FilePath: "package-lock.json", + }, + allowlist: &config.Allowlist{ + MatchCondition: config.AllowlistMatchAnd, + Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"}, + Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)}, + Regexes: []*regexp.Regexp{regexp.MustCompile("password")}, + }, + expected: []report.Finding{ + { + StartLine: 1, + EndLine: 1, + StartColumn: 18, + EndColumn: 28, + Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';", + Match: "Summer2024!", + Secret: "Summer2024!", + File: "package-lock.json", + Commit: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", + Entropy: 3.095795154571533, + RuleID: "test-rule", + }, + }, + }, + "commit OR path allowed": { + fragment: Fragment{ + CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", + FilePath: "package-lock.json", + }, + allowlist: &config.Allowlist{ + MatchCondition: config.AllowlistMatchOr, + Commits: []string{"704178e7dca77ff143778a31cff0fc192d59b030"}, + Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)}, + }, + }, + + // Regex / stopwords + "regex allowed": { + fragment: Fragment{}, + allowlist: &config.Allowlist{ + Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)}, + }, + }, + "stopwords allowed": { + fragment: Fragment{}, + allowlist: &config.Allowlist{ + StopWords: []string{"summer"}, + }, + }, + "regex AND stopword allowed": { + fragment: Fragment{}, + allowlist: &config.Allowlist{ + MatchCondition: config.AllowlistMatchAnd, + Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)}, + StopWords: []string{"2024"}, + }, + }, + "regex AND stopword allowed - other conditions": { + fragment: Fragment{ + CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48", + FilePath: "config.js", + }, + allowlist: &config.Allowlist{ + MatchCondition: config.AllowlistMatchAnd, + Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"}, + Paths: []*regexp.Regexp{regexp.MustCompile(`config.js`)}, + Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)}, + StopWords: []string{"2024"}, + }, + }, + "regex AND stopword NOT allowed - non-git, other conditions": { + fragment: Fragment{ + FilePath: "config.js", + }, + allowlist: &config.Allowlist{ + MatchCondition: config.AllowlistMatchAnd, + Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"}, + Paths: []*regexp.Regexp{regexp.MustCompile(`config.js`)}, + Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)}, + StopWords: []string{"2024"}, + }, + expected: []report.Finding{ + { + StartLine: 1, + EndLine: 1, + StartColumn: 18, + EndColumn: 28, + Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';", + Match: "Summer2024!", + Secret: "Summer2024!", + File: "config.js", + Entropy: 3.095795154571533, + RuleID: "test-rule", + }, + }, + }, + "regex AND stopword NOT allowed": { + fragment: Fragment{}, + allowlist: &config.Allowlist{ + MatchCondition: config.AllowlistMatchAnd, + Regexes: []*regexp.Regexp{ + regexp.MustCompile(`(?i)winter.+`), + }, + StopWords: []string{"2024"}, + }, + expected: []report.Finding{ + { + StartLine: 1, + EndLine: 1, + StartColumn: 18, + EndColumn: 28, + Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';", + Match: "Summer2024!", + Secret: "Summer2024!", + Entropy: 3.095795154571533, + RuleID: "test-rule", + }, + }, + }, + "regex AND stopword NOT allowed - other conditions": { + fragment: Fragment{ + CommitSHA: "a060c9d2d5e90c992763f1bd4c3cd2a6f121241b", + FilePath: "config.js", + }, + allowlist: &config.Allowlist{ + MatchCondition: config.AllowlistMatchAnd, + Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"}, + Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)}, + Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)winter.+`)}, + StopWords: []string{"2024"}, + }, + expected: []report.Finding{ + { + StartLine: 1, + EndLine: 1, + StartColumn: 18, + EndColumn: 28, + Line: "let username = 'james@mail.com';\nlet password = 'Summer2024!';", + Match: "Summer2024!", + Secret: "Summer2024!", + File: "config.js", + Commit: "a060c9d2d5e90c992763f1bd4c3cd2a6f121241b", + Entropy: 3.095795154571533, + RuleID: "test-rule", + }, + }, + }, + "regex OR stopword allowed": { + fragment: Fragment{}, + allowlist: &config.Allowlist{ + MatchCondition: config.AllowlistMatchOr, + Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)}, + StopWords: []string{"winter"}, + }, + }, + } + + raw := `let username = 'james@mail.com'; +let password = 'Summer2024!';` + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := tc.allowlist.Validate() + require.NoError(t, err) + + rule := config.Rule{ + RuleID: "test-rule", + Regex: regexp.MustCompile(`Summer2024!`), + Allowlists: []*config.Allowlist{ + tc.allowlist, + }, + } + d, err := NewDetectorDefaultConfig() + require.NoError(t, err) + + f := tc.fragment + f.Raw = raw + + actual := d.detectRule(f, raw, rule, []*codec.EncodedSegment{}) + compare(t, tc.expected, actual) + }) + } +} + +func moveDotGit(t *testing.T, from, to string) { + t.Helper() + + repoDirs, err := os.ReadDir("../testdata/repos") + require.NoError(t, 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)) + require.NoError(t, err) + } +} + +// region Windows-specific tests[] +func TestNormalizeGitleaksIgnorePaths(t *testing.T) { + d, err := NewDetectorDefaultConfig() + require.NoError(t, err) + + err = d.AddGitleaksIgnore("../testdata/gitleaksignore/.windowspaths") + require.NoError(t, err) + + assert.Len(t, d.gitleaksIgnore, 3) + expected := map[string]struct{}{ + "foo/bar/gitleaks-false-positive.yaml:aws-access-token:4": {}, + "foo/bar/gitleaks-false-positive.yaml:aws-access-token:5": {}, + "b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:73": {}, + } + assert.ElementsMatch(t, maps.Keys(d.gitleaksIgnore), maps.Keys(expected)) +} + +func TestWindowsFileSeparator_RulePath(t *testing.T) { + unixRule := config.Rule{ + RuleID: "test-rule", + Path: regexp.MustCompile(`(^|/)\.m2/settings\.xml`), + } + windowsRule := config.Rule{ + RuleID: "test-rule", + Path: regexp.MustCompile(`(^|\\)\.m2\\settings\.xml`), + } + expected := []report.Finding{ + { + RuleID: "test-rule", + Match: "file detected: .m2/settings.xml", + File: ".m2/settings.xml", + }, + } + tests := map[string]struct { + fragment Fragment + rule config.Rule + expected []report.Finding + }{ + // unix rule + "unix rule - unix path separator": { + fragment: Fragment{ + FilePath: `.m2/settings.xml`, + }, + rule: unixRule, + expected: expected, + }, + "unix rule - windows path separator": { + fragment: Fragment{ + FilePath: `.m2/settings.xml`, + WindowsFilePath: `.m2\settings.xml`, + }, + rule: unixRule, + expected: expected, + }, + "unix regex+path rule - windows path separator": { + fragment: Fragment{ + Raw: `s3cr3t`, + FilePath: `.m2/settings.xml`, + }, + rule: config.Rule{ + RuleID: "test-rule", + Regex: regexp.MustCompile(`(.+?)`), + Path: regexp.MustCompile(`(^|/)\.m2/settings\.xml`), + }, + expected: []report.Finding{ + { + RuleID: "test-rule", + StartColumn: 1, + EndColumn: 27, + Line: "s3cr3t", + Match: "s3cr3t", + Secret: "s3cr3t", + Entropy: 2.251629114151001, + File: ".m2/settings.xml", + }, + }, + }, + + // windows rule + "windows rule - unix path separator": { + fragment: Fragment{ + FilePath: `.m2/settings.xml`, + }, + rule: windowsRule, + // This never worked, and continues not to work. + // Paths should be normalized to use Unix file separators. + expected: nil, + }, + "windows rule - windows path separator": { + fragment: Fragment{ + FilePath: `.m2/settings.xml`, + WindowsFilePath: `.m2\settings.xml`, + }, + rule: windowsRule, + expected: expected, + }, + "windows regex+path rule - windows path separator": { + fragment: Fragment{ + Raw: `s3cr3t`, + FilePath: `.m2/settings.xml`, + WindowsFilePath: `.m2\settings.xml`, + }, + rule: config.Rule{ + RuleID: "test-rule", + Regex: regexp.MustCompile(`(.+?)`), + Path: regexp.MustCompile(`(^|\\)\.m2\\settings\.xml`), + }, + expected: []report.Finding{ + { + RuleID: "test-rule", + StartColumn: 1, + EndColumn: 27, + Line: "s3cr3t", + Match: "s3cr3t", + Secret: "s3cr3t", + Entropy: 2.251629114151001, + File: ".m2/settings.xml", + }, + }}, + } + + d, err := NewDetectorDefaultConfig() + require.NoError(t, err) + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := d.detectRule(test.fragment, test.fragment.Raw, test.rule, []*codec.EncodedSegment{}) + compare(t, test.expected, actual) + }) + } +} + +func TestWindowsFileSeparator_RuleAllowlistPaths(t *testing.T) { + tests := map[string]struct { + fragment Fragment + rule config.Rule + expected []report.Finding + }{ + // unix + "unix path separator - unix rule - OR allowlist path-only": { + fragment: Fragment{ + Raw: `value: "s3cr3t"`, + FilePath: `ignoreme/unix.txt`, + }, + rule: config.Rule{ + RuleID: "unix-rule", + Regex: regexp.MustCompile(`s3cr3t`), + Allowlists: []*config.Allowlist{ + { + Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)}, + }, + }, + }, + expected: nil, + }, + "unix path separator - windows rule - OR allowlist path-only": { + fragment: Fragment{ + Raw: `value: "s3cr3t"`, + FilePath: `ignoreme/unix.txt`, + }, + rule: config.Rule{ + RuleID: "windows-rule", + Regex: regexp.MustCompile(`s3cr3t`), + Allowlists: []*config.Allowlist{ + { + Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)}, + }, + }, + }, + // Windows separators in regex don't work for unix. + expected: []report.Finding{ + { + RuleID: "windows-rule", + StartColumn: 9, + EndColumn: 14, + Line: `value: "s3cr3t"`, + Match: `s3cr3t`, + Secret: `s3cr3t`, + File: "ignoreme/unix.txt", + Entropy: 2.251629114151001, + }, + }, + }, + "unix path separator - unix rule - AND allowlist path+stopwords": { + fragment: Fragment{ + Raw: `value: "f4k3s3cr3t"`, + FilePath: `ignoreme/unix.txt`, + }, + rule: config.Rule{ + RuleID: "unix-rule", + Regex: regexp.MustCompile(`value: "[^"]+"`), + Allowlists: []*config.Allowlist{ + { + MatchCondition: config.AllowlistMatchAnd, + Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)}, + StopWords: []string{"f4k3"}, + }, + }, + }, + expected: nil, + }, + "unix path separator - windows rule - AND allowlist path+stopwords": { + fragment: Fragment{ + Raw: `value: "f4k3s3cr3t"`, + FilePath: `ignoreme/unix.txt`, + }, + rule: config.Rule{ + RuleID: "windows-rule", + Regex: regexp.MustCompile(`value: "[^"]+"`), + Allowlists: []*config.Allowlist{ + { + MatchCondition: config.AllowlistMatchAnd, + Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)}, + StopWords: []string{"f4k3"}, + }, + }, + }, + expected: []report.Finding{ + { + RuleID: "windows-rule", + StartColumn: 1, + EndColumn: 19, + Line: `value: "f4k3s3cr3t"`, + Match: `value: "f4k3s3cr3t"`, + Secret: `value: "f4k3s3cr3t"`, + File: "ignoreme/unix.txt", + Entropy: 3.892407178878784, + }, + }, + }, + + // windows + "windows path separator - unix rule - OR allowlist path-only": { + fragment: Fragment{ + Raw: `value: "s3cr3t"`, + FilePath: `ignoreme/windows.txt`, + WindowsFilePath: `ignoreme\windows.txt`, + }, + rule: config.Rule{ + RuleID: "unix-rule", + Regex: regexp.MustCompile(`s3cr3t`), + Allowlists: []*config.Allowlist{ + { + Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)}, + }, + }, + }, + expected: nil, + }, + "windows path separator - windows rule - OR allowlist path-only": { + fragment: Fragment{ + Raw: `value: "s3cr3t"`, + FilePath: `ignoreme/windows.txt`, + WindowsFilePath: `ignoreme\windows.txt`, + }, + rule: config.Rule{ + RuleID: "windows-rule", + Regex: regexp.MustCompile(`s3cr3t`), + Allowlists: []*config.Allowlist{ + { + Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)}, + }, + }, + }, + expected: nil, + }, + "windows path separator - unix rule - AND allowlist path+stopwords": { + fragment: Fragment{ + Raw: `value: "f4k3s3cr3t"`, + FilePath: `ignoreme/unix.txt`, + WindowsFilePath: `ignoreme\windows.txt`, + }, + rule: config.Rule{ + RuleID: "unix-rule", + Regex: regexp.MustCompile(`value: "[^"]+"`), + Allowlists: []*config.Allowlist{ + { + MatchCondition: config.AllowlistMatchAnd, + Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)}, + StopWords: []string{"f4k3"}, + }, + }, + }, + expected: nil, + }, + "windows path separator - windows rule - AND allowlist path+stopwords": { + fragment: Fragment{ + Raw: `value: "f4k3s3cr3t"`, + FilePath: `ignoreme/unix.txt`, + WindowsFilePath: `ignoreme\windows.txt`, + }, + rule: config.Rule{ + RuleID: "windows-rule", + Regex: regexp.MustCompile(`value: "[^"]+"`), + Allowlists: []*config.Allowlist{ + { + MatchCondition: config.AllowlistMatchAnd, + Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)}, + StopWords: []string{"f4k3"}, + }, + }, + }, + expected: nil, + }, + } + + d, err := NewDetectorDefaultConfig() + require.NoError(t, err) + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := d.detectRule(test.fragment, test.fragment.Raw, test.rule, []*codec.EncodedSegment{}) + compare(t, test.expected, actual) + }) + } +} diff --git a/detect/files.go b/detect/files.go new file mode 100644 index 0000000..ca7557a --- /dev/null +++ b/detect/files.go @@ -0,0 +1,92 @@ +package detect + +import ( + "context" + "errors" + "os" + "sync" + + "github.com/zricethezav/gitleaks/v8/logging" + "github.com/zricethezav/gitleaks/v8/report" + "github.com/zricethezav/gitleaks/v8/sources" +) + +// DetectFiles runs detections against a chanel of scan targets +// +// Deprecated: Use sources.Files and Detector.DetectSource instead +func (d *Detector) DetectFiles(scanTargets <-chan sources.ScanTarget) ([]report.Finding, error) { + var wg sync.WaitGroup + + for scanTarget := range scanTargets { + wg.Add(1) + + d.Sema.Go(func() error { + defer wg.Done() + + logger := logging.With().Str("path", scanTarget.Path).Logger() + logger.Trace().Msg("scanning path") + + f, err := os.Open(scanTarget.Path) + if err != nil { + if os.IsPermission(err) { + err = errors.New("permission denied") + } + + logger.Warn().Err(err).Msg("skipping file") + return nil + } + defer func() { + _ = f.Close() + }() + + info, err := f.Stat() + if err != nil { + logger.Error().Err(err).Msg("skipping file: could not get info") + return nil + } + + // Empty; nothing to do here. + if info.Size() == 0 { + logger.Debug().Msg("skipping empty file") + return nil + } + + // Too large; nothing to do here. + if d.MaxTargetMegaBytes > 0 { + rawLength := info.Size() / 1_000_000 + if rawLength > int64(d.MaxTargetMegaBytes) { + logger.Warn().Msgf( + "skipping file: too large max_size=%dMB, size=%dMB", + d.MaxTargetMegaBytes, rawLength, + ) + return nil + } + } + + // Convert this to a file source + file := sources.File{ + Content: f, + Path: scanTarget.Path, + Symlink: scanTarget.Symlink, + Config: &d.Config, + MaxArchiveDepth: d.MaxArchiveDepth, + } + + ctx := context.Background() + return file.Fragments(ctx, func(fragment sources.Fragment, err error) error { + if err != nil { + logging.Error().Err(err) + return nil + } + + for _, finding := range d.Detect(Fragment(fragment)) { + d.AddFinding(finding) + } + return nil + }) + }) + } + + wg.Wait() + return d.findings, nil +} diff --git a/detect/git.go b/detect/git.go new file mode 100644 index 0000000..ea3a8a7 --- /dev/null +++ b/detect/git.go @@ -0,0 +1,35 @@ +package detect + +import ( + "context" + + "github.com/zricethezav/gitleaks/v8/cmd/scm" + "github.com/zricethezav/gitleaks/v8/report" + "github.com/zricethezav/gitleaks/v8/sources" +) + +// RemoteInfo is an alias for sources.RemoteInfo for backwards compatibility +// +// Deprecated: This will be replaced with sources.RemoteInfo in v9 +type RemoteInfo sources.RemoteInfo + +// DetectGit runs detections against a GitCmd with its remote info +// +// Deprecated: Use sources.Git and detector.DetectSource instead +func (d *Detector) DetectGit(cmd *sources.GitCmd, remote *RemoteInfo) ([]report.Finding, error) { + return d.DetectSource( + context.Background(), + &sources.Git{ + Cmd: cmd, + Config: &d.Config, + Remote: (*sources.RemoteInfo)(remote), + Sema: d.Sema, + MaxArchiveDepth: d.MaxArchiveDepth, + }, + ) +} + +// Deprecated: use sources.NewRemoteInfo instead +func NewRemoteInfo(platform scm.Platform, source string) *RemoteInfo { + return (*RemoteInfo)(sources.NewRemoteInfo(platform, source)) +} diff --git a/detect/location.go b/detect/location.go new file mode 100644 index 0000000..627a490 --- /dev/null +++ b/detect/location.go @@ -0,0 +1,80 @@ +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 location(newlineIndices [][]int, raw string, matchIndex []int) Location { + var ( + prevNewLine int + location Location + lineSet bool + _lineNum int + ) + + start := matchIndex[0] + end := matchIndex[1] + + // default startLineIndex to 0 + location.startLineIndex = 0 + + // Fixes: https://github.com/zricethezav/gitleaks/issues/1037 + // When a fragment does NOT have any newlines, a default "newline" + // will be counted to make the subsequent location calculation logic work + // for fragments will no newlines. + if len(newlineIndices) == 0 { + newlineIndices = [][]int{ + {len(raw), len(raw) + 1}, + } + } + + for lineNum, pair := range newlineIndices { + _lineNum = lineNum + newLineByteIndex := pair[0] + if prevNewLine <= start && start < newLineByteIndex { + lineSet = true + 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] + } + + if !lineSet { + // if lines never get set then that means the secret is most likely + // on the last line of the diff output and the diff output does not have + // a newline + location.startColumn = (start - prevNewLine) + 1 // +1 because counting starts at 1 + location.endColumn = (end - prevNewLine) + location.startLine = _lineNum + 1 + location.endLine = _lineNum + 1 + + // search for new line byte index + i := 0 + for end+i < len(raw) { + if raw[end+i] == '\n' { + break + } + if raw[end+i] == '\r' { + break + } + i++ + } + location.endLineIndex = end + i + } + return location +} diff --git a/detect/location_test.go b/detect/location_test.go new file mode 100644 index 0000000..1bb71d4 --- /dev/null +++ b/detect/location_test.go @@ -0,0 +1,57 @@ +package detect + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// 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 := location(test.linePairs, "", []int{test.start, test.end}) + assert.Equal(t, test.wantLocation, loc) + } +} diff --git a/detect/reader.go b/detect/reader.go new file mode 100644 index 0000000..48b3736 --- /dev/null +++ b/detect/reader.go @@ -0,0 +1,108 @@ +package detect + +import ( + "context" + "io" + + "github.com/zricethezav/gitleaks/v8/report" + "github.com/zricethezav/gitleaks/v8/sources" +) + +// DetectReader accepts an io.Reader and a buffer size for the reader in KB +// +// Deprecated: Use sources.File with no path defined and Detector.DetectSource instead +func (d *Detector) DetectReader(r io.Reader, bufSize int) ([]report.Finding, error) { + var findings []report.Finding + file := sources.File{ + Content: r, + Buffer: make([]byte, 1000*bufSize), + MaxArchiveDepth: d.MaxArchiveDepth, + } + + ctx := context.Background() + err := file.Fragments(ctx, func(fragment sources.Fragment, err error) error { + if err != nil { + return err + } + + for _, finding := range d.Detect(Fragment(fragment)) { + findings = append(findings, finding) + if d.Verbose { + printFinding(finding, d.NoColor) + } + } + + return nil + }) + + return findings, err +} + +// StreamDetectReader streams the detection results from the provided io.Reader. +// It reads data using the specified buffer size (in KB) and processes each chunk through +// the existing detection logic. Findings are sent down the returned findings channel as soon as +// they are detected, while a separate error channel signals a terminal error (or nil upon successful completion). +// The function returns two channels: +// - findingsCh: a receive-only channel that emits report.Finding objects as they are found. +// - errCh: a receive-only channel that emits a single final error (or nil if no error occurred) +// once the stream ends. +// +// Recommended Usage: +// +// Since there will only ever be a single value on the errCh, it is recommended to consume the findingsCh +// first. Once findingsCh is closed, the consumer should then read from errCh to determine +// if the stream completed successfully or if an error occurred. +// +// This design avoids the need for a select loop, keeping client code simple. +// +// Example: +// +// // Assume detector is an instance of *Detector and myReader implements io.Reader. +// findingsCh, errCh := detector.StreamDetectReader(myReader, 64) // using 64 KB buffer size +// +// // Process findings as they arrive. +// for finding := range findingsCh { +// fmt.Printf("Found secret: %+v\n", finding) +// } +// +// // After the findings channel is closed, check the final error. +// if err := <-errCh; err != nil { +// log.Fatalf("StreamDetectReader encountered an error: %v", err) +// } else { +// fmt.Println("Scanning completed successfully.") +// } +// +// Deprecated: Use sources.File.Fragments(context.Context, FragmentsFunc) instead +func (d *Detector) StreamDetectReader(r io.Reader, bufSize int) (<-chan report.Finding, <-chan error) { + findingsCh := make(chan report.Finding, 1) + errCh := make(chan error, 1) + file := sources.File{ + Content: r, + Buffer: make([]byte, 1000*bufSize), + MaxArchiveDepth: d.MaxArchiveDepth, + } + + go func() { + defer close(findingsCh) + defer close(errCh) + + ctx := context.Background() + errCh <- file.Fragments(ctx, func(fragment sources.Fragment, err error) error { + if err != nil { + return err + } + + for _, finding := range d.Detect(Fragment(fragment)) { + findingsCh <- finding + if d.Verbose { + printFinding(finding, d.NoColor) + } + } + + return nil + }) + + }() + + return findingsCh, errCh +} diff --git a/detect/reader_test.go b/detect/reader_test.go new file mode 100644 index 0000000..10902ba --- /dev/null +++ b/detect/reader_test.go @@ -0,0 +1,163 @@ +package detect + +import ( + "bytes" + "errors" + "io" + "strings" + "testing" + "testing/iotest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zricethezav/gitleaks/v8/report" +) + +const secret = "AKIAIRYLJVKMPEGZMPJS" + +type mockReader struct { + data []byte + read bool + + errToReturn error +} + +func (r *mockReader) Read(p []byte) (n int, err error) { + if r.read { + return 0, io.EOF + } + + // Copy data to the provided buffer. + n = copy(p, r.data) + r.read = true + if r.errToReturn != nil { + return n, r.errToReturn + } + + // Return io.EOF along with the bytes. + return n, io.EOF +} + +// TestDetectReader tests the DetectReader function. +func TestDetectReader(t *testing.T) { + tests := []struct { + name string + reader io.Reader + bufSize int + findingsCount int + }{ + { + name: "Test case - Reader returns n > 0 bytes and nil error", + bufSize: 10, + findingsCount: 1, + reader: strings.NewReader(secret), + }, + { + name: "Test case - Reader returns n > 0 bytes and io.EOF error", + bufSize: 10, + findingsCount: 1, + reader: &mockReader{ + data: []byte(secret), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + detector, err := NewDetectorDefaultConfig() + require.NoError(t, err) + + findings, err := detector.DetectReader(test.reader, test.bufSize) + require.NoError(t, err) + + assert.Equal(t, test.findingsCount, len(findings)) + }) + } +} + +func TestStreamDetectReader(t *testing.T) { + tests := []struct { + name string + reader io.Reader + bufSize int + expectedCount int + expectError bool + }{ + { + name: "Single secret streaming", + bufSize: 10, + expectedCount: 1, + reader: strings.NewReader(secret), + expectError: false, + }, + { + name: "Empty reader", + bufSize: 10, + expectedCount: 0, + reader: strings.NewReader(""), + expectError: false, + }, + { + name: "Reader returns error", + bufSize: 10, + expectedCount: 0, + reader: iotest.ErrReader(errors.New("simulated read error")), + expectError: true, + }, + { + name: "Multiple secrets with larger buffer", + bufSize: 20, + expectedCount: 2, + reader: strings.NewReader(secret + "\n" + secret), + expectError: false, + }, + { + name: "Mock reader with EOF", + bufSize: 10, + expectedCount: 1, + reader: &mockReader{data: []byte(secret)}, + expectError: false, + }, + { + name: "Secret split across boundary", + bufSize: 1, // 1KB buffer forces multiple reads + expectedCount: 1, + reader: io.MultiReader( + strings.NewReader(secret[:len(secret)/2]), + strings.NewReader(secret[len(secret)/2:])), + expectError: false, + }, + { + name: "Reader returns error after first read", + bufSize: 1, + expectedCount: 0, + reader: &mockReader{ + data: append(bytes.Repeat([]byte("blah"), 1000), []byte(secret)...), + errToReturn: errors.New("simulated read error"), + }, + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + detector, err := NewDetectorDefaultConfig() + require.NoError(t, err) + + findingsCh, errCh := detector.StreamDetectReader(test.reader, test.bufSize) + var findings []report.Finding + for f := range findingsCh { + findings = append(findings, f) + } + finalErr := <-errCh + + if test.expectError { + require.Error(t, finalErr) + } else { + require.NoError(t, finalErr) + } + + assert.Equal(t, test.expectedCount, len(findings)) + }) + } +} diff --git a/detect/utils.go b/detect/utils.go new file mode 100644 index 0000000..672b47b --- /dev/null +++ b/detect/utils.go @@ -0,0 +1,263 @@ +package detect + +import ( + // "encoding/json" + "fmt" + "math" + "path/filepath" + "strings" + + "github.com/zricethezav/gitleaks/v8/cmd/scm" + "github.com/zricethezav/gitleaks/v8/logging" + "github.com/zricethezav/gitleaks/v8/report" + "github.com/zricethezav/gitleaks/v8/sources" + + "github.com/charmbracelet/lipgloss" +) + +var linkCleaner = strings.NewReplacer( + " ", "%20", + "%", "%25", +) + +func createScmLink(remote *sources.RemoteInfo, finding report.Finding) string { + if remote.Platform == scm.UnknownPlatform || + remote.Platform == scm.NoPlatform || + finding.Commit == "" { + return "" + } + + // Clean the path. + filePath, _, hasInnerPath := strings.Cut(finding.File, sources.InnerPathSeparator) + filePath = linkCleaner.Replace(filePath) + + switch remote.Platform { + case scm.GitHubPlatform: + link := fmt.Sprintf("%s/blob/%s/%s", remote.Url, finding.Commit, filePath) + if hasInnerPath { + return link + } + ext := strings.ToLower(filepath.Ext(filePath)) + if ext == ".ipynb" || ext == ".md" { + link += "?plain=1" + } + if finding.StartLine != 0 { + link += fmt.Sprintf("#L%d", finding.StartLine) + } + if finding.EndLine != finding.StartLine { + link += fmt.Sprintf("-L%d", finding.EndLine) + } + return link + case scm.GitLabPlatform: + link := fmt.Sprintf("%s/blob/%s/%s", remote.Url, finding.Commit, filePath) + if hasInnerPath { + return link + } + if finding.StartLine != 0 { + link += fmt.Sprintf("#L%d", finding.StartLine) + } + if finding.EndLine != finding.StartLine { + link += fmt.Sprintf("-%d", finding.EndLine) + } + return link + case scm.AzureDevOpsPlatform: + link := fmt.Sprintf("%s/commit/%s?path=/%s", remote.Url, finding.Commit, filePath) + // Add line information if applicable + if hasInnerPath { + return link + } + if finding.StartLine != 0 { + link += fmt.Sprintf("&line=%d", finding.StartLine) + } + if finding.EndLine != finding.StartLine { + link += fmt.Sprintf("&lineEnd=%d", finding.EndLine) + } + // This is a bit dirty, but Azure DevOps does not highlight the line when the lineStartColumn and lineEndColumn are not provided + link += "&lineStartColumn=1&lineEndColumn=10000000&type=2&lineStyle=plain&_a=files" + return link + case scm.GiteaPlatform: + link := fmt.Sprintf("%s/src/commit/%s/%s", remote.Url, finding.Commit, filePath) + if hasInnerPath { + return link + } + ext := strings.ToLower(filepath.Ext(filePath)) + if ext == ".ipynb" || ext == ".md" { + link += "?display=source" + } + if finding.StartLine != 0 { + link += fmt.Sprintf("#L%d", finding.StartLine) + } + if finding.EndLine != finding.StartLine { + link += fmt.Sprintf("-L%d", finding.EndLine) + } + return link + case scm.BitbucketPlatform: + link := fmt.Sprintf("%s/src/%s/%s", remote.Url, finding.Commit, filePath) + if hasInnerPath { + return link + } + if finding.StartLine != 0 { + link += fmt.Sprintf("#lines-%d", finding.StartLine) + } + if finding.EndLine != finding.StartLine { + link += fmt.Sprintf(":%d", finding.EndLine) + } + return link + default: + // This should never happen. + return "" + } +} + +// 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 +} + +// filter will dedupe and redact findings +func filter(findings []report.Finding, redact uint) []report.Finding { + var retFindings []report.Finding + for _, f := range findings { + include := true + if strings.Contains(strings.ToLower(f.RuleID), "generic") { + for _, fPrime := range findings { + if f.StartLine == fPrime.StartLine && + f.Commit == fPrime.Commit && + f.RuleID != fPrime.RuleID && + strings.Contains(fPrime.Secret, f.Secret) && + !strings.Contains(strings.ToLower(fPrime.RuleID), "generic") { + + genericMatch := strings.ReplaceAll(f.Match, f.Secret, "REDACTED") + betterMatch := strings.ReplaceAll(fPrime.Match, fPrime.Secret, "REDACTED") + logging.Trace().Msgf("skipping %s finding (%s), %s rule takes precedence (%s)", f.RuleID, genericMatch, fPrime.RuleID, betterMatch) + include = false + break + } + } + } + + if redact > 0 { + f.Redact(redact) + } + if include { + retFindings = append(retFindings, f) + } + } + return retFindings +} + +func printFinding(f report.Finding, noColor bool) { + // trim all whitespace and tabs + f.Line = strings.TrimSpace(f.Line) + f.Secret = strings.TrimSpace(f.Secret) + f.Match = strings.TrimSpace(f.Match) + + isFileMatch := strings.HasPrefix(f.Match, "file detected:") + skipColor := noColor + finding := "" + var secret lipgloss.Style + + // Matches from filenames do not have a |line| or |secret| + if !isFileMatch { + matchInLineIDX := strings.Index(f.Line, f.Match) + secretInMatchIdx := strings.Index(f.Match, f.Secret) + + skipColor = false + + if matchInLineIDX == -1 || noColor { + skipColor = true + matchInLineIDX = 0 + } + + start := f.Line[0:matchInLineIDX] + startMatchIdx := 0 + if matchInLineIDX > 20 { + startMatchIdx = matchInLineIDX - 20 + start = "..." + f.Line[startMatchIdx:matchInLineIDX] + } + + matchBeginning := lipgloss.NewStyle().SetString(f.Match[0:secretInMatchIdx]).Foreground(lipgloss.Color("#f5d445")) + secret = lipgloss.NewStyle().SetString(f.Secret). + Bold(true). + Italic(true). + Foreground(lipgloss.Color("#f05c07")) + matchEnd := lipgloss.NewStyle().SetString(f.Match[secretInMatchIdx+len(f.Secret):]).Foreground(lipgloss.Color("#f5d445")) + + lineEndIdx := matchInLineIDX + len(f.Match) + if len(f.Line)-1 <= lineEndIdx { + lineEndIdx = len(f.Line) + } + + lineEnd := f.Line[lineEndIdx:] + + if len(f.Secret) > 100 { + secret = lipgloss.NewStyle().SetString(f.Secret[0:100] + "..."). + Bold(true). + Italic(true). + Foreground(lipgloss.Color("#f05c07")) + } + if len(lineEnd) > 20 { + lineEnd = lineEnd[0:20] + "..." + } + + finding = fmt.Sprintf("%s%s%s%s%s\n", strings.TrimPrefix(strings.TrimLeft(start, " "), "\n"), matchBeginning, secret, matchEnd, lineEnd) + } + + if skipColor || isFileMatch { + fmt.Printf("%-12s %s\n", "Finding:", f.Match) + fmt.Printf("%-12s %s\n", "Secret:", f.Secret) + } else { + fmt.Printf("%-12s %s", "Finding:", finding) + fmt.Printf("%-12s %s\n", "Secret:", secret) + } + + fmt.Printf("%-12s %s\n", "RuleID:", f.RuleID) + fmt.Printf("%-12s %f\n", "Entropy:", f.Entropy) + + if f.File == "" { + f.PrintRequiredFindings() + fmt.Println("") + return + } + if len(f.Tags) > 0 { + fmt.Printf("%-12s %s\n", "Tags:", f.Tags) + } + fmt.Printf("%-12s %s\n", "File:", f.File) + fmt.Printf("%-12s %d\n", "Line:", f.StartLine) + if f.Commit == "" { + fmt.Printf("%-12s %s\n", "Fingerprint:", f.Fingerprint) + f.PrintRequiredFindings() + fmt.Println("") + return + } + fmt.Printf("%-12s %s\n", "Commit:", f.Commit) + fmt.Printf("%-12s %s\n", "Author:", f.Author) + fmt.Printf("%-12s %s\n", "Email:", f.Email) + fmt.Printf("%-12s %s\n", "Date:", f.Date) + fmt.Printf("%-12s %s\n", "Fingerprint:", f.Fingerprint) + if f.Link != "" { + fmt.Printf("%-12s %s\n", "Link:", f.Link) + } + + f.PrintRequiredFindings() + fmt.Println("") +} diff --git a/detect/utils_test.go b/detect/utils_test.go new file mode 100644 index 0000000..03a6e32 --- /dev/null +++ b/detect/utils_test.go @@ -0,0 +1,214 @@ +package detect + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/zricethezav/gitleaks/v8/cmd/scm" + "github.com/zricethezav/gitleaks/v8/report" + "github.com/zricethezav/gitleaks/v8/sources" +) + +func Test_createScmLink(t *testing.T) { + tests := map[string]struct { + remote *sources.RemoteInfo + finding report.Finding + want string + }{ + // None + "no platform": { + remote: &sources.RemoteInfo{ + Platform: scm.NoPlatform, + Url: "", + }, + want: "", + }, + + // GitHub + "github - single line": { + remote: &sources.RemoteInfo{ + Platform: scm.GitHubPlatform, + Url: "https://github.com/gitleaks/test", + }, + finding: report.Finding{ + Commit: "20553ad96a4a080c94a54d677db97eed8ce2560d", + File: "metrics/% of sales/.env", + StartLine: 25, + EndLine: 25, + }, + want: "https://github.com/gitleaks/test/blob/20553ad96a4a080c94a54d677db97eed8ce2560d/metrics/%25%20of%20sales/.env#L25", + }, + "github - multi line": { + remote: &sources.RemoteInfo{ + Platform: scm.GitHubPlatform, + Url: "https://github.com/gitleaks/test", + }, + finding: report.Finding{ + Commit: "7bad9f7654cf9701b62400281748c0e8efd97666", + File: "config.json", + StartLine: 235, + EndLine: 238, + }, + want: "https://github.com/gitleaks/test/blob/7bad9f7654cf9701b62400281748c0e8efd97666/config.json#L235-L238", + }, + "github - markdown": { + remote: &sources.RemoteInfo{ + Platform: scm.GitHubPlatform, + Url: "https://github.com/gitleaks/test", + }, + finding: report.Finding{ + Commit: "1fc8961d172f39ffb671766e472aa76f8d713e87", + File: "docs/guides/ecosystem/discordjs.MD", + StartLine: 34, + EndLine: 34, + }, + want: "https://github.com/gitleaks/test/blob/1fc8961d172f39ffb671766e472aa76f8d713e87/docs/guides/ecosystem/discordjs.MD?plain=1#L34", + }, + "github - jupyter notebook": { + remote: &sources.RemoteInfo{ + Platform: scm.GitHubPlatform, + Url: "https://github.com/gitleaks/test", + }, + finding: report.Finding{ + Commit: "8f56bd2369595bcadbb007e88ba294630fb05c7b", + File: "Cloud/IPYNB/Overlapping Recommendation algorithm _OCuLaR_.ipynb", + StartLine: 293, + EndLine: 293, + }, + want: "https://github.com/gitleaks/test/blob/8f56bd2369595bcadbb007e88ba294630fb05c7b/Cloud/IPYNB/Overlapping%20Recommendation%20algorithm%20_OCuLaR_.ipynb?plain=1#L293", + }, + + // GitLab + "gitlab - single line": { + remote: &sources.RemoteInfo{ + Platform: scm.GitLabPlatform, + Url: "https://gitlab.com/example-org/example-group/gitleaks", + }, + finding: report.Finding{ + Commit: "213ffd1c9bfa906eb4c7731771132c58a4ca0139", + File: ".gitlab-ci.yml", + StartLine: 41, + EndLine: 41, + }, + want: "https://gitlab.com/example-org/example-group/gitleaks/blob/213ffd1c9bfa906eb4c7731771132c58a4ca0139/.gitlab-ci.yml#L41", + }, + "gitlab - multi line": { + remote: &sources.RemoteInfo{ + Platform: scm.GitLabPlatform, + Url: "https://gitlab.com/example-org/example-group/gitleaks", + }, + finding: report.Finding{ + Commit: "63410f74e23a4e51e1f60b9feb073b5d325af878", + File: ".vscode/launchSettings.json", + StartLine: 6, + EndLine: 8, + }, + want: "https://gitlab.com/example-org/example-group/gitleaks/blob/63410f74e23a4e51e1f60b9feb073b5d325af878/.vscode/launchSettings.json#L6-8", + }, + + // Azure DevOps + "azuredevops - single line": { + remote: &sources.RemoteInfo{ + Platform: scm.AzureDevOpsPlatform, + Url: "https://dev.azure.com/exampleorganisation/exampleproject/_git/exampleRepository", + }, + finding: report.Finding{ + Commit: "20553ad96a4a080c94a54d677db97eed8ce2560d", + File: "examplefile.json", + StartLine: 25, + EndLine: 25, + }, + want: "https://dev.azure.com/exampleorganisation/exampleproject/_git/exampleRepository/commit/20553ad96a4a080c94a54d677db97eed8ce2560d?path=/examplefile.json&line=25&lineStartColumn=1&lineEndColumn=10000000&type=2&lineStyle=plain&_a=files", + }, + + // Azure DevOps + "azuredevops - multi line": { + remote: &sources.RemoteInfo{ + Platform: scm.AzureDevOpsPlatform, + Url: "https://dev.azure.com/exampleorganisation/exampleproject/_git/exampleRepository", + }, + finding: report.Finding{ + Commit: "20553ad96a4a080c94a54d677db97eed8ce2560d", + File: "examplefile.json", + StartLine: 25, + EndLine: 30, + }, + want: "https://dev.azure.com/exampleorganisation/exampleproject/_git/exampleRepository/commit/20553ad96a4a080c94a54d677db97eed8ce2560d?path=/examplefile.json&line=25&lineEnd=30&lineStartColumn=1&lineEndColumn=10000000&type=2&lineStyle=plain&_a=files", + }, + + // Gitea + "gitea - single line": { + remote: &sources.RemoteInfo{ + Platform: scm.GiteaPlatform, + Url: "https://gitea.com/exampleorganisation/exampleproject", + }, + finding: report.Finding{ + Commit: "20553ad96a4a080c94a54d677db97eed8ce2560d", + File: "examplefile.json", + StartLine: 25, + EndLine: 25, + }, + want: "https://gitea.com/exampleorganisation/exampleproject/src/commit/20553ad96a4a080c94a54d677db97eed8ce2560d/examplefile.json#L25", + }, + "gitea- multi line": { + remote: &sources.RemoteInfo{ + Platform: scm.GiteaPlatform, + Url: "https://gitea.com/exampleorganisation/exampleproject", + }, + finding: report.Finding{ + Commit: "20553ad96a4a080c94a54d677db97eed8ce2560d", + File: "examplefile.json", + StartLine: 25, + EndLine: 30, + }, + want: "https://gitea.com/exampleorganisation/exampleproject/src/commit/20553ad96a4a080c94a54d677db97eed8ce2560d/examplefile.json#L25-L30", + }, + "gitea - markdown": { + remote: &sources.RemoteInfo{ + Platform: scm.GiteaPlatform, + Url: "https://gitea.com/exampleorganisation/exampleproject", + }, + finding: report.Finding{ + Commit: "20553ad96a4a080c94a54d677db97eed8ce2560d", + File: "Readme.md", + StartLine: 34, + EndLine: 34, + }, + want: "https://gitea.com/exampleorganisation/exampleproject/src/commit/20553ad96a4a080c94a54d677db97eed8ce2560d/Readme.md?display=source#L34", + }, + // bitbucket + "bitbucket - single line": { + remote: &sources.RemoteInfo{ + Platform: scm.BitbucketPlatform, + Url: "https://bitbucket.org/exampleorganisation/exampleproject", + }, + finding: report.Finding{ + Commit: "20553ad96a4a080c94a54d677db97eed8ce2560d", + File: "examplefile.json", + StartLine: 25, + EndLine: 25, + }, + want: "https://bitbucket.org/exampleorganisation/exampleproject/src/20553ad96a4a080c94a54d677db97eed8ce2560d/examplefile.json#lines-25", + }, + "bitbucket- multi line": { + remote: &sources.RemoteInfo{ + Platform: scm.BitbucketPlatform, + Url: "https://bitbucket.org/exampleorganisation/exampleproject", + }, + finding: report.Finding{ + Commit: "20553ad96a4a080c94a54d677db97eed8ce2560d", + File: "examplefile.json", + StartLine: 25, + EndLine: 30, + }, + want: "https://bitbucket.org/exampleorganisation/exampleproject/src/20553ad96a4a080c94a54d677db97eed8ce2560d/examplefile.json#lines-25:30", + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + actual := createScmLink(tt.remote, tt.finding) + assert.Equal(t, tt.want, actual) + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3a16143 --- /dev/null +++ b/go.mod @@ -0,0 +1,84 @@ +module github.com/zricethezav/gitleaks/v8 + +go 1.24.11 + +require ( + github.com/BobuSumisu/aho-corasick v1.0.3 + github.com/Masterminds/sprig/v3 v3.3.0 + github.com/charmbracelet/lipgloss v0.5.0 + github.com/fatih/semgroup v1.2.0 + github.com/gitleaks/go-gitdiff v0.9.1 + github.com/google/go-cmp v0.7.0 + github.com/h2non/filetype v1.1.3 + github.com/hashicorp/go-version v1.7.0 + github.com/mholt/archives v0.1.5 + github.com/rs/zerolog v1.33.0 + github.com/spf13/cobra v1.9.1 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.10.0 + golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/STARRY-S/zip v0.2.3 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/bodgit/plumbing v1.3.0 // indirect + github.com/bodgit/sevenzip v1.6.1 // indirect + github.com/bodgit/windows v1.0.1 // indirect + github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mikelolasagasti/xz v1.0.1 // indirect + github.com/minio/minlz v1.0.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 // indirect + github.com/muesli/termenv v0.15.1 // indirect + github.com/nwaples/rardecode/v2 v2.2.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sorairolake/lzip-go v0.3.8 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect + github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect + go.uber.org/multierr v1.11.0 // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect + golang.org/x/crypto v0.35.0 // indirect +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb + github.com/magiconair/properties v1.8.9 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/wasilibs/go-re2 v1.9.0 + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.29.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..30e1833 --- /dev/null +++ b/go.sum @@ -0,0 +1,423 @@ +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.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +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/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +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/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8a+4nPE9g= +github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= +github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= +github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= +github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4= +github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= +github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= +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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/semgroup v1.2.0 h1:h/OLXwEM+3NNyAdZEpMiH1OzfplU09i2qXPVThGZvyg= +github.com/fatih/semgroup v1.2.0/go.mod h1:1KAD4iIYfXjE4U13B48VM4z9QUwV5Tt8O4rS879kgm8= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gitleaks/go-gitdiff v0.9.1 h1:ni6z6/3i9ODT685OLCTf+s/ERlWUNWQF4x1pvoNICw0= +github.com/gitleaks/go-gitdiff v0.9.1/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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +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/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/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.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.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +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-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/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/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= +github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +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/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +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/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb h1:w1g9wNDIE/pHSTmAaUhv4TZQuPBS6GV3mMz5hkgziIU= +github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb/go.mod h1:5ELEyG+X8f+meRWHuqUOewBOhvHkl7M76pdGEansxW4= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ= +github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4= +github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= +github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= +github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= +github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= +github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A= +github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik= +github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/wasilibs/go-re2 v1.9.0 h1:kjAd8qbNvV4Ve2Uf+zrpTCrDHtqH4dlsRXktywo73JQ= +github.com/wasilibs/go-re2 v1.9.0/go.mod h1:0sRtscWgpUdNA137bmr1IUgrRX0Su4dcn9AEe61y+yI= +github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4= +github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +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.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= +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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +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-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= +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/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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +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-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-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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +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/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-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-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-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +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/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-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-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-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-20200130002326-2f3ba24bd6e7/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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +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= +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/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/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-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +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.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= +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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/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= +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= diff --git a/logging/log.go b/logging/log.go new file mode 100644 index 0000000..bce7ddf --- /dev/null +++ b/logging/log.go @@ -0,0 +1,50 @@ +package logging + +import ( + "os" + + "github.com/rs/zerolog" +) + +var Logger zerolog.Logger + +func init() { + // send all logs to stdout + Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}). + Level(zerolog.InfoLevel). + With().Timestamp().Logger() +} + +func With() zerolog.Context { + return Logger.With() +} + +func Trace() *zerolog.Event { + return Logger.Trace() +} + +func Debug() *zerolog.Event { + return Logger.Debug() +} +func Info() *zerolog.Event { + return Logger.Info() +} +func Warn() *zerolog.Event { + return Logger.Warn() +} + +func Error() *zerolog.Event { + return Logger.Error() +} + +func Err(err error) *zerolog.Event { + return Logger.Err(err) +} + +func Fatal() *zerolog.Event { + return Logger.Fatal() +} + +func Panic() *zerolog.Event { + return Logger.Panic() +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..af1e9b8 --- /dev/null +++ b/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "os" + "os/signal" + + "github.com/zricethezav/gitleaks/v8/cmd" + "github.com/zricethezav/gitleaks/v8/logging" +) + +func main() { + // this block sets up a go routine to listen for an interrupt signal + // which will immediately exit gitleaks + stopChan := make(chan os.Signal, 1) + signal.Notify(stopChan, os.Interrupt) + go listenForInterrupt(stopChan) + + cmd.Execute() +} + +func listenForInterrupt(stopScan chan os.Signal) { + <-stopScan + logging.Fatal().Msg("Interrupt signal received. Exiting...") +} diff --git a/regexp/stdlib_regex.go b/regexp/stdlib_regex.go new file mode 100644 index 0000000..3a6d0ab --- /dev/null +++ b/regexp/stdlib_regex.go @@ -0,0 +1,15 @@ +//go:build !gore2regex + +package regexp + +import ( + re "regexp" +) + +const Version = "stdlib" + +type Regexp = re.Regexp + +func MustCompile(str string) *re.Regexp { + return re.MustCompile(str) +} diff --git a/regexp/wasilibs_regex.go b/regexp/wasilibs_regex.go new file mode 100644 index 0000000..f3ad702 --- /dev/null +++ b/regexp/wasilibs_regex.go @@ -0,0 +1,15 @@ +//go:build gore2regex + +package regexp + +import ( + re "github.com/wasilibs/go-re2" +) + +const Version = "github.com/wasilibs/go-re2" + +type Regexp = re.Regexp + +func MustCompile(str string) *re.Regexp { + return re.MustCompile(str) +} diff --git a/report/constants.go b/report/constants.go new file mode 100644 index 0000000..7772950 --- /dev/null +++ b/report/constants.go @@ -0,0 +1,4 @@ +package report + +const version = "v8.0.0" +const driver = "gitleaks" diff --git a/report/csv.go b/report/csv.go new file mode 100644 index 0000000..38c824a --- /dev/null +++ b/report/csv.go @@ -0,0 +1,78 @@ +package report + +import ( + "encoding/csv" + "io" + "strconv" + "strings" +) + +type CsvReporter struct { +} + +var _ Reporter = (*CsvReporter)(nil) + +func (r *CsvReporter) Write(w io.WriteCloser, findings []Finding) error { + if len(findings) == 0 { + return nil + } + + var ( + cw = csv.NewWriter(w) + err error + ) + columns := []string{"RuleID", + "Commit", + "File", + "SymlinkFile", + "Secret", + "Match", + "StartLine", + "EndLine", + "StartColumn", + "EndColumn", + "Author", + "Message", + "Date", + "Email", + "Fingerprint", + "Tags", + } + // A miserable attempt at "omitempty" so tests don't yell at me. + if findings[0].Link != "" { + columns = append(columns, "Link") + } + + if err = cw.Write(columns); err != nil { + return err + } + for _, f := range findings { + row := []string{f.RuleID, + f.Commit, + f.File, + f.SymlinkFile, + f.Secret, + f.Match, + strconv.Itoa(f.StartLine), + strconv.Itoa(f.EndLine), + strconv.Itoa(f.StartColumn), + strconv.Itoa(f.EndColumn), + f.Author, + f.Message, + f.Date, + f.Email, + f.Fingerprint, + strings.Join(f.Tags, " "), + } + if findings[0].Link != "" { + row = append(row, f.Link) + } + + if err = cw.Write(row); err != nil { + return err + } + } + + cw.Flush() + return cw.Error() +} diff --git a/report/csv_test.go b/report/csv_test.go new file mode 100644 index 0000000..2a7c3b3 --- /dev/null +++ b/report/csv_test.go @@ -0,0 +1,77 @@ +package report + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +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", + Match: "line containing secret", + Secret: "a secret", + StartLine: 1, + EndLine: 2, + StartColumn: 1, + EndColumn: 2, + Message: "opps", + File: "auth.py", + SymlinkFile: "", + Commit: "0000000000000000", + Author: "John Doe", + Email: "johndoe@gmail.com", + Date: "10-19-2003", + Fingerprint: "fingerprint", + Tags: []string{"tag1", "tag2", "tag3"}, + }, + }}, + { + + wantEmpty: true, + testReportName: "empty", + expected: filepath.Join(expectPath, "report", "this_should_not_exist.csv"), + findings: []Finding{}, + }, + } + + reporter := CsvReporter{} + for _, test := range tests { + t.Run(test.testReportName, func(t *testing.T) { + tmpfile, err := os.Create(filepath.Join(t.TempDir(), test.testReportName+".csv")) + require.NoError(t, err) + defer tmpfile.Close() + + err = reporter.Write(tmpfile, test.findings) + require.NoError(t, err) + assert.FileExists(t, tmpfile.Name()) + + got, err := os.ReadFile(tmpfile.Name()) + require.NoError(t, err) + if test.wantEmpty { + assert.Empty(t, got) + return + } + + want, err := os.ReadFile(test.expected) + require.NoError(t, err) + + wantStr := lineEndingReplacer.Replace(string(want)) + gotStr := lineEndingReplacer.Replace(string(got)) + assert.Equal(t, wantStr, gotStr) + }) + } +} diff --git a/report/finding.go b/report/finding.go new file mode 100644 index 0000000..55ad911 --- /dev/null +++ b/report/finding.go @@ -0,0 +1,126 @@ +package report + +import ( + "fmt" + "math" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/zricethezav/gitleaks/v8/sources" +) + +// Finding contains a whole bunch of information about a secret finding. +// Plenty of real estate in this bad boy so fillerup as needed. +type Finding struct { + // Rule is the name of the rule that was matched + RuleID string + Description string + + StartLine int + EndLine int + StartColumn int + EndColumn int + + Line string `json:"-"` + + Match string + + // Captured secret + Secret string + + // File is the name of the file containing the finding + File string + SymlinkFile string + Commit string + Link string `json:",omitempty"` + + // Entropy is the shannon entropy of Value + Entropy float32 + + Author string + Email string + Date string + Message string + Tags []string + + // unique identifier + Fingerprint string + + // Fragment used for multi-part rule checking, CEL filtering, + // and eventually ML validation + Fragment *sources.Fragment `json:",omitempty"` + + // TODO keeping private for now to during experimental phase + requiredFindings []*RequiredFinding +} + +type RequiredFinding struct { + // contains a subset of the Finding fields + // only used for reporting + RuleID string + StartLine int + EndLine int + StartColumn int + EndColumn int + Line string `json:"-"` + Match string + Secret string +} + +func (f *Finding) AddRequiredFindings(afs []*RequiredFinding) { + if f.requiredFindings == nil { + f.requiredFindings = make([]*RequiredFinding, 0) + } + f.requiredFindings = append(f.requiredFindings, afs...) +} + +// Redact removes sensitive information from a finding. +func (f *Finding) Redact(percent uint) { + secret := maskSecret(f.Secret, percent) + if percent >= 100 { + secret = "REDACTED" + } + f.Line = strings.ReplaceAll(f.Line, f.Secret, secret) + f.Match = strings.ReplaceAll(f.Match, f.Secret, secret) + f.Secret = secret +} + +func maskSecret(secret string, percent uint) string { + if percent > 100 { + percent = 100 + } + len := float64(len(secret)) + if len <= 0 { + return secret + } + prc := float64(100 - percent) + lth := int64(math.RoundToEven(len * prc / float64(100))) + + return secret[:lth] + "..." +} + +func (f *Finding) PrintRequiredFindings() { + if len(f.requiredFindings) == 0 { + return + } + + fmt.Printf("%-12s ", "Required:") + + // Create orange style for secrets + orangeStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#bf9478")) + + for i, aux := range f.requiredFindings { + auxSecret := strings.TrimSpace(aux.Secret) + // Truncate long secrets for readability + if len(auxSecret) > 40 { + auxSecret = auxSecret[:37] + "..." + } + + // Format: rule-id:line:secret + if i == 0 { + fmt.Printf("%s:%d:%s\n", aux.RuleID, aux.StartLine, orangeStyle.Render(auxSecret)) + } else { + fmt.Printf("%-12s %s:%d:%s\n", "", aux.RuleID, aux.StartLine, orangeStyle.Render(auxSecret)) + } + } +} diff --git a/report/finding_test.go b/report/finding_test.go new file mode 100644 index 0000000..07a36bf --- /dev/null +++ b/report/finding_test.go @@ -0,0 +1,84 @@ +package report + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRedact(t *testing.T) { + tests := []struct { + findings []Finding + redact bool + }{ + { + redact: true, + findings: []Finding{ + { + Match: "line containing secret", + Secret: "secret", + }, + }}, + } + for _, test := range tests { + for _, f := range test.findings { + f.Redact(100) + assert.Equal(t, "REDACTED", f.Secret) + assert.Equal(t, "line containing REDACTED", f.Match) + } + } +} + +func TestMask(t *testing.T) { + + tests := map[string]struct { + finding Finding + percent uint + expect Finding + }{ + "normal secret": { + finding: Finding{Match: "line containing secret", Secret: "secret"}, + expect: Finding{Match: "line containing se...", Secret: "se..."}, + percent: 75, + }, + "empty secret": { + finding: Finding{Match: "line containing", Secret: ""}, + expect: Finding{Match: "line containing", Secret: ""}, + percent: 75, + }, + "short secret": { + finding: Finding{Match: "line containing", Secret: "ss"}, + expect: Finding{Match: "line containing", Secret: "..."}, + percent: 75, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + f := test.finding + e := test.expect + f.Redact(test.percent) + assert.Equal(t, e.Secret, f.Secret) + assert.Equal(t, e.Match, f.Match) + }) + } +} + +func TestMaskSecret(t *testing.T) { + + tests := map[string]struct { + secret string + percent uint + expect string + }{ + "normal masking": {secret: "secret", percent: 75, expect: "se..."}, + "high masking": {secret: "secret", percent: 90, expect: "s..."}, + "low masking": {secret: "secret", percent: 10, expect: "secre..."}, + "invalid masking": {secret: "secret", percent: 1000, expect: "..."}, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := maskSecret(test.secret, test.percent) + assert.Equal(t, test.expect, got) + }) + } +} diff --git a/report/json.go b/report/json.go new file mode 100644 index 0000000..867c98a --- /dev/null +++ b/report/json.go @@ -0,0 +1,17 @@ +package report + +import ( + "encoding/json" + "io" +) + +type JsonReporter struct { +} + +var _ Reporter = (*JsonReporter)(nil) + +func (t *JsonReporter) Write(w io.WriteCloser, findings []Finding) error { + encoder := json.NewEncoder(w) + encoder.SetIndent("", " ") + return encoder.Encode(findings) +} diff --git a/report/json_test.go b/report/json_test.go new file mode 100644 index 0000000..9c45905 --- /dev/null +++ b/report/json_test.go @@ -0,0 +1,77 @@ +package report + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var simpleFinding = Finding{ + Description: "", + RuleID: "test-rule", + Match: "line containing secret", + Secret: "a secret", + StartLine: 1, + EndLine: 2, + StartColumn: 1, + EndColumn: 2, + Message: "opps", + File: "auth.py", + SymlinkFile: "", + Commit: "0000000000000000", + Author: "John Doe", + Email: "johndoe@gmail.com", + Date: "10-19-2003", + Tags: []string{}, +} + +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{ + simpleFinding, + }}, + { + + testReportName: "empty", + expected: filepath.Join(expectPath, "report", "empty.json"), + findings: []Finding{}}, + } + + reporter := JsonReporter{} + for _, test := range tests { + t.Run(test.testReportName, func(t *testing.T) { + tmpfile, err := os.Create(filepath.Join(t.TempDir(), test.testReportName+".json")) + require.NoError(t, err) + defer tmpfile.Close() + + err = reporter.Write(tmpfile, test.findings) + require.NoError(t, err) + assert.FileExists(t, tmpfile.Name()) + + got, err := os.ReadFile(tmpfile.Name()) + require.NoError(t, err) + if test.wantEmpty { + assert.Empty(t, got) + return + } + + want, err := os.ReadFile(test.expected) + require.NoError(t, err) + + wantStr := lineEndingReplacer.Replace(string(want)) + gotStr := lineEndingReplacer.Replace(string(got)) + assert.Equal(t, wantStr, gotStr) + }) + } +} diff --git a/report/junit.go b/report/junit.go new file mode 100644 index 0000000..e9ac57b --- /dev/null +++ b/report/junit.go @@ -0,0 +1,107 @@ +package report + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "io" + "strconv" +) + +type JunitReporter struct { +} + +var _ Reporter = (*JunitReporter)(nil) + +func (r *JunitReporter) Write(w io.WriteCloser, findings []Finding) error { + testSuites := TestSuites{ + TestSuites: getTestSuites(findings), + } + + io.WriteString(w, xml.Header) + encoder := xml.NewEncoder(w) + encoder.Indent("", "\t") + return encoder.Encode(testSuites) +} + +func getTestSuites(findings []Finding) []TestSuite { + return []TestSuite{ + { + Failures: strconv.Itoa(len(findings)), + Name: "gitleaks", + Tests: strconv.Itoa(len(findings)), + TestCases: getTestCases(findings), + Time: "", + }, + } +} + +func getTestCases(findings []Finding) []TestCase { + testCases := []TestCase{} + for _, f := range findings { + testCase := TestCase{ + Classname: f.Description, + Failure: getFailure(f), + File: f.File, + Name: getMessage(f), + Time: "", + } + testCases = append(testCases, testCase) + } + return testCases +} + +func getFailure(f Finding) Failure { + return Failure{ + Data: getData(f), + Message: getMessage(f), + Type: f.Description, + } +} + +func getData(f Finding) string { + data, err := json.MarshalIndent(f, "", "\t") + if err != nil { + fmt.Println(err) + return "" + } + return string(data) +} + +func getMessage(f Finding) string { + if f.Commit == "" { + return fmt.Sprintf("%s has detected a secret in file %s, line %s.", f.RuleID, f.File, strconv.Itoa(f.StartLine)) + } + + return fmt.Sprintf("%s has detected a secret in file %s, line %s, at commit %s.", f.RuleID, f.File, strconv.Itoa(f.StartLine), f.Commit) +} + +type TestSuites struct { + XMLName xml.Name `xml:"testsuites"` + TestSuites []TestSuite +} + +type TestSuite struct { + XMLName xml.Name `xml:"testsuite"` + Failures string `xml:"failures,attr"` + Name string `xml:"name,attr"` + Tests string `xml:"tests,attr"` + TestCases []TestCase `xml:"testcase"` + Time string `xml:"time,attr"` +} + +type TestCase struct { + XMLName xml.Name `xml:"testcase"` + Classname string `xml:"classname,attr"` + Failure Failure `xml:"failure"` + File string `xml:"file,attr"` + Name string `xml:"name,attr"` + Time string `xml:"time,attr"` +} + +type Failure struct { + XMLName xml.Name `xml:"failure"` + Data string `xml:",chardata"` + Message string `xml:"message,attr"` + Type string `xml:"type,attr"` +} diff --git a/report/junit_test.go b/report/junit_test.go new file mode 100644 index 0000000..44555a5 --- /dev/null +++ b/report/junit_test.go @@ -0,0 +1,92 @@ +package report + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWriteJunit(t *testing.T) { + tests := []struct { + findings []Finding + testReportName string + expected string + wantEmpty bool + }{ + { + testReportName: "simple", + expected: filepath.Join(expectPath, "report", "junit_simple.xml"), + findings: []Finding{ + { + + Description: "Test Rule", + RuleID: "test-rule", + Match: "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{}, + }, + { + + Description: "Test Rule", + RuleID: "test-rule", + Match: "line containing secret", + Secret: "a secret", + StartLine: 2, + EndLine: 3, + StartColumn: 1, + EndColumn: 2, + Message: "", + File: "auth.py", + Commit: "", + Author: "", + Email: "", + Date: "", + Tags: []string{}, + }, + }, + }, + { + testReportName: "empty", + expected: filepath.Join(expectPath, "report", "junit_empty.xml"), + findings: []Finding{}, + }, + } + + reporter := JunitReporter{} + for _, test := range tests { + tmpfile, err := os.Create(filepath.Join(t.TempDir(), test.testReportName+".xml")) + require.NoError(t, err) + defer tmpfile.Close() + + err = reporter.Write(tmpfile, test.findings) + require.NoError(t, err) + assert.FileExists(t, tmpfile.Name()) + + got, err := os.ReadFile(tmpfile.Name()) + require.NoError(t, err) + if test.wantEmpty { + assert.Empty(t, got) + return + } + + want, err := os.ReadFile(test.expected) + require.NoError(t, err) + + wantStr := lineEndingReplacer.Replace(string(want)) + gotStr := lineEndingReplacer.Replace(string(got)) + assert.Equal(t, wantStr, gotStr) + } +} diff --git a/report/report.go b/report/report.go new file mode 100644 index 0000000..db6cd22 --- /dev/null +++ b/report/report.go @@ -0,0 +1,16 @@ +package report + +import ( + "io" +) + +const ( + // https://cwe.mitre.org/data/definitions/798.html + CWE = "CWE-798" + CWE_DESCRIPTION = "Use of Hard-coded Credentials" + StdoutReportPath = "-" +) + +type Reporter interface { + Write(w io.WriteCloser, findings []Finding) error +} diff --git a/report/report_test.go b/report/report_test.go new file mode 100644 index 0000000..3537dec --- /dev/null +++ b/report/report_test.go @@ -0,0 +1,49 @@ +package report + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const expectPath = "../testdata/expected/" +const templatePath = "../testdata/report/" + +func TestWriteStdout(t *testing.T) { + // Arrange + reporter := JsonReporter{} + buf := testWriter{ + bytes.NewBuffer(nil), + } + findings := []Finding{ + { + RuleID: "test-rule", + }, + } + + // Act + err := reporter.Write(buf, findings) + require.NoError(t, err) + got := buf.Bytes() + + // Assert + assert.NotEmpty(t, got) +} + +type testWriter struct { + *bytes.Buffer +} + +func (t testWriter) Close() error { + return nil +} + +// lineEndingReplacer normalizes CRLF to LF so tests pass on Windows. +var lineEndingReplacer = strings.NewReplacer( + "\\r\\n", "\\n", + "\r", "", + "\\r", "", +) diff --git a/report/sarif.go b/report/sarif.go new file mode 100644 index 0000000..e86cd7e --- /dev/null +++ b/report/sarif.go @@ -0,0 +1,217 @@ +package report + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/zricethezav/gitleaks/v8/config" +) + +type SarifReporter struct { + OrderedRules []config.Rule +} + +var _ Reporter = (*SarifReporter)(nil) + +func (r *SarifReporter) Write(w io.WriteCloser, findings []Finding) error { + sarif := Sarif{ + Schema: "https://json.schemastore.org/sarif-2.1.0.json", + Version: "2.1.0", + Runs: r.getRuns(findings), + } + + encoder := json.NewEncoder(w) + encoder.SetIndent("", " ") + return encoder.Encode(sarif) +} + +func (r *SarifReporter) getRuns(findings []Finding) []Runs { + return []Runs{ + { + Tool: r.getTool(), + Results: getResults(findings), + }, + } +} + +func (r *SarifReporter) getTool() Tool { + tool := Tool{ + Driver: Driver{ + Name: driver, + SemanticVersion: version, + InformationUri: "https://github.com/gitleaks/gitleaks", + Rules: r.getRules(), + }, + } + + // if this tool has no rules, ensure that it is represented as [] instead of null/nil + if hasEmptyRules(tool) { + tool.Driver.Rules = make([]Rules, 0) + } + + return tool +} + +func hasEmptyRules(tool Tool) bool { + return len(tool.Driver.Rules) == 0 +} + +func (r *SarifReporter) getRules() []Rules { + // TODO for _, rule := range cfg.Rules { + var rules []Rules + for _, rule := range r.OrderedRules { + rules = append(rules, Rules{ + ID: rule.RuleID, + Description: ShortDescription{ + Text: rule.Description, + }, + }) + } + 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 { + 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, + }, + Properties: Properties{ + Tags: f.Tags, + }, + } + results = append(results, r) + } + return results +} + +func getLocation(f Finding) []Locations { + uri := f.File + if f.SymlinkFile != "" { + uri = f.SymlinkFile + } + return []Locations{ + { + PhysicalLocation: PhysicalLocation{ + ArtifactLocation: ArtifactLocation{ + URI: uri, + }, + 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"` + Description ShortDescription `json:"shortDescription"` +} + +type Driver struct { + Name string `json:"name"` + SemanticVersion string `json:"semanticVersion"` + InformationUri string `json:"informationUri"` + 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 Properties struct { + Tags []string `json:"tags"` +} + +type Results struct { + Message Message `json:"message"` + RuleId string `json:"ruleId"` + Locations []Locations `json:"locations"` + PartialFingerPrints `json:"partialFingerprints"` + Properties Properties `json:"properties"` +} + +type Runs struct { + Tool Tool `json:"tool"` + Results []Results `json:"results"` +} diff --git a/report/sarif_test.go b/report/sarif_test.go new file mode 100644 index 0000000..7c8f458 --- /dev/null +++ b/report/sarif_test.go @@ -0,0 +1,86 @@ +package report + +import ( + "os" + "path/filepath" + "testing" + + "github.com/zricethezav/gitleaks/v8/config" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +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{ + { + + RuleID: "test-rule", + Description: "A test rule", + Match: "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{"tag1", "tag2", "tag3"}, + }, + }}, + } + + for _, test := range tests { + t.Run(test.cfgName, func(t *testing.T) { + tmpfile, err := os.Create(filepath.Join(t.TempDir(), test.testReportName+".json")) + require.NoError(t, err) + defer tmpfile.Close() + + reporter := SarifReporter{ + OrderedRules: []config.Rule{ + { + RuleID: "aws-access-key", + Description: "AWS Access Key", + }, + { + RuleID: "pypi", + Description: "PyPI upload token", + }, + }, + } + err = reporter.Write(tmpfile, test.findings) + require.NoError(t, err) + assert.FileExists(t, tmpfile.Name()) + + got, err := os.ReadFile(tmpfile.Name()) + require.NoError(t, err) + + if test.wantEmpty { + assert.Empty(t, got) + return + } + + want, err := os.ReadFile(test.expected) + require.NoError(t, err) + + wantStr := lineEndingReplacer.Replace(string(want)) + gotStr := lineEndingReplacer.Replace(string(got)) + assert.Equal(t, wantStr, gotStr) + }) + } +} diff --git a/report/template.go b/report/template.go new file mode 100644 index 0000000..e7b1819 --- /dev/null +++ b/report/template.go @@ -0,0 +1,53 @@ +package report + +import ( + "errors" + "fmt" + "io" + "os" + "text/template" + + "github.com/Masterminds/sprig/v3" +) + +type TemplateReporter struct { + template *template.Template +} + +var _ Reporter = (*TemplateReporter)(nil) + +func NewTemplateReporter(templatePath string) (*TemplateReporter, error) { + if templatePath == "" { + return nil, errors.New("template path cannot be empty") + } + + file, err := os.ReadFile(templatePath) + if err != nil { + return nil, fmt.Errorf("error reading file: %w", err) + } + templateText := string(file) + + // TODO: Add helper functions like escaping for JSON, XML, etc. + t := template.New("custom") + + funcMap := sprig.TxtFuncMap() + delete(funcMap, "env") + delete(funcMap, "expandenv") + delete(funcMap, "getHostByName") + + t = t.Funcs(funcMap) + t, err = t.Parse(templateText) + if err != nil { + return nil, fmt.Errorf("error parsing file: %w", err) + } + return &TemplateReporter{template: t}, nil +} + +// writeTemplate renders the findings using the user-provided template. +// https://www.digitalocean.com/community/tutorials/how-to-use-templates-in-go +func (t *TemplateReporter) Write(w io.WriteCloser, findings []Finding) error { + if err := t.template.Execute(w, findings); err != nil { + return err + } + return nil +} diff --git a/report/template_test.go b/report/template_test.go new file mode 100644 index 0000000..b89f4d1 --- /dev/null +++ b/report/template_test.go @@ -0,0 +1,147 @@ +package report + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWriteTemplate(t *testing.T) { + tests := []struct { + findings []Finding + testReportName string + expected string + wantEmpty bool + }{ + { + testReportName: "markdown", + expected: filepath.Join(expectPath, "report", "template_markdown.md"), + findings: []Finding{ + { + + RuleID: "test-rule", + Description: "A test rule", + Match: "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{"tag1", "tag2", "tag3"}, + }, + }, + }, + { + testReportName: "jsonextra", + expected: filepath.Join(expectPath, "report", "template_jsonextra.json"), + findings: []Finding{ + { + + RuleID: "test-rule", + Description: "A test rule", + Line: "whole line containing secret", + Match: "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{"tag1", "tag2", "tag3"}, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.testReportName, func(t *testing.T) { + reporter, err := NewTemplateReporter(templatePath + test.testReportName + ".tmpl") + require.NoError(t, err) + + tmpfile, err := os.Create(filepath.Join(t.TempDir(), test.testReportName+filepath.Ext(test.expected))) + require.NoError(t, err) + defer tmpfile.Close() + + err = reporter.Write(tmpfile, test.findings) + require.NoError(t, err) + assert.FileExists(t, tmpfile.Name()) + + got, err := os.ReadFile(tmpfile.Name()) + require.NoError(t, err) + if test.wantEmpty { + assert.Empty(t, got) + return + } + + want, err := os.ReadFile(test.expected) + require.NoError(t, err) + + wantStr := lineEndingReplacer.Replace(string(want)) + gotStr := lineEndingReplacer.Replace(string(got)) + assert.Equal(t, wantStr, gotStr) + }) + } +} + +func TestTemplateDangerousFunctions(t *testing.T) { + tests := []struct { + name string + template string + wantErr string + }{ + { + name: "env is blocked", + template: `{{ env "SECRET" }}`, + wantErr: `function "env" not defined`, + }, + { + name: "expandenv is blocked", + template: `{{ expandenv "$SECRET" }}`, + wantErr: `function "expandenv" not defined`, + }, + { + name: "getHostByName is blocked", + template: `{{ getHostByName "localhost" }}`, + wantErr: `function "getHostByName" not defined`, + }, + { + name: "now is allowed (benign)", + template: `{{ now | date "2006-01-02" }}`, + wantErr: "", // should not error on parse + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpfile, err := os.CreateTemp(t.TempDir(), "test*.tmpl") + require.NoError(t, err) + defer os.Remove(tmpfile.Name()) + + _, err = tmpfile.WriteString(tt.template) + require.NoError(t, err) + tmpfile.Close() + + _, err = NewTemplateReporter(tmpfile.Name()) + if tt.wantErr != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/report_templates/README.md b/report_templates/README.md new file mode 100644 index 0000000..f73ec1f --- /dev/null +++ b/report_templates/README.md @@ -0,0 +1,25 @@ +# Report Templates +Gitleaks has a neat little feature that lets you format the output of your findings via templates. This means Gitleaks can inject finding data into an html file for a web ui. Use the following command then open `index.html` after a scan. +``` +--report-path=index.html --report-format=template --report-template=report_templates/basic.tmpl +``` + +Below lies "vibe coding" slop. + +## Basic (Light) +Screenshot 2025-03-02 at 8 09 39 PM + +## Basic (Dark) +Screenshot 2025-03-02 at 8 20 36 PM + +## Windows 98 +Screenshot 2025-03-02 at 8 21 31 PM + +## Windows XP +Screenshot 2025-03-02 at 8 22 14 PM + +## l33t +Screenshot 2025-03-02 at 8 23 09 PM + +## myspace +Screenshot 2025-03-02 at 8 23 43 PM diff --git a/report_templates/basic.tmpl b/report_templates/basic.tmpl new file mode 100644 index 0000000..78869e1 --- /dev/null +++ b/report_templates/basic.tmpl @@ -0,0 +1,760 @@ + + + + + + Gitleaks Security Findings Report + + + +
    +
    + +
    + +
    +
    + +
    +
    +

    Security Scan Report

    +

    Generated on {{now | date "Jan 02, 2006 15:04:05 MST"}}

    + +
    +
    + {{len .}} + Total Findings +
    + +
    + - + Files Affected +
    + +
    + - + Unique Rules Triggered +
    + +
    + - + Scan Mode +
    +
    +
    + +
    +
    + + +
    + +
    + + +
    + +
    + +
    +
    + +
    + + + + + + + + + + + + {{- range . }} + + + + + + + + {{- end }} + +
    RuleFileDescriptionSecretMetadata
    {{.RuleID}} +
    + {{.File}} +
    +
    + {{- range .Tags }} + {{.}} + {{- end}} +
    +
    +
    + Line: + {{.StartLine}} +
    +
    +
    + {{.Description}} + + +
    +
    {{.Secret}}
    + + +
    +
    +
    +
    + Entropy: + {{printf "%.2f" .Entropy}} +
    + {{- if .Commit}} +
    + Commit: + {{if gt (len .Commit) 7}}{{printf "%.7s" .Commit}}{{else}}{{.Commit}}{{end}} +
    + {{- if .Author}} +
    + Author: + {{.Author}} +
    + {{- end}} + {{- if .Date}} +
    + Date: + {{.Date}} +
    + {{- end}} + {{- if .Link}} +
    + Link: + View Commit +
    + {{- end}} + {{- else}} + {{- if .Author}} +
    + Author: + {{.Author}} +
    + {{- end}} + {{- end}} +
    + + {{- if not .Match}} +
    -
    + {{- end}} +
    +
    +
    + +
    +
    Generated by Gitleaks
    +
    Total Findings: {{len .}}
    +
    +
    + + + + \ No newline at end of file diff --git a/report_templates/leet.tmpl b/report_templates/leet.tmpl new file mode 100644 index 0000000..3bd8fcd --- /dev/null +++ b/report_templates/leet.tmpl @@ -0,0 +1,847 @@ + + + + + + Gitleaks Security Findings Report + + + +
    +
    + +
    + +
    +
    +

    Security Scan Report

    +

    Generated on {{now | date "Jan 02, 2006 15:04:05 MST"}}

    + +
    +
    + {{len .}} + Total Findings +
    + +
    + - + Files Affected +
    + +
    + - + Unique Rules Triggered +
    + +
    + - + Scan Mode +
    +
    +
    + +
    +
    + + +
    + +
    + + +
    + +
    + +
    +
    + +
    + + + + + + + + + + + + {{- range . }} + + + + + + + + {{- end }} + +
    RuleFileDescriptionSecretMetadata
    {{.RuleID}} +
    + {{.File}} +
    +
    + {{- range .Tags }} + {{.}} + {{- end}} +
    +
    +
    + Line: + {{.StartLine}} +
    +
    +
    + {{.Description}} + + +
    +
    {{.Secret}}
    + + +
    +
    +
    +
    + Entropy: + {{printf "%.2f" .Entropy}} +
    + {{- if .Commit}} +
    + Commit: + {{if gt (len .Commit) 7}}{{printf "%.7s" .Commit}}{{else}}{{.Commit}}{{end}} +
    + {{- if .Author}} +
    + Author: + {{.Author}} +
    + {{- end}} + {{- if .Date}} +
    + Date: + {{.Date}} +
    + {{- end}} + {{- if .Link}} +
    + Link: + View Commit +
    + {{- end}} + {{- else}} + {{- if .Author}} +
    + Author: + {{.Author}} +
    + {{- end}} + {{- end}} +
    + + {{- if not .Match}} +
    -
    + {{- end}} +
    +
    +
    + +
    +
    Generated by Gitleaks
    +
    Total Findings: {{len .}}
    +
    +
    + + + + \ No newline at end of file diff --git a/report_templates/myspace.tmpl b/report_templates/myspace.tmpl new file mode 100644 index 0000000..0d0febc --- /dev/null +++ b/report_templates/myspace.tmpl @@ -0,0 +1,800 @@ + + + + + + Gitleaks Security Findings Report + + + +
    +
    + +
    + +
    +
    +

    Security Scan Report

    +

    Generated on {{now | date "Jan 02, 2006 15:04:05 MST"}}

    + +
    +
    + {{len .}} + Total Findings +
    + +
    + - + Files Affected +
    + +
    + - + Unique Rules Triggered +
    + +
    + - + Scan Mode +
    +
    +
    + +
    +
    + + +
    + +
    + + +
    + +
    + +
    +
    + +
    + + + + + + + + + + + + {{- range . }} + + + + + + + + {{- end }} + +
    RuleFileDescriptionSecretMetadata
    {{.RuleID}} +
    + {{.File}} +
    +
    + {{- range .Tags }} + {{.}} + {{- end}} +
    +
    +
    + Line: + {{.StartLine}} +
    +
    +
    + {{.Description}} + + +
    +
    {{.Secret}}
    + + +
    +
    +
    +
    + Entropy: + {{printf "%.2f" .Entropy}} +
    + {{- if .Commit}} +
    + Commit: + {{if gt (len .Commit) 7}}{{printf "%.7s" .Commit}}{{else}}{{.Commit}}{{end}} +
    + {{- if .Author}} +
    + Author: + {{.Author}} +
    + {{- end}} + {{- if .Date}} +
    + Date: + {{.Date}} +
    + {{- end}} + {{- if .Link}} +
    + Link: + View Commit +
    + {{- end}} + {{- else}} + {{- if .Author}} +
    + Author: + {{.Author}} +
    + {{- end}} + {{- end}} +
    + + {{- if not .Match}} +
    -
    + {{- end}} +
    +
    +
    + +
    +
    Generated by Gitleaks
    +
    Total Findings: {{len .}}
    +
    +
    + + + + \ No newline at end of file diff --git a/report_templates/w98.tmpl b/report_templates/w98.tmpl new file mode 100644 index 0000000..5a8717d --- /dev/null +++ b/report_templates/w98.tmpl @@ -0,0 +1,723 @@ + + + + + + Gitleaks Security Findings Report + + + +
    +
    + +
    + +
    +
    +

    Security Scan Report

    +

    Generated on {{now | date "Jan 02, 2006 15:04:05 MST"}}

    + +
    +
    + {{len .}} + Total Findings +
    + +
    + - + Files Affected +
    + +
    + - + Unique Rules Triggered +
    + +
    + - + Scan Mode +
    +
    +
    + +
    +
    + + +
    + +
    + + +
    + +
    + +
    +
    + +
    + + + + + + + + + + + + {{- range . }} + + + + + + + + {{- end }} + +
    RuleFileDescriptionSecretMetadata
    {{.RuleID}} +
    + {{.File}} +
    +
    + {{- range .Tags }} + {{.}} + {{- end}} +
    +
    +
    + Line: + {{.StartLine}} +
    +
    +
    + {{.Description}} + + +
    +
    {{.Secret}}
    + + +
    +
    +
    +
    + Entropy: + {{printf "%.2f" .Entropy}} +
    + {{- if .Commit}} +
    + Commit: + {{if gt (len .Commit) 7}}{{printf "%.7s" .Commit}}{{else}}{{.Commit}}{{end}} +
    + {{- if .Author}} +
    + Author: + {{.Author}} +
    + {{- end}} + {{- if .Date}} +
    + Date: + {{.Date}} +
    + {{- end}} + {{- if .Link}} +
    + Link: + View Commit +
    + {{- end}} + {{- else}} + {{- if .Author}} +
    + Author: + {{.Author}} +
    + {{- end}} + {{- end}} +
    + + {{- if not .Match}} +
    -
    + {{- end}} +
    +
    +
    + +
    +
    Generated by Gitleaks
    +
    Total Findings: {{len .}}
    +
    +
    + + + + \ No newline at end of file diff --git a/report_templates/wxp.tmpl b/report_templates/wxp.tmpl new file mode 100644 index 0000000..cf5e932 --- /dev/null +++ b/report_templates/wxp.tmpl @@ -0,0 +1,760 @@ + + + + + + Gitleaks Security Findings Report + + + +
    +
    + +
    + +
    +
    +

    Security Scan Report

    +

    Generated on {{now | date "Jan 02, 2006 15:04:05 MST"}}

    + +
    +
    + {{len .}} + Total Findings +
    + +
    + - + Files Affected +
    + +
    + - + Unique Rules Triggered +
    + +
    + - + Scan Mode +
    +
    +
    + +
    +
    + + +
    + +
    + + +
    + +
    + +
    +
    + +
    + + + + + + + + + + + + {{- range . }} + + + + + + + + {{- end }} + +
    RuleFileDescriptionSecretMetadata
    {{.RuleID}} +
    + {{.File}} +
    +
    + {{- range .Tags }} + {{.}} + {{- end}} +
    +
    +
    + Line: + {{.StartLine}} +
    +
    +
    + {{.Description}} + + +
    +
    {{.Secret}}
    + + +
    +
    +
    +
    + Entropy: + {{printf "%.2f" .Entropy}} +
    + {{- if .Commit}} +
    + Commit: + {{if gt (len .Commit) 7}}{{printf "%.7s" .Commit}}{{else}}{{.Commit}}{{end}} +
    + {{- if .Author}} +
    + Author: + {{.Author}} +
    + {{- end}} + {{- if .Date}} +
    + Date: + {{.Date}} +
    + {{- end}} + {{- if .Link}} +
    + Link: + View Commit +
    + {{- end}} + {{- else}} + {{- if .Author}} +
    + Author: + {{.Author}} +
    + {{- end}} + {{- end}} +
    + + {{- if not .Match}} +
    -
    + {{- end}} +
    +
    +
    + +
    +
    Generated by Gitleaks
    +
    Total Findings: {{len .}}
    +
    +
    + + + + \ No newline at end of file diff --git a/scripts/pre-commit.py b/scripts/pre-commit.py new file mode 100644 index 0000000..64d48d5 --- /dev/null +++ b/scripts/pre-commit.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Helper script to be used as a pre-commit hook.""" +import os +import sys +import subprocess + + +def gitleaksEnabled(): + """Determine if the pre-commit hook for gitleaks is enabled.""" + 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`)') diff --git a/scripts/profile.sh b/scripts/profile.sh new file mode 100755 index 0000000..942b5d4 --- /dev/null +++ b/scripts/profile.sh @@ -0,0 +1,80 @@ +#! /usr/bin/env bash +# NAME +# profile.sh - generate gitleaks profile data +# +# USAGE +# profile.sh +# +# DESCRIPTION +# Generates profile data for tuning gitleaks performance under ./profile/ +# +# Options: +# - gitleaks binary to profile +# - git repo to run profile against +# +# SEE ALSO +# Dave Cheney GopherCon 2019 talk on go profiling: +# +# https://www.youtube.com/watch?v=nok0aYiGiYA +# +set -euo pipefail +gitleaks_path="$1" +test_repo_path="$2" +base_scan_cmd="${gitleaks_path} --exit-code=0 --max-decode-depth 8" +base_profile_dir="profile/$(date +%s)" + +log() { + echo >&2 "$@" +} + +log '========================================================================' +log 'generating profile data' +log '------------------------------------------------------------------------' +# Warm up the fs and also get benchmark data +for scan_mode in dir git +do + profile_dir="${base_profile_dir}/${scan_mode}" + scan_cmd="${base_scan_cmd} ${scan_mode} ${test_repo_path}" + mkdir -p "${profile_dir}" + + echo "- mode: ${scan_mode}" + # include hyperfine benchmrak results if hyperfine is installed + if command -v hyperfine > /dev/null + then + export_path="${profile_dir}/benchmark.json" + echo " benchmark:" + echo " tool: hyperfine" + hyperfine -w 3 --export-json "${export_path}" "${scan_cmd}" &> /dev/null + echo " path: ${export_path}" + # Show the results if we can :D + if command -v yq > /dev/null + then + echo " results:" + yq -P -oy \ + '.results[] | pick(["mean","stddev","median","user","system","min","max"])' \ + ${export_path} | sed 's/^/ /g' + else + echo " view: ${PAGER:-less} ${export_path}" + fi + fi + + # generate profile data + echo " profile:" + for profile_mode in cpu mem trace + do + # Generate diagnostics data + ${scan_cmd} --diagnostics=$profile_mode --diagnostics-dir="${profile_dir}" &> /dev/null + profile_file="$(find "${profile_dir}" -type f -name "${profile_mode}*")" + echo " - mode: ${profile_mode}" + echo " path: ${profile_file}" + if [[ "${profile_mode}" = "trace" ]] + then + echo " view: go tool trace ${profile_file}" + else + echo " view: go tool pprof -http=localhost: ${gitleaks_path} ${profile_file}" + fi + done +done +log '------------------------------------------------------------------------' +log "results in: ${base_profile_dir}" +log '========================================================================' diff --git a/sources/common.go b/sources/common.go new file mode 100644 index 0000000..7efc371 --- /dev/null +++ b/sources/common.go @@ -0,0 +1,125 @@ +package sources + +import ( + "bufio" + "bytes" + "context" + "io" + "path/filepath" + "runtime" + + "github.com/mholt/archives" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/logging" +) + +const maxPeekSize = 25 * 1_000 // 10kb +var isWhitespace [256]bool +var isWindows = runtime.GOOS == "windows" + +func init() { + // define whitespace characters + isWhitespace[' '] = true + isWhitespace['\t'] = true + isWhitespace['\n'] = true + isWhitespace['\r'] = true +} + +// isArchive does a light check to see if the provided path is an archive or +// compressed file. The File source already does this, so this exists mainly +// to avoid expensive calls before sending things to the File source +func isArchive(ctx context.Context, path string) bool { + format, _, err := archives.Identify(ctx, path, nil) + return err == nil && format != nil +} + +// shouldSkipPath checks a path against all the allowlists to see if it can +// be skipped +func shouldSkipPath(cfg *config.Config, path string) bool { + if cfg == nil { + logging.Trace().Str("path", path).Msg("not skipping path because config is nil") + return false + } + + for _, a := range cfg.Allowlists { + if a.PathAllowed(path) || + // TODO: Remove this in v9. + // This is an awkward hack to mitigate https://github.com/gitleaks/gitleaks/issues/1641. + (isWindows && a.PathAllowed(filepath.ToSlash(path))) { + return true + } + } + + return false +} + +// readUntilSafeBoundary consumes |f| until it finds two consecutive `\n` characters, up to |maxPeekSize|. +// This hopefully avoids splitting. (https://github.com/gitleaks/gitleaks/issues/1651) +func readUntilSafeBoundary(r *bufio.Reader, n int, maxPeekSize int, peekBuf *bytes.Buffer) error { + if peekBuf.Len() == 0 { + return nil + } + + // Does the buffer end in consecutive newlines? + var ( + data = peekBuf.Bytes() + lastChar = data[len(data)-1] + newlineCount = 0 // Tracks consecutive newlines + ) + + if isWhitespace[lastChar] { + for i := len(data) - 1; i >= 0; i-- { + lastChar = data[i] + if lastChar == '\n' { + newlineCount++ + + // Stop if two consecutive newlines are found + if newlineCount >= 2 { + return nil + } + } else if isWhitespace[lastChar] { + // The presence of other whitespace characters (`\r`, ` `, `\t`) shouldn't reset the count. + // (Intentionally do nothing.) + } else { + break + } + } + } + + // If not, read ahead until we (hopefully) find some. + newlineCount = 0 + for { + data = peekBuf.Bytes() + // Check if the last character is a newline. + lastChar = data[len(data)-1] + if lastChar == '\n' { + newlineCount++ + + // Stop if two consecutive newlines are found + if newlineCount >= 2 { + break + } + } else if isWhitespace[lastChar] { + // The presence of other whitespace characters (`\r`, ` `, `\t`) shouldn't reset the count. + // (Intentionally do nothing.) + } else { + newlineCount = 0 // Reset if a non-newline character is found + } + + // Stop growing the buffer if it reaches maxSize + if (peekBuf.Len() - n) >= maxPeekSize { + break + } + + // Read additional data into a temporary buffer + b, err := r.ReadByte() + if err != nil { + if err == io.EOF { + break + } + return err + } + peekBuf.WriteByte(b) + } + return nil +} diff --git a/sources/common_test.go b/sources/common_test.go new file mode 100644 index 0000000..aebe0da --- /dev/null +++ b/sources/common_test.go @@ -0,0 +1,72 @@ +package sources + +import ( + "bufio" + "bytes" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_readUntilSafeBoundary(t *testing.T) { + // Arrange + cases := []struct { + name string + r io.Reader + expected string + }{ + // Current split is fine, exit early. + { + name: "safe original split - LF", + r: strings.NewReader("abc\n\ndefghijklmnop\n\nqrstuvwxyz"), + expected: "abc\n\n", + }, + { + name: "safe original split - CRLF", + r: strings.NewReader("a\r\n\r\nbcdefghijklmnop\n"), + expected: "a\r\n\r\n", + }, + // Current split is bad, look for a better one. + { + name: "safe split - LF", + r: strings.NewReader("abcdefg\nhijklmnop\n\nqrstuvwxyz"), + expected: "abcdefg\nhijklmnop\n\n", + }, + { + name: "safe split - CRLF", + r: strings.NewReader("abcdefg\r\nhijklmnop\r\n\r\nqrstuvwxyz"), + expected: "abcdefg\r\nhijklmnop\r\n\r\n", + }, + { + name: "safe split - blank line", + r: strings.NewReader("abcdefg\nhijklmnop\n\t \t\nqrstuvwxyz"), + expected: "abcdefg\nhijklmnop\n\t \t\n", + }, + // Current split is bad, exhaust options. + { + name: "no safe split", + r: strings.NewReader("abcdefg\nhijklmnopqrstuvwxyz"), + expected: "abcdefg\nhijklmnopqrstuvwx", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + buf := make([]byte, 5) + n, err := c.r.Read(buf) + require.NoError(t, err) + + // Act + reader := bufio.NewReader(c.r) + peekBuf := bytes.NewBuffer(buf[:n]) + err = readUntilSafeBoundary(reader, n, 20, peekBuf) + require.NoError(t, err) + + // Assert + t.Log(peekBuf.String()) + require.Equal(t, c.expected, peekBuf.String()) + }) + } +} diff --git a/sources/file.go b/sources/file.go new file mode 100644 index 0000000..4286528 --- /dev/null +++ b/sources/file.go @@ -0,0 +1,264 @@ +package sources + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/h2non/filetype" + "github.com/mholt/archives" + "github.com/rs/zerolog" + + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/logging" +) + +const defaultBufferSize = 100 * 1_000 // 100kb +const InnerPathSeparator = "!" + +type seekReaderAt interface { + io.ReaderAt + io.Seeker +} + +// File is a source for yielding fragments from a file or other reader +type File struct { + // Content provides a reader to the file's content + Content io.Reader + // Path is the resolved real path of the file + Path string + // Symlink represents a symlink to the file if that's how it was discovered + Symlink string + // Buffer is used for reading the content in chunks + Buffer []byte + // Config is the gitleaks config used for shouldSkipPath. If not set, then + // shouldSkipPath is ignored + Config *config.Config + // outerPaths is the list of container paths (e.g. archives) that lead to + // this file + outerPaths []string + // MaxArchiveDepth limits how deep the sources will explore nested archives + MaxArchiveDepth int + // archiveDepth is the current archive nesting depth + archiveDepth int +} + +// Fragments yields fragments for the this source +func (s *File) Fragments(ctx context.Context, yield FragmentsFunc) error { + format, _, err := archives.Identify(ctx, s.Path, nil) + // Process the file as an archive if there's no error && Identify returns + // a format; but if there's an error or no format, just swallow the error + // and fall back on treating it like a normal file and let fileFragments + // decide what to do with it. + if err == nil && format != nil { + if s.archiveDepth+1 > s.MaxArchiveDepth { + var event *zerolog.Event + + // Warn if the feature is enabled; else emit a trace log. + if s.MaxArchiveDepth != 0 { + event = logging.Warn() + } else { + event = logging.Trace() + } + + event.Str( + "path", s.FullPath(), + ).Int( + "max_archive_depth", s.MaxArchiveDepth, + ).Msg("skipping archive: exceeds max archive depth") + + return nil + } + if extractor, ok := format.(archives.Extractor); ok { + return s.extractorFragments(ctx, extractor, s.Content, yield) + } + if decompressor, ok := format.(archives.Decompressor); ok { + return s.decompressorFragments(ctx, decompressor, s.Content, yield) + } + logging.Warn().Str("path", s.FullPath()).Msg("skipping unknown archive type") + } + + return s.fileFragments(ctx, bufio.NewReader(s.Content), yield) +} + +// extractorFragments recursively crawls archives and yields fragments +func (s *File) extractorFragments(ctx context.Context, extractor archives.Extractor, reader io.Reader, yield FragmentsFunc) error { + if _, isSeekReaderAt := reader.(seekReaderAt); !isSeekReaderAt { + switch extractor.(type) { + case archives.SevenZip, archives.Zip: + tmpfile, err := os.CreateTemp("", "gitleaks-archive-") + if err != nil { + logging.Error().Str("path", s.FullPath()).Msg("could not create tmp file") + return nil + } + defer func() { + _ = tmpfile.Close() + _ = os.Remove(tmpfile.Name()) + }() + + _, err = io.Copy(tmpfile, reader) + if err != nil { + logging.Error().Str("path", s.FullPath()).Msg("could not copy archive file") + return nil + } + + reader = tmpfile + } + } + + return extractor.Extract(ctx, reader, func(_ context.Context, d archives.FileInfo) error { + if d.IsDir() { + return nil + } + + innerReader, err := d.Open() + if err != nil { + logging.Error().Err(err).Str("path", s.FullPath()).Msg("could not open archive inner file") + return nil + } + defer innerReader.Close() + path := filepath.Clean(d.NameInArchive) + + if s.Config != nil && shouldSkipPath(s.Config, path) { + logging.Debug().Str("path", s.FullPath()).Msg("skipping file: global allowlist") + return nil + } + + file := &File{ + Content: innerReader, + Path: path, + Symlink: s.Symlink, + outerPaths: append(s.outerPaths, filepath.ToSlash(s.Path)), + MaxArchiveDepth: s.MaxArchiveDepth, + archiveDepth: s.archiveDepth + 1, + } + + if err := file.Fragments(ctx, yield); err != nil { + return err + } + + return nil + }) +} + +// decompressorFragments recursively crawls archives and yields fragments +func (s *File) decompressorFragments(ctx context.Context, decompressor archives.Decompressor, reader io.Reader, yield FragmentsFunc) error { + innerReader, err := decompressor.OpenReader(reader) + if err != nil { + logging.Error().Str("path", s.FullPath()).Msg("could read compressed file") + return nil + } + + if err := s.fileFragments(ctx, bufio.NewReader(innerReader), yield); err != nil { + _ = innerReader.Close() + return err + } + + _ = innerReader.Close() + return nil +} + +// fileFragments reads the file into fragments to yield +func (s *File) fileFragments(ctx context.Context, reader *bufio.Reader, yield FragmentsFunc) error { + // Create a buffer if the caller hasn't provided one + if s.Buffer == nil { + s.Buffer = make([]byte, defaultBufferSize) + } + + totalLines := 0 + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + fragment := Fragment{ + FilePath: s.FullPath(), + } + + n, err := reader.Read(s.Buffer) + if n == 0 { + if err != nil && err != io.EOF { + return yield(fragment, fmt.Errorf("could not read file: %w", err)) + } + + return nil + } + + // Only check the filetype at the start of file. + if totalLines == 0 { + // TODO: could other optimizations be introduced here? + if mimetype, err := filetype.Match(s.Buffer[:n]); err != nil { + return yield( + fragment, + fmt.Errorf("could not read file: could not determine type: %w", err), + ) + } else if mimetype.MIME.Type == "application" { + logging.Debug(). + Str("mime_type", mimetype.MIME.Value). + Str("path", s.FullPath()). + Msgf("skipping binary file") + + return nil + } + } + + // Try to split chunks across large areas of whitespace, if possible. + peekBuf := bytes.NewBuffer(s.Buffer[:n]) + if err := readUntilSafeBoundary(reader, n, maxPeekSize, peekBuf); err != nil { + return yield( + fragment, + fmt.Errorf("could not read file: could not read until safe boundary: %w", err), + ) + } + + fragment.Raw = peekBuf.String() + fragment.Bytes = peekBuf.Bytes() + fragment.StartLine = totalLines + 1 + + // Count the number of newlines in this chunk + totalLines += strings.Count(fragment.Raw, "\n") + + if len(s.Symlink) > 0 { + fragment.SymlinkFile = s.Symlink + } + + if isWindows { + fragment.FilePath = filepath.ToSlash(fragment.FilePath) + fragment.SymlinkFile = filepath.ToSlash(s.Symlink) + fragment.WindowsFilePath = s.FullPath() + } + + // log errors but continue since there's content + if err != nil && err != io.EOF { + logging.Warn().Err(err).Msgf("issue reading file") + } + + // Done with the file! + if err == io.EOF { + return yield(fragment, nil) + } + + if err := yield(fragment, err); err != nil { + return err + } + } + } +} + +// FullPath returns the File.Path with any preceding outer paths +func (s *File) FullPath() string { + if len(s.outerPaths) > 0 { + return strings.Join( + // outerPaths have already been normalized to slash + append(s.outerPaths, s.Path), + InnerPathSeparator, + ) + } + + return s.Path +} diff --git a/sources/files.go b/sources/files.go new file mode 100644 index 0000000..e39a370 --- /dev/null +++ b/sources/files.go @@ -0,0 +1,191 @@ +package sources + +import ( + "context" + "errors" + "io/fs" + "os" + "path/filepath" + "sync" + + "github.com/fatih/semgroup" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/logging" +) + +// TODO: remove this in v9 and have scanTargets yield file sources +type ScanTarget struct { + Path string + Symlink string +} + +// Deprecated: Use Files and detector.DetectSource instead +func DirectoryTargets(sourcePath string, s *semgroup.Group, followSymlinks bool, allowlists []*config.Allowlist) (<-chan ScanTarget, error) { + paths := make(chan ScanTarget) + + // create a Files source + files := Files{ + FollowSymlinks: followSymlinks, + Path: sourcePath, + Sema: s, + Config: &config.Config{ + Allowlists: allowlists, + }, + } + + s.Go(func() error { + ctx := context.Background() + err := files.scanTargets(ctx, func(scanTarget ScanTarget, err error) error { + paths <- scanTarget + return nil + }) + close(paths) + return err + }) + + return paths, nil +} + +// Files is a source for yielding fragments from a collection of files +type Files struct { + Config *config.Config + FollowSymlinks bool + MaxFileSize int + Path string + Sema *semgroup.Group + MaxArchiveDepth int +} + +// scanTargets yields scan targets to a callback func +func (s *Files) scanTargets(ctx context.Context, yield func(ScanTarget, error) error) error { + return filepath.WalkDir(s.Path, func(path string, d fs.DirEntry, err error) error { + scanTarget := ScanTarget{Path: path} + logger := logging.With().Str("path", path).Logger() + + if err != nil { + if os.IsPermission(err) { + // This seems to only fail on directories at this stage. + logger.Warn().Err(errors.New("permission denied")).Msg("skipping directory") + return filepath.SkipDir + } + logger.Warn().Err(err).Msg("skipping") + return nil + } + + info, err := d.Info() + if err != nil { + if d.IsDir() { + logger.Error().Err(err).Msg("skipping directory: could not get info") + return filepath.SkipDir + } + logger.Error().Err(err).Msg("skipping file: could not get info") + return nil + } + + if !d.IsDir() { + // Empty; nothing to do here. + if info.Size() == 0 { + logger.Debug().Msg("skipping empty file") + return nil + } + + // Too large; nothing to do here. + if s.MaxFileSize > 0 && info.Size() > int64(s.MaxFileSize) { + logger.Warn().Msgf( + "skipping file: too large max_size=%dMB, size=%dMB", + s.MaxFileSize/1_000_000, info.Size()/1_000_000, + ) + return nil + } + } + + // set the initial scan target values + if d.Type() == fs.ModeSymlink { + if !s.FollowSymlinks { + logger.Debug().Msg("skipping symlink: follow symlinks disabled") + return nil + } + realPath, err := filepath.EvalSymlinks(path) + if err != nil { + logger.Error().Err(err).Msg("skipping symlink: could not evaluate") + return nil + } + if realPathFileInfo, _ := os.Stat(realPath); realPathFileInfo.IsDir() { + logger.Debug().Str("target", realPath).Msgf("skipping symlink: target is directory") + return nil + } + scanTarget = ScanTarget{ + Path: realPath, + Symlink: path, + } + } + + // handle dir cases (mainly just see if it should be skipped + if info.IsDir() { + if shouldSkipPath(s.Config, path) { + logger.Debug().Msg("skipping directory: global allowlist") + return filepath.SkipDir + } + return nil + } + + if shouldSkipPath(s.Config, path) { + logger.Debug().Msg("skipping file: global allowlist") + return nil + } + + return yield(scanTarget, nil) + }) +} + +// Fragments yields fragments from files discovered under the path +func (s *Files) Fragments(ctx context.Context, yield FragmentsFunc) error { + var wg sync.WaitGroup + + err := s.scanTargets(ctx, func(scanTarget ScanTarget, err error) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + wg.Add(1) + s.Sema.Go(func() error { + logger := logging.With().Str("path", scanTarget.Path).Logger() + logger.Trace().Msg("scanning path") + + f, err := os.Open(scanTarget.Path) + if err != nil { + if os.IsPermission(err) { + logger.Warn().Msg("skipping file: permission denied") + } + wg.Done() + return nil + } + + // Convert this to a file source + file := File{ + Content: f, + Path: scanTarget.Path, + Symlink: scanTarget.Symlink, + Config: s.Config, + MaxArchiveDepth: s.MaxArchiveDepth, + } + + err = file.Fragments(ctx, yield) + // Avoiding a defer in a hot loop + _ = f.Close() + wg.Done() + return err + }) + + return nil + } + }) + + select { + case <-ctx.Done(): + return ctx.Err() + default: + wg.Wait() + return err + } +} diff --git a/sources/fragment.go b/sources/fragment.go new file mode 100644 index 0000000..7af1f8c --- /dev/null +++ b/sources/fragment.go @@ -0,0 +1,28 @@ +package sources + +// Fragment represents a fragment of a source with its meta data +type Fragment struct { + // Raw is the raw content of the fragment + Raw string + + Bytes []byte + + // FilePath is the path to the file if applicable. + // The path separator MUST be normalized to `/`. + FilePath string + SymlinkFile string + // WindowsFilePath is the path with the original separator. + // This provides a backwards-compatible solution to https://github.com/gitleaks/gitleaks/issues/1565. + WindowsFilePath string `json:"-"` // TODO: remove this in v9. + + // CommitSHA is the SHA of the commit if applicable + CommitSHA string // TODO: remove this in v9 and use CommitInfo instead + + // StartLine is the line number this fragment starts on + StartLine int + + // CommitInfo captures additional information about the git commit if applicable + CommitInfo *CommitInfo + + InheritedFromFinding bool // Indicates if this fragment is inherited from a finding +} diff --git a/sources/git.go b/sources/git.go new file mode 100644 index 0000000..0b9872f --- /dev/null +++ b/sources/git.go @@ -0,0 +1,530 @@ +package sources + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "io" + "net/url" + "os/exec" + "path/filepath" + "regexp" + "strings" + "sync" + "time" + + "github.com/fatih/semgroup" + "github.com/gitleaks/go-gitdiff/gitdiff" + + "github.com/zricethezav/gitleaks/v8/cmd/scm" + "github.com/zricethezav/gitleaks/v8/config" + "github.com/zricethezav/gitleaks/v8/logging" +) + +var quotedOptPattern = regexp.MustCompile(`^(?:"[^"]+"|'[^']+')$`) + +// GitCmd helps to work with Git's output. +type GitCmd struct { + cmd *exec.Cmd + diffFilesCh <-chan *gitdiff.File + errCh <-chan error + repoPath string +} + +// blobReader provides a ReadCloser interface git cat-file blob to fetch +// a blob from a repo +type blobReader struct { + io.ReadCloser + cmd *exec.Cmd +} + +// Close closes the underlying reader and then waits for the command to complete, +// releasing its resources. +func (br *blobReader) Close() error { + // Discard the remaining data from the pipe to avoid blocking + _, drainErr := io.Copy(io.Discard, br) + // Close the pipe (should signal the command to stop if it hasn't already) + closeErr := br.ReadCloser.Close() + // Wait to prevent zombie processes. + waitErr := br.cmd.Wait() + // Return the first error encountered + if drainErr != nil { + return drainErr + } + if closeErr != nil { + return closeErr + } + return waitErr +} + +// NewGitLogCmd returns `*DiffFilesCmd` with two channels: `<-chan *gitdiff.File` and `<-chan error`. +// Caller should read everything from channels until receiving a signal about their closure and call +// the `func (*DiffFilesCmd) Wait()` error in order to release resources. +func NewGitLogCmd(source string, logOpts string) (*GitCmd, error) { + return NewGitLogCmdContext(context.Background(), source, logOpts) +} + +// NewGitLogCmdContext is the same as NewGitLogCmd but supports passing in a +// context to use for timeouts +func NewGitLogCmdContext(ctx context.Context, source string, logOpts string) (*GitCmd, error) { + sourceClean := filepath.Clean(source) + var cmd *exec.Cmd + if logOpts != "" { + args := []string{"-C", sourceClean, "log", "-p", "-U0"} + + // Ensure that the user-provided |logOpts| aren't wrapped in quotes. + // https://github.com/gitleaks/gitleaks/issues/1153 + userArgs := strings.Split(logOpts, " ") + var quotedOpts []string + for _, element := range userArgs { + if quotedOptPattern.MatchString(element) { + quotedOpts = append(quotedOpts, element) + } + } + if len(quotedOpts) > 0 { + logging.Warn().Msgf("the following `--log-opts` values may not work as expected: %v\n\tsee https://github.com/gitleaks/gitleaks/issues/1153 for more information", quotedOpts) + } + + args = append(args, userArgs...) + cmd = exec.CommandContext(ctx, "git", args...) + } else { + cmd = exec.CommandContext(ctx, "git", "-C", sourceClean, "log", "-p", "-U0", + "--full-history", "--all", "--diff-filter=tuxdb") + } + + logging.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 + } + + errCh := make(chan error) + go listenForStdErr(stderr, errCh) + + gitdiffFiles, err := gitdiff.Parse(stdout) + if err != nil { + return nil, err + } + + return &GitCmd{ + cmd: cmd, + diffFilesCh: gitdiffFiles, + errCh: errCh, + repoPath: sourceClean, + }, nil +} + +// NewGitDiffCmd returns `*DiffFilesCmd` with two channels: `<-chan *gitdiff.File` and `<-chan error`. +// Caller should read everything from channels until receiving a signal about their closure and call +// the `func (*DiffFilesCmd) Wait()` error in order to release resources. +func NewGitDiffCmd(source string, staged bool) (*GitCmd, error) { + return NewGitDiffCmdContext(context.Background(), source, staged) +} + +// NewGitDiffCmdContext is the same as NewGitDiffCmd but supports passing in a +// context to use for timeouts +func NewGitDiffCmdContext(ctx context.Context, source string, staged bool) (*GitCmd, error) { + sourceClean := filepath.Clean(source) + var cmd *exec.Cmd + cmd = exec.CommandContext(ctx, "git", "-C", sourceClean, "diff", "-U0", "--no-ext-diff", ".") + if staged { + cmd = exec.CommandContext(ctx, "git", "-C", sourceClean, "diff", "-U0", "--no-ext-diff", + "--staged", ".") + } + logging.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 + } + + errCh := make(chan error) + go listenForStdErr(stderr, errCh) + + gitdiffFiles, err := gitdiff.Parse(stdout) + if err != nil { + return nil, err + } + + return &GitCmd{ + cmd: cmd, + diffFilesCh: gitdiffFiles, + errCh: errCh, + repoPath: sourceClean, + }, nil +} + +// DiffFilesCh returns a channel with *gitdiff.File. +func (c *GitCmd) DiffFilesCh() <-chan *gitdiff.File { + return c.diffFilesCh +} + +// ErrCh returns a channel that could produce an error if there is something in stderr. +func (c *GitCmd) ErrCh() <-chan error { + return c.errCh +} + +// Wait waits for the command to exit and waits for any copying to +// stdin or copying from stdout or stderr to complete. +// +// Wait also closes underlying stdout and stderr. +func (c *GitCmd) Wait() error { + return c.cmd.Wait() +} + +// String displays the command used for GitCmd +func (c *GitCmd) String() string { + return c.cmd.String() +} + +// NewBlobReader returns an io.ReadCloser that can be used to read a blob +// within the git repo used to create the GitCmd. +// +// The caller is responsible for closing the reader. +func (c *GitCmd) NewBlobReader(commit, path string) (io.ReadCloser, error) { + return c.NewBlobReaderContext(context.Background(), commit, path) +} + +// NewBlobReaderContext is the same as NewBlobReader but supports passing in a +// context to use for timeouts +func (c *GitCmd) NewBlobReaderContext(ctx context.Context, commit, path string) (io.ReadCloser, error) { + gitArgs := []string{"-C", c.repoPath, "cat-file", "blob", commit + ":" + path} + cmd := exec.CommandContext(ctx, "git", gitArgs...) + cmd.Stderr = io.Discard + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("failed to get stdout pipe: %w", err) + } + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start git command: %w", err) + } + return &blobReader{ + ReadCloser: stdout, + cmd: cmd, + }, nil +} + +// listenForStdErr listens for stderr output from git, prints it to stdout, +// sends to errCh and closes it. +func listenForStdErr(stderr io.ReadCloser, errCh chan<- error) { + defer close(errCh) + + var errEncountered bool + + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + // if git throws one of the following errors: + // + // exhaustive rename detection was skipped due to too many files. + // you may want to set your diff.renameLimit variable to at least + // (some large number) and retry the command. + // + // inexact rename detection was skipped due to too many files. + // you may want to set your diff.renameLimit variable to at least + // (some large number) and retry the command. + // + // Auto packing the repository in background for optimum performance. + // See "git help gc" for manual housekeeping. + // + // we skip exiting the program as git log -p/git diff will continue + // to send data to stdout and finish executing. This next bit of + // code prevents gitleaks from stopping mid scan if this error is + // encountered + if strings.Contains(scanner.Text(), + "exhaustive rename detection was skipped") || + strings.Contains(scanner.Text(), + "inexact rename detection was skipped") || + strings.Contains(scanner.Text(), + "you may want to set your diff.renameLimit") || + strings.Contains(scanner.Text(), + "See \"git help gc\" for manual housekeeping") || + strings.Contains(scanner.Text(), + "Auto packing the repository in background for optimum performance") { + logging.Warn().Msg(scanner.Text()) + } else { + logging.Error().Msgf("[git] %s", scanner.Text()) + errEncountered = true + } + } + + if errEncountered { + errCh <- errors.New("stderr is not empty") + return + } +} + +// RemoteInfo provides the info needed for reconstructing links from findings +type RemoteInfo struct { + Platform scm.Platform + Url string +} + +// Git is a source for yielding fragments from a git repo +type Git struct { + Cmd *GitCmd + Config *config.Config + Remote *RemoteInfo + Sema *semgroup.Group + MaxArchiveDepth int +} + +// CommitInfo captures metadata about the commit +type CommitInfo struct { + AuthorEmail string + AuthorName string + Date string + Message string + Remote *RemoteInfo + SHA string +} + +// Fragments yields fragments from a git repo +func (s *Git) Fragments(ctx context.Context, yield FragmentsFunc) error { + defer func() { + if err := s.Cmd.Wait(); err != nil { + logging.Debug().Err(err).Str("cmd", s.Cmd.String()).Msg("command aborted") + } + }() + + var ( + diffFilesCh = s.Cmd.DiffFilesCh() + errCh = s.Cmd.ErrCh() + wg sync.WaitGroup + ) + + // loop to range over both DiffFiles (stdout) and ErrCh (stderr) + for diffFilesCh != nil || errCh != nil { + select { + case <-ctx.Done(): + return ctx.Err() + case gitdiffFile, open := <-diffFilesCh: + if !open { + diffFilesCh = nil + break + } + + if gitdiffFile.IsDelete { + continue + } + + // skip non-archive binary files + yieldAsArchive := false + if gitdiffFile.IsBinary { + if !isArchive(ctx, gitdiffFile.NewName) { + continue + } + yieldAsArchive = true + } + + // Check if commit is allowed + commitSHA := "" + var commitInfo *CommitInfo + if gitdiffFile.PatchHeader != nil { + commitSHA = gitdiffFile.PatchHeader.SHA + for _, a := range s.Config.Allowlists { + if ok, c := a.CommitAllowed(gitdiffFile.PatchHeader.SHA); ok { + logging.Trace().Str("allowed-commit", c).Msg("skipping commit: global allowlist") + continue + } + } + + commitInfo = &CommitInfo{ + Date: gitdiffFile.PatchHeader.AuthorDate.UTC().Format(time.RFC3339), + Message: gitdiffFile.PatchHeader.Message(), + Remote: s.Remote, + SHA: commitSHA, + } + + if gitdiffFile.PatchHeader.Author != nil { + commitInfo.AuthorName = gitdiffFile.PatchHeader.Author.Name + commitInfo.AuthorEmail = gitdiffFile.PatchHeader.Author.Email + } + } + + wg.Add(1) + s.Sema.Go(func() error { + defer wg.Done() + + if yieldAsArchive { + blob, err := s.Cmd.NewBlobReaderContext(ctx, commitSHA, gitdiffFile.NewName) + if err != nil { + logging.Error().Err(err).Msg("could not read archive blob") + return nil + } + + file := File{ + Content: blob, + Path: gitdiffFile.NewName, + MaxArchiveDepth: s.MaxArchiveDepth, + Config: s.Config, + } + + // enrich and yield fragments + err = file.Fragments(ctx, func(fragment Fragment, err error) error { + fragment.CommitSHA = commitSHA + fragment.CommitInfo = commitInfo + return yield(fragment, err) + }) + + // Close the blob reader and log any issues + if err := blob.Close(); err != nil { + logging.Debug().Err(err).Msg("blobReader.Close() returned an error") + } + + return err + } + + for _, textFragment := range gitdiffFile.TextFragments { + if textFragment == nil { + return nil + } + + fragment := Fragment{ + CommitSHA: commitSHA, + FilePath: gitdiffFile.NewName, + Raw: textFragment.Raw(gitdiff.OpAdd), + StartLine: int(textFragment.NewPosition), + CommitInfo: commitInfo, + } + + if err := yield(fragment, nil); err != nil { + return err + } + } + + return nil + }) + case err, open := <-errCh: + if !open { + errCh = nil + break + } + + return yield(Fragment{}, err) + } + } + + select { + case <-ctx.Done(): + return ctx.Err() + default: + wg.Wait() + return nil + } +} + +// NewRemoteInfo builds a new RemoteInfo for generating finding links +func NewRemoteInfo(platform scm.Platform, source string) *RemoteInfo { + return NewRemoteInfoContext(context.Background(), platform, source) +} + +// NewRemoteInfoContext is the same as NewRemoteInfo but supports passing in a +// context to use for timeouts +func NewRemoteInfoContext(ctx context.Context, platform scm.Platform, source string) *RemoteInfo { + if platform == scm.NoPlatform { + return &RemoteInfo{Platform: platform} + } + + remoteUrl, err := getRemoteUrl(ctx, source) + if err != nil { + if strings.Contains(err.Error(), "No remote configured") { + logging.Debug().Msg("skipping finding links: repository has no configured remote.") + platform = scm.NoPlatform + } else { + logging.Error().Err(err).Msg("skipping finding links: unable to parse remote URL") + } + goto End + } + + if platform == scm.UnknownPlatform { + platform = platformFromHost(remoteUrl) + if platform == scm.UnknownPlatform { + logging.Info(). + Str("host", remoteUrl.Hostname()). + Msg("Unknown SCM platform. Use --platform to include links in findings.") + } else { + logging.Debug(). + Str("host", remoteUrl.Hostname()). + Str("platform", platform.String()). + Msg("SCM platform parsed from host") + } + } + +End: + var rUrl string + if remoteUrl != nil { + rUrl = remoteUrl.String() + } + return &RemoteInfo{ + Platform: platform, + Url: rUrl, + } +} + +var sshUrlpat = regexp.MustCompile(`^git@([a-zA-Z0-9.-]+):(?:\d{1,5}/)?([\w/.-]+?)(?:\.git)?$`) + +func getRemoteUrl(ctx context.Context, source string) (*url.URL, error) { + // This will return the first remote — typically, "origin". + cmd := exec.CommandContext(ctx, "git", "ls-remote", "--quiet", "--get-url") + if source != "." { + cmd.Dir = source + } + + stdout, err := cmd.Output() + if err != nil { + var exitError *exec.ExitError + if errors.As(err, &exitError) { + return nil, fmt.Errorf("command failed (%d): %w, stderr: %s", exitError.ExitCode(), err, string(bytes.TrimSpace(exitError.Stderr))) + } + return nil, err + } + + remoteUrl := string(bytes.TrimSpace(stdout)) + if matches := sshUrlpat.FindStringSubmatch(remoteUrl); matches != nil { + remoteUrl = fmt.Sprintf("https://%s/%s", matches[1], matches[2]) + } + remoteUrl = strings.TrimSuffix(remoteUrl, ".git") + + parsedUrl, err := url.Parse(remoteUrl) + if err != nil { + return nil, fmt.Errorf("unable to parse remote URL: %w", err) + } + + // Remove any user info. + parsedUrl.User = nil + return parsedUrl, nil +} + +func platformFromHost(u *url.URL) scm.Platform { + switch strings.ToLower(u.Hostname()) { + case "github.com": + return scm.GitHubPlatform + case "gitlab.com": + return scm.GitLabPlatform + case "dev.azure.com", "visualstudio.com": + return scm.AzureDevOpsPlatform + case "gitea.com", "code.forgejo.org", "codeberg.org": + return scm.GiteaPlatform + case "bitbucket.org": + return scm.BitbucketPlatform + default: + return scm.UnknownPlatform + } +} diff --git a/sources/git_test.go b/sources/git_test.go new file mode 100644 index 0000000..7553a2a --- /dev/null +++ b/sources/git_test.go @@ -0,0 +1,158 @@ +package sources + +// TODO: commenting out this test for now because it's flaky. Alternatives to consider to get this working: +// -- use `git stash` instead of `restore()` + +// 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 func() { +// if err = moveDotGit(".git", "dotGit"); err != nil { +// t.Fatal(err) +// } +// }() + +// 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 func() { +// if err = moveDotGit(".git", "dotGit"); err != nil { +// t.Fatal(err) +// } +// }() + +// 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 +// } diff --git a/sources/source.go b/sources/source.go new file mode 100644 index 0000000..28e12a4 --- /dev/null +++ b/sources/source.go @@ -0,0 +1,16 @@ +package sources + +import ( + "context" +) + +// FragmentsFunc is the type of function called by Fragments to yield the next +// fragment +type FragmentsFunc func(fragment Fragment, err error) error + +// Source is a thing that can yield fragments +type Source interface { + // Fragments provides a filepath.WalkDir like interface for scanning the + // fragments in the source + Fragments(ctx context.Context, yield FragmentsFunc) error +} diff --git a/testdata/archives/files.7z b/testdata/archives/files.7z new file mode 100644 index 0000000..6dff5bf Binary files /dev/null and b/testdata/archives/files.7z differ diff --git a/testdata/archives/files.tar b/testdata/archives/files.tar new file mode 100644 index 0000000..5594d7f Binary files /dev/null and b/testdata/archives/files.tar differ diff --git a/testdata/archives/files.tar.xz b/testdata/archives/files.tar.xz new file mode 100644 index 0000000..7d99d6e Binary files /dev/null and b/testdata/archives/files.tar.xz differ diff --git a/testdata/archives/files.tar.zst b/testdata/archives/files.tar.zst new file mode 100644 index 0000000..c628f55 Binary files /dev/null and b/testdata/archives/files.tar.zst differ diff --git a/testdata/archives/files.zip b/testdata/archives/files.zip new file mode 100644 index 0000000..d2bd45c Binary files /dev/null and b/testdata/archives/files.zip differ diff --git a/testdata/archives/files/.env.prod b/testdata/archives/files/.env.prod new file mode 100644 index 0000000..472c30e --- /dev/null +++ b/testdata/archives/files/.env.prod @@ -0,0 +1,6 @@ +DB_HOST=example.com +DB_PORT=443 +DB_USERNAME=postgres +DB_PASSWORD=8ae31cacf141669ddfb5da +DB_NAME=best_db +DB_SSL=true \ No newline at end of file diff --git a/testdata/archives/files/.gitleaksignore b/testdata/archives/files/.gitleaksignore new file mode 100644 index 0000000..981ab84 --- /dev/null +++ b/testdata/archives/files/.gitleaksignore @@ -0,0 +1 @@ +../testdata/repos/nogit/api.go:aws-access-key:20 diff --git a/testdata/archives/files/api.go b/testdata/archives/files/api.go new file mode 100644 index 0000000..acbef43 --- /dev/null +++ b/testdata/archives/files/api.go @@ -0,0 +1,24 @@ +package main + +import "fmt" + +func main() { + + var a = "initial" + fmt.Println(a) + + var b, c int = 1, 2 + fmt.Println(b, c) + + var d = true + fmt.Println(d) + + var e int + fmt.Println(e) + + // opps I added a secret at line 20 + awsToken := "AKIALALEMEL33243OLIA" + + f := "apple" + fmt.Println(f) +} diff --git a/testdata/archives/files/main.go b/testdata/archives/files/main.go new file mode 100644 index 0000000..acbef43 --- /dev/null +++ b/testdata/archives/files/main.go @@ -0,0 +1,24 @@ +package main + +import "fmt" + +func main() { + + var a = "initial" + fmt.Println(a) + + var b, c int = 1, 2 + fmt.Println(b, c) + + var d = true + fmt.Println(d) + + var e int + fmt.Println(e) + + // opps I added a secret at line 20 + awsToken := "AKIALALEMEL33243OLIA" + + f := "apple" + fmt.Println(f) +} diff --git a/testdata/archives/files/main.go.gz b/testdata/archives/files/main.go.gz new file mode 100644 index 0000000..d72b673 Binary files /dev/null and b/testdata/archives/files/main.go.gz differ diff --git a/testdata/archives/files/main.go.xz b/testdata/archives/files/main.go.xz new file mode 100644 index 0000000..0ea15cd Binary files /dev/null and b/testdata/archives/files/main.go.xz differ diff --git a/testdata/archives/files/main.go.zst b/testdata/archives/files/main.go.zst new file mode 100644 index 0000000..80917d9 Binary files /dev/null and b/testdata/archives/files/main.go.zst differ diff --git a/testdata/archives/nested.tar.gz b/testdata/archives/nested.tar.gz new file mode 100644 index 0000000..1872b69 Binary files /dev/null and b/testdata/archives/nested.tar.gz differ diff --git a/testdata/baseline/baseline.csv b/testdata/baseline/baseline.csv new file mode 100644 index 0000000..d3f9537 --- /dev/null +++ b/testdata/baseline/baseline.csv @@ -0,0 +1,2 @@ +RuleID,Commit,File,Secret,Match,StartLine,EndLine,StartColumn,EndColumn,Author,Message,Date,Email,Fingerprint +1,b,c,f,s,m,s,e,s,e,a,m,f,r,f \ No newline at end of file diff --git a/testdata/baseline/baseline.json b/testdata/baseline/baseline.json new file mode 100644 index 0000000..3a4c542 --- /dev/null +++ b/testdata/baseline/baseline.json @@ -0,0 +1,40 @@ +[ + { + "Description": "PyPI upload token", + "StartLine": 32, + "EndLine": 32, + "StartColumn": 21, + "EndColumn": 106, + "Match": "************************", + "Secret": "************************", + "File": "detect/detect_test.go", + "Commit": "9326f35380636bcbe61e94b0584d1618c4b5c2c2", + "Entropy": 1.9606875, + "Author": "****", + "Email": "****", + "Date": "2022-03-07T14:33:06Z", + "Message": "Escape - character in regex character groups (#802)\n\n* fix char escape\n\n* add test\n\n* fix verbosity in make test", + "Tags": [], + "RuleID": "pypi-upload-token", + "Fingerprint": "9326f35380636bcbe61e94b0584d1618c4b5c2c2:detect/detect_test.go:pypi-upload-token:32" + }, + { + "Description": "PyPI upload token", + "StartLine": 33, + "EndLine": 33, + "StartColumn": 21, + "EndColumn": 106, + "Match": "************************", + "Secret": "************************", + "File": "detect/detect_test.go", + "Commit": "9326f35380636bcbe61e94b0584d1618c4b5c2c2", + "Entropy": 1.9606875, + "Author": "****", + "Email": "****", + "Date": "2022-03-07T14:33:06Z", + "Message": "Escape - character in regex character groups (#802)\n\n* fix char escape\n\n* add test\n\n* fix verbosity in make test", + "Tags": [], + "RuleID": "pypi-upload-token", + "Fingerprint": "9326f35380636bcbe61e94b0584d1618c4b5c2c2:detect/detect_test.go:pypi-upload-token:33" + } +] diff --git a/testdata/baseline/baseline.sarif b/testdata/baseline/baseline.sarif new file mode 100644 index 0000000..b2f8489 --- /dev/null +++ b/testdata/baseline/baseline.sarif @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "version": "2.1.0", + "runs": [ + ] +} diff --git a/testdata/config/archives.toml b/testdata/config/archives.toml new file mode 100644 index 0000000..d03c537 --- /dev/null +++ b/testdata/config/archives.toml @@ -0,0 +1,21 @@ +title = "gitleaks config" +# https://learnxinyminutes.com/docs/toml/ for toml reference + +[[rules]] +id = "aws-access-key" +description = "AWS Access Key" +regex = '''(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}''' +tags = ["key", "AWS"] + +# Here to confirm that allowlists work in archives +[[rules]] +id = 'password' +description = "Find the DB password in .env.prod" +path = '''\.env\.prod$''' +regex = '''(?i)password=([^\s]+)''' + +# Now ignore it to confirm allowlists work +[[allowlists]] +paths = [ + '''\.env\.prod$''', +] diff --git a/testdata/config/composite.toml b/testdata/config/composite.toml new file mode 100644 index 0000000..d74b237 --- /dev/null +++ b/testdata/config/composite.toml @@ -0,0 +1,14 @@ +title = "Fragment level composite rule" + +[[rules]] +id = "primary-rule" +description = "Primary rule" +regex = 'password\s*=\s*"([^"]+)"' +[[rules.required]] +id = "username-rule" + +[[rules]] +id = "username-rule" +description = "Username rule" +regex = 'username\s*=\s*"([^"]+)"' +skipReport = true \ No newline at end of file diff --git a/testdata/config/encoded.toml b/testdata/config/encoded.toml new file mode 100644 index 0000000..9b05886 --- /dev/null +++ b/testdata/config/encoded.toml @@ -0,0 +1,126 @@ +# We want to be able to find this key regardless if it's b64 encoded or not +[[rules]] + id = 'private-key' + description = 'Private Key' + regex = '''(?i)-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----[\s\S-]*?-----END[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----''' + tags = ['key', 'private'] + keywords = [ + '-----begin', + ] + +# This exists to test what would happen if a normal rule matched something that +# also gets decoded. We don't want to break anyone's existing rules that might +# be looking for specific segments of b64 encoded data. +[[rules]] + id = 'b64-encoded-private-key' + description = 'Private Key' + regex = '''(?:LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0t|0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tL|tLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS)[a-zA-Z0-9+\/]+={0,3}''' + tags = ['key', 'private'] + keywords = [ + 'ls0tls1crudjtibquklwqvrfietfws0tls0t', + '0tls0tqkvhsu4gufjjvkfursblrvktls0tl', + 'tls0tlujfr0loifbssvzbveugs0vzls0tls', + ] + + +[[rules]] + id = 'aws-iam-unique-identifier' + description = 'AWS IAM Unique Identifier' + # The funky not group at the beginning consists of ascii ranges + regex = '''(?:^|[^!$-&\(-9<>-~])((?:A3T[A-Z0-9]|ACCA|ABIA|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16})\b''' + tags = ['aws', 'identifier'] + entropy = 3.2 + secretGroup = 1 + keywords = [ + 'a3t', + 'abia', + 'acca', + 'agpa', + 'aida', + 'aipa', + 'akia', + 'anpa', + 'anva', + 'aroa', + 'asia', + ] + +[[rules]] + id = 'aws-secret-access-key' + description = 'AWS Secret Access Key' + regex = '''(?i)aws[\w\-]{0,32}[\'\"]?\s*?[:=\(]\s*?[\'\"]?([a-z0-9\/+]{40})\b''' + tags = ['aws', 'secret'] + entropy = 4 + secretGroup = 1 + keywords = [ + 'aws', + ] + +[[rules]] + # Use a small one for making sure things shifting around are kept up with + # appropriately + id = 'small-secret' + description = 'Small Secret' + regex = '''\bsmall-secret\b''' + tags = ['small', 'secret'] + +[[rules]] + # When the example value is decoded this will overlap and this is here to + # test that the location information is reported accurately when the match + # goes outside the bounds of the encoded value + id = 'overlapping' + description = 'Overlapping' + regex = '''secret=(decoded-secret-value\w*)''' + tags = ['overlapping'] + secretGroup = 1 + +# -----BEGIN REGEX TARGET DECODED MATCH PATTERNS----- +[[rules]] + id = 'decoded-password-dont-ignore' + description = 'Make sure this would be detected with no allowlist' + regex = '''password\s*=\s*\"([^\"]+please-ignore-me[^\"]+)\"''' + tags = ['decode-ignore'] + secretGroup = 1 + +[[rules]] + id = 'decoded-password-ignore-secret' + description = 'Test ignore on decoded secrets: regexTarget = "secret"' + regex = '''password\s*=\s*\"([^\"]+please-ignore-me[^\"]+)\"''' + tags = ['decode-ignore'] + secretGroup = 1 + + [[rules.allowlists]] + regexTarget = 'secret' + regexes = [ + # The decoded segment that we are testing against + 'please-ignore-me', + ] + +[[rules]] + id = 'decoded-password-ignore-match' + description = 'Test ignore on decoded secrets: regexTarget = "match"' + regex = '''password\s*=\s*\"([^\"]+please-ignore-me[^\"]+)\"''' + tags = ['decode-ignore'] + secretGroup = 1 + + [[rules.allowlists]] + regexTarget = 'match' + regexes = [ + # The decoded segment that we are testing against + 'please-ignore-me', + ] + +[[rules]] + id = 'decoded-password-ignore-line' + description = 'Test ignore on decoded secrets: regexTarget = "line"' + regex = '''password\s*=\s*\"([^\"]+please-ignore-me[^\"]+)\"''' + tags = ['decode-ignore'] + secretGroup = 1 + + [[rules.allowlists]] + regexTarget = 'line' + regexes = [ + # The decoded segment that we are testing against + 'please-ignore-me', + ] +# -----END REGEX TARGET DECODED MATCH PATTERNS----- diff --git a/testdata/config/generic.toml b/testdata/config/generic.toml new file mode 100644 index 0000000..b5d7445 --- /dev/null +++ b/testdata/config/generic.toml @@ -0,0 +1,10 @@ +title = "gitleaks config" + +[[rules]] +id = "generic-api-key" +description = "Generic API Key" +regex = '''(?i)(?:key|api|token|secret|client|passwd|password|auth|access)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:{1,3}=|\|\|:|<=|=>|:|\?=)(?:'|\"|\s|=|\x60){0,5}([0-9a-z\-_.=]{10,150})(?:['|\"|\n|\r|\s|\x60|;]|$)''' +entropy = 3.5 +keywords = [ + "key","api","token","secret","client","passwd","password","auth","access", +] diff --git a/testdata/config/generic_with_py_path.toml b/testdata/config/generic_with_py_path.toml new file mode 100644 index 0000000..8c19fcd --- /dev/null +++ b/testdata/config/generic_with_py_path.toml @@ -0,0 +1,35 @@ +title = "gitleaks config" + +[[rules]] +id = "generic-api-key" +description = "Generic API Key" +path = '''.py''' +regex = '''(?i)((key|api|token|secret|password)[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9a-zA-Z\-_=]{8,64})['\"]''' +secretGroup = 4 +entropy = 3.7 + +[allowlist] +description = "global allow lists" +paths = [ + '''gitleaks.toml''', + '''(.*?)(jpg|gif|doc|pdf|bin|svg|socket)$''', + '''(go.mod|go.sum)$''' +] +regexes = [ + '''219-09-9999''', + '''078-05-1120''', + '''(9[0-9]{2}|666)-\d{2}-\d{4}''', + '''process''', + '''getenv''', + '''\.env''', + '''env\(''', + '''env\.''', + '''setting''', + '''load''', + '''token''', + '''password''', + '''secret''', + '''api\_key''', + '''apikey''', + '''api\-key''', +] diff --git a/testdata/config/invalid/allowlist_global_empty.toml b/testdata/config/invalid/allowlist_global_empty.toml new file mode 100644 index 0000000..1caaf91 --- /dev/null +++ b/testdata/config/invalid/allowlist_global_empty.toml @@ -0,0 +1,2 @@ + +[[allowlists]] diff --git a/testdata/config/invalid/allowlist_global_old_and_new.toml b/testdata/config/invalid/allowlist_global_old_and_new.toml new file mode 100644 index 0000000..18b542e --- /dev/null +++ b/testdata/config/invalid/allowlist_global_old_and_new.toml @@ -0,0 +1,4 @@ +[allowlist] +regexes = ['''123'''] +[[allowlists]] +regexes = ['''456'''] diff --git a/testdata/config/invalid/allowlist_global_regextarget.toml b/testdata/config/invalid/allowlist_global_regextarget.toml new file mode 100644 index 0000000..2eed836 --- /dev/null +++ b/testdata/config/invalid/allowlist_global_regextarget.toml @@ -0,0 +1,3 @@ +[[allowlists]] +regexTarget = "mtach" +regexes = ['''456'''] diff --git a/testdata/config/invalid/allowlist_global_target_rule_id.toml b/testdata/config/invalid/allowlist_global_target_rule_id.toml new file mode 100644 index 0000000..a329d9c --- /dev/null +++ b/testdata/config/invalid/allowlist_global_target_rule_id.toml @@ -0,0 +1,7 @@ +[[rules]] +id = "github-app-token" +regex = '''(?:ghu|ghs)_[0-9a-zA-Z]{36}''' + +[[allowlists]] +targetRules = ["github-app-token", "github-pat"] +regexes = ['''.*fake.*'''] diff --git a/testdata/config/invalid/allowlist_rule_empty.toml b/testdata/config/invalid/allowlist_rule_empty.toml new file mode 100644 index 0000000..4c7187a --- /dev/null +++ b/testdata/config/invalid/allowlist_rule_empty.toml @@ -0,0 +1,4 @@ +[[rules]] +id = "example" +regex = '''example\d+''' + [[rules.allowlists]] diff --git a/testdata/config/invalid/allowlist_rule_old_and_new.toml b/testdata/config/invalid/allowlist_rule_old_and_new.toml new file mode 100644 index 0000000..dd4ecb2 --- /dev/null +++ b/testdata/config/invalid/allowlist_rule_old_and_new.toml @@ -0,0 +1,7 @@ +[[rules]] +id = "example" +regex = '''example\d+''' + [rules.allowlist] + regexes = ['''123'''] + [[rules.allowlists]] + regexes = ['''456'''] diff --git a/testdata/config/invalid/allowlist_rule_regextarget.toml b/testdata/config/invalid/allowlist_rule_regextarget.toml new file mode 100644 index 0000000..41ea713 --- /dev/null +++ b/testdata/config/invalid/allowlist_rule_regextarget.toml @@ -0,0 +1,6 @@ +[[rules]] +id = "example" +regex = '''example\d+''' + [[rules.allowlists]] + regexTarget = "mtach" + regexes = ['''456'''] diff --git a/testdata/config/invalid/extend_invalid_base.toml b/testdata/config/invalid/extend_invalid_base.toml new file mode 100644 index 0000000..0c78aff --- /dev/null +++ b/testdata/config/invalid/extend_invalid_base.toml @@ -0,0 +1,10 @@ +title = "gitleaks extended 1" + +[extend] +path="../testdata/config/invalid/does_not_exist.toml" + +[[rules]] + description = "AWS Access Key" + id = "aws-access-key" + regex = '''(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}''' + tags = ["key", "AWS"] diff --git a/testdata/config/invalid/extend_invalid_ruleid.toml b/testdata/config/invalid/extend_invalid_ruleid.toml new file mode 100644 index 0000000..86a89bb --- /dev/null +++ b/testdata/config/invalid/extend_invalid_ruleid.toml @@ -0,0 +1,7 @@ +[extend] +useDefault = true + +[[rules]] + +[[rules.allowlists]] +paths = ['^.*\.(xml|log|json)$'] diff --git a/testdata/config/invalid/rule_bad_entropy_group.toml b/testdata/config/invalid/rule_bad_entropy_group.toml new file mode 100755 index 0000000..8e4d1c2 --- /dev/null +++ b/testdata/config/invalid/rule_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})['\"]''' +secretGroup = 5 +entropy = 3.5 diff --git a/testdata/config/invalid/rule_missing_id.toml b/testdata/config/invalid/rule_missing_id.toml new file mode 100755 index 0000000..a43fd53 --- /dev/null +++ b/testdata/config/invalid/rule_missing_id.toml @@ -0,0 +1,7 @@ +title = "gitleaks config" + +[[rules]] +description = "Discord API key" +regex = '''(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{64})['\"]''' +secretGroup = 5 +entropy = 3.5 diff --git a/testdata/config/invalid/rule_no_regex_or_path.toml b/testdata/config/invalid/rule_no_regex_or_path.toml new file mode 100755 index 0000000..5c41985 --- /dev/null +++ b/testdata/config/invalid/rule_no_regex_or_path.toml @@ -0,0 +1,5 @@ +title = "gitleaks config" + +[[rules]] +id = 'discord-api-key' +description = "Discord API key" diff --git a/testdata/config/simple.toml b/testdata/config/simple.toml new file mode 100644 index 0000000..2bbdaad --- /dev/null +++ b/testdata/config/simple.toml @@ -0,0 +1,227 @@ +title = "gitleaks config" +# https://learnxinyminutes.com/docs/toml/ for toml reference + +[[rules]] + description = "1Password Secret Key" + id = "1password-secret-key" + regex = '''A3-[A-Z0-9]{6}-(?:(?:[A-Z0-9]{11})|(?:[A-Z0-9]{6}-[A-Z0-9]{5}))-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}''' + tags = ["1Password"] + +[[rules]] + description = "AWS Access Key" + id = "aws-access-key" + regex = '''(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}''' + tags = ["key", "AWS"] + +[[rules]] + description = "AWS Secret Key" + id = "aws-secret-key" + regex = '''(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}''' + tags = ["key", "AWS"] + +[[rules]] + description = "AWS MWS key" + id = "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" + id = "facebook-secret-key" + regex = '''(?i)(facebook|fb)(.{0,20})?(?-i)['\"][0-9a-f]{32}['\"]''' + tags = ["key", "Facebook"] + +[[rules]] + description = "Facebook Client ID" + id = "facebook-client-id" + regex = '''(?i)(facebook|fb)(.{0,20})?['\"][0-9]{13,17}['\"]''' + tags = ["key", "Facebook"] + +[[rules]] + description = "Twitter Secret Key" + id = "twitter-secret-key" + regex = '''(?i)twitter(.{0,20})?['\"][0-9a-z]{35,44}['\"]''' + tags = ["key", "Twitter"] + +[[rules]] + description = "Twitter Client ID" + id = "twitter-client-id" + regex = '''(?i)twitter(.{0,20})?['\"][0-9a-z]{18,25}['\"]''' + tags = ["client", "Twitter"] + +[[rules]] + description = "Github Personal Access Token" + id = "github-pat" + regex = '''ghp_[0-9a-zA-Z]{36}''' + tags = ["key", "Github"] +[[rules]] + description = "Github OAuth Access Token" + id = "github-oauth" + regex = '''gho_[0-9a-zA-Z]{36}''' + tags = ["key", "Github"] +[[rules]] + id = "github-app" + description = "Github App Token" + regex = '''(ghu|ghs)_[0-9a-zA-Z]{36}''' + tags = ["key", "Github"] +[[rules]] + id = "github-refresh" + description = "Github Refresh Token" + regex = '''ghr_[0-9a-zA-Z]{76}''' + tags = ["key", "Github"] + +[[rules]] + id = "linkedin-client" + description = "LinkedIn Client ID" + regex = '''(?i)linkedin(.{0,20})?(?-i)[0-9a-z]{12}''' + tags = ["client", "LinkedIn"] + +[[rules]] + id = "linkedin-secret" + description = "LinkedIn Secret Key" + regex = '''(?i)linkedin(.{0,20})?[0-9a-z]{16}''' + tags = ["secret", "LinkedIn"] + +[[rules]] + id = "slack" + description = "Slack" + regex = '''xox[baprs]-(?:[0-9a-zA-Z]{10,48})?''' + tags = ["key", "Slack"] + +[[rules]] + id = "apkey" + description = "Asymmetric Private Key" + regex = '''-----BEGIN (?:(?:EC|PGP|DSA|RSA|OPENSSH) )?PRIVATE KEY(?: BLOCK)?-----''' + tags = ["key", "AsymmetricPrivateKey"] + +[[rules]] + id = "google" + description = "Google API key" + regex = '''AIza[0-9A-Za-z\-_]{35}''' + tags = ["key", "Google"] + +[[rules]] + id = "google" + description = "Google (GCP) Service Account" + regex = '''"type": "service_account"''' + tags = ["key", "Google"] + +[[rules]] + id = "heroku" + 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]] + id = "mailchimp" + description = "MailChimp API key" + regex = '''(?i)(mailchimp|mc)(.{0,20})?[0-9a-f]{32}-us[0-9]{1,2}''' + tags = ["key", "Mailchimp"] + +[[rules]] + id = "mailgun" + description = "Mailgun API key" + regex = '''((?i)(mailgun|mg)(.{0,20})?)?key-[0-9a-z]{32}''' + tags = ["key", "Mailgun"] + +[[rules]] + id = "paypal" + description = "PayPal Braintree access token" + regex = '''access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}''' + tags = ["key", "Paypal"] + +[[rules]] + id = "piacatic" + description = "Picatic API key" + regex = '''sk_live_[0-9a-z]{32}''' + tags = ["key", "Picatic"] + +[[rules]] + id = "sendgrid" + description = "SendGrid API Key" + regex = '''SG\.[\w_]{16,32}\.[\w_]{16,64}''' + tags = ["key", "SendGrid"] + +[[rules]] + description = "Sidekiq Secret" + id = "sidekiq-secret" + regex = '''(?i)(?:BUNDLE_ENTERPRISE__CONTRIBSYS__COM|BUNDLE_GEMS__CONTRIBSYS__COM)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{8}:[a-f0-9]{8})(?:['|\"|\n|\r|\s|\x60|;]|$)''' + secretGroup = 1 + keywords = [ + "bundle_enterprise__contribsys__com","bundle_gems__contribsys__com", + ] + +[[rules]] + description = "Sidekiq Sensitive URL" + id = "sidekiq-sensitive-url" + regex = '''(?i)\b(http(?:s??):\/\/)([a-f0-9]{8}:[a-f0-9]{8})@(?:gems.contribsys.com|enterprise.contribsys.com)(?:[\/|\#|\?|:]|$)''' + secretGroup = 2 + keywords = [ + "gems.contribsys.com","enterprise.contribsys.com", + ] + +[[rules]] + id = "slack-webhook" + 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]] + id = "stripe" + description = "Stripe API key" + regex = '''(?i)stripe(.{0,20})?[sr]k_live_[0-9a-zA-Z]{24}''' + tags = ["key", "Stripe"] + +[[rules]] + id = "square" + description = "Square access token" + regex = '''sq0atp-[0-9A-Za-z\-_]{22}''' + tags = ["key", "square"] + +[[rules]] + id = "square-oauth" + description = "Square OAuth secret" + regex = '''sq0csp-[0-9A-Za-z\-_]{43}''' + tags = ["key", "square"] + +[[rules]] + id = "twilio" + description = "Twilio API key" + regex = '''(?i)twilio(.{0,20})?SK[0-9a-f]{32}''' + tags = ["key", "twilio"] + +[[rules]] + id = "dynatrace" + description = "Dynatrace ttoken" + regex = '''dt0[a-zA-Z]{1}[0-9]{2}\.[A-Z0-9]{24}\.[A-Z0-9]{64}''' + tags = ["key", "Dynatrace"] + +[[rules]] + id = "shopify" + description = "Shopify shared secret" + regex = '''shpss_[a-fA-F0-9]{32}''' + tags = ["key", "Shopify"] + +[[rules]] + id = "shopify-access" + description = "Shopify access token" + regex = '''shpat_[a-fA-F0-9]{32}''' + tags = ["key", "Shopify"] + +[[rules]] + id = "shopify-custom" + description = "Shopify custom app access token" + regex = '''shpca_[a-fA-F0-9]{32}''' + tags = ["key", "Shopify"] + +[[rules]] + id = "shopify-private" + description = "Shopify private app access token" + regex = '''shppa_[a-fA-F0-9]{32}''' + tags = ["key", "Shopify"] + +[[rules]] + id = "pypi" + description = "PyPI upload token" + regex = '''pypi-AgEIcHlwaS5vcmc[A-Za-z0-9-_]{50,1000}''' + tags = ["key", "pypi"] diff --git a/testdata/config/valid/allowlist_global_multiple.toml b/testdata/config/valid/allowlist_global_multiple.toml new file mode 100644 index 0000000..8c8cd44 --- /dev/null +++ b/testdata/config/valid/allowlist_global_multiple.toml @@ -0,0 +1,10 @@ +[[rules]] +id = "test" +regex = '''token = "(.+)"''' + +[[allowlists]] +regexes = ["^changeit$"] +[[allowlists]] +condition = "AND" +paths = ["^node_modules/.*"] +stopwords = ["mock"] diff --git a/testdata/config/valid/allowlist_global_old_compat.toml b/testdata/config/valid/allowlist_global_old_compat.toml new file mode 100644 index 0000000..b12a5c1 --- /dev/null +++ b/testdata/config/valid/allowlist_global_old_compat.toml @@ -0,0 +1,2 @@ +[allowlist] +stopwords = ["0989c462-69c9-49fa-b7d2-30dc5c576a97"] diff --git a/testdata/config/valid/allowlist_global_regex.toml b/testdata/config/valid/allowlist_global_regex.toml new file mode 100644 index 0000000..107e7bb --- /dev/null +++ b/testdata/config/valid/allowlist_global_regex.toml @@ -0,0 +1,2 @@ +[allowlist] + regexes = ['''AKIALALEM.L33243OLIA'''] diff --git a/testdata/config/valid/allowlist_global_target_rules.toml b/testdata/config/valid/allowlist_global_target_rules.toml new file mode 100644 index 0000000..111719c --- /dev/null +++ b/testdata/config/valid/allowlist_global_target_rules.toml @@ -0,0 +1,20 @@ +[[rules]] +id = "github-app-token" +regex = '''(?:ghu|ghs)_[0-9a-zA-Z]{36}''' + +[[rules]] +id = "github-oauth" +regex = '''gho_[0-9a-zA-Z]{36}''' + +[[rules]] +id = "github-pat" +regex = '''ghp_[0-9a-zA-Z]{36}''' + + +[[allowlists]] +regexes = ['''.*fake.*'''] +[[allowlists]] +targetRules = ["github-app-token", "github-pat"] +paths = [ + '''(?:^|/)@octokit/auth-token/README\.md$''', +] diff --git a/testdata/config/valid/allowlist_rule_commit.toml b/testdata/config/valid/allowlist_rule_commit.toml new file mode 100644 index 0000000..ccdd540 --- /dev/null +++ b/testdata/config/valid/allowlist_rule_commit.toml @@ -0,0 +1,9 @@ +title = "simple config with allowlist for a specific commit" + +[[rules]] + description = "AWS Access Key" + id = "aws-access-key" + regex = '''(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}''' + tags = ["key", "AWS"] + [[rules.allowlists]] + commits = ['''allowthiscommit'''] diff --git a/testdata/config/valid/allowlist_rule_extend_default.toml b/testdata/config/valid/allowlist_rule_extend_default.toml new file mode 100644 index 0000000..4fe41d5 --- /dev/null +++ b/testdata/config/valid/allowlist_rule_extend_default.toml @@ -0,0 +1,11 @@ +# https://github.com/gitleaks/gitleaks/issues/1844 +[extend] +useDefault = true + +[[rules]] +id = "generic-api-key" +[[rules.allowlists]] +description = "Exclude a specific file from generic-api-key rule" +paths = [ + '''^path/to/your/problematic/file\.js$''' +] diff --git a/testdata/config/valid/allowlist_rule_old_compat.toml b/testdata/config/valid/allowlist_rule_old_compat.toml new file mode 100644 index 0000000..73247ee --- /dev/null +++ b/testdata/config/valid/allowlist_rule_old_compat.toml @@ -0,0 +1,5 @@ +[[rules]] + id = "example" + regex = '''example\d+''' + [rules.allowlist] + regexes = ['''123'''] diff --git a/testdata/config/valid/allowlist_rule_path.toml b/testdata/config/valid/allowlist_rule_path.toml new file mode 100644 index 0000000..bc616be --- /dev/null +++ b/testdata/config/valid/allowlist_rule_path.toml @@ -0,0 +1,9 @@ +title = "simple config with allowlist for .go files" + +[[rules]] + description = "AWS Access Key" + id = "aws-access-key" + regex = '''(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}''' + tags = ["key", "AWS"] + [[rules.allowlists]] + paths = ['''.go'''] diff --git a/testdata/config/valid/allowlist_rule_regex.toml b/testdata/config/valid/allowlist_rule_regex.toml new file mode 100644 index 0000000..e4a3bad --- /dev/null +++ b/testdata/config/valid/allowlist_rule_regex.toml @@ -0,0 +1,9 @@ +title = "simple config with allowlist for aws" + +[[rules]] + description = "AWS Access Key" + id = "aws-access-key" + regex = '''(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}''' + tags = ["key", "AWS"] + [[rules.allowlists]] + regexes = ['''AKIALALEMEL33243OLIA'''] diff --git a/testdata/config/valid/extend.toml b/testdata/config/valid/extend.toml new file mode 100644 index 0000000..208818d --- /dev/null +++ b/testdata/config/valid/extend.toml @@ -0,0 +1,8 @@ +[extend] +path="../testdata/config/valid/extend_base_1.toml" + +[[rules]] + description = "AWS Secret Key" + id = "aws-secret-key" + regex = '''(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}''' + tags = ["key", "AWS"] diff --git a/testdata/config/valid/extend_base_1.toml b/testdata/config/valid/extend_base_1.toml new file mode 100644 index 0000000..9cdd391 --- /dev/null +++ b/testdata/config/valid/extend_base_1.toml @@ -0,0 +1,10 @@ +title = "gitleaks extended 1" + +[extend] +path="../testdata/config/valid/extend_base_2.toml" + +[[rules]] + description = "AWS Access Key" + id = "aws-access-key" + regex = '''(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}''' + tags = ["key", "AWS"] diff --git a/testdata/config/valid/extend_base_2.toml b/testdata/config/valid/extend_base_2.toml new file mode 100644 index 0000000..7532c99 --- /dev/null +++ b/testdata/config/valid/extend_base_2.toml @@ -0,0 +1,10 @@ +title = "gitleaks extended 2" + +[extend] +path="../testdata/config/extend_3.toml" + +[[rules]] + description = "AWS Secret Key" + id = "aws-secret-key-again" + regex = '''(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}''' + tags = ["key", "AWS"] diff --git a/testdata/config/valid/extend_base_3.toml b/testdata/config/valid/extend_base_3.toml new file mode 100644 index 0000000..ed0dfd2 --- /dev/null +++ b/testdata/config/valid/extend_base_3.toml @@ -0,0 +1,9 @@ +title = "gitleaks extended 3" + +## This should not be loaded since we can only extend configs to a depth of 3 + +[[rules]] + id = "aws-secret-key-again-again" + description = "AWS Secret Key" + regex = '''(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}''' + tags = ["key", "AWS"] diff --git a/testdata/config/valid/extend_base_rule_including_keywords_with_attribute.toml b/testdata/config/valid/extend_base_rule_including_keywords_with_attribute.toml new file mode 100644 index 0000000..a7853dd --- /dev/null +++ b/testdata/config/valid/extend_base_rule_including_keywords_with_attribute.toml @@ -0,0 +1,8 @@ +title = "gitleaks extended 3" + +[extend] +path="../testdata/config/valid/extend_rule_keywords_base.toml" + +[[rules]] + id = "aws-secret-key-again-again" + description = "A new description" diff --git a/testdata/config/valid/extend_disabled.toml b/testdata/config/valid/extend_disabled.toml new file mode 100644 index 0000000..1b1735c --- /dev/null +++ b/testdata/config/valid/extend_disabled.toml @@ -0,0 +1,11 @@ +title = "gitleaks extend disable" + +[extend] +path = "../testdata/config/valid/extend_disabled_base.toml" +disabledRules = [ + 'custom-rule1' +] + +[[rules]] +id = "pypi-upload-token" +regex = '''pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\-_]{50,1000}''' diff --git a/testdata/config/valid/extend_disabled_base.toml b/testdata/config/valid/extend_disabled_base.toml new file mode 100644 index 0000000..d676b03 --- /dev/null +++ b/testdata/config/valid/extend_disabled_base.toml @@ -0,0 +1,11 @@ +title = "gitleaks extended 3" + + +[[rules]] +id = "aws-secret-key" +regex = '''(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}''' +tags = ["key", "AWS"] + +[[rules]] +id = "custom-rule1" +regex = '''[Cc]ustom!''' diff --git a/testdata/config/valid/extend_rule_allowlist_and.toml b/testdata/config/valid/extend_rule_allowlist_and.toml new file mode 100644 index 0000000..b3f8ab7 --- /dev/null +++ b/testdata/config/valid/extend_rule_allowlist_and.toml @@ -0,0 +1,14 @@ +title = "gitleaks extended 3" + +[extend] +path="../testdata/config/valid/extend_rule_allowlist_base.toml" + +[[rules]] + id = "aws-secret-key-again-again" +[[rules.allowlists]] + condition = "AND" + commits = ['''abcdefg1'''] + regexes = ['''foo.+bar'''] + regexTarget = "line" + paths = ['''ignore\.xaml'''] + stopwords = ['''example'''] diff --git a/testdata/config/valid/extend_rule_allowlist_base.toml b/testdata/config/valid/extend_rule_allowlist_base.toml new file mode 100644 index 0000000..7ac8d14 --- /dev/null +++ b/testdata/config/valid/extend_rule_allowlist_base.toml @@ -0,0 +1,11 @@ +title = "gitleaks extended 3" + +## This should not be loaded since we can only extend configs to a depth of 3 + +[[rules]] + id = "aws-secret-key-again-again" + description = "AWS Secret Key" + regex = '''(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}''' + tags = ["key", "AWS"] +[[rules.allowlists]] + stopwords = ["fake"] diff --git a/testdata/config/valid/extend_rule_allowlist_or.toml b/testdata/config/valid/extend_rule_allowlist_or.toml new file mode 100644 index 0000000..b7f9384 --- /dev/null +++ b/testdata/config/valid/extend_rule_allowlist_or.toml @@ -0,0 +1,13 @@ +title = "gitleaks extended 3" + +[extend] +path="../testdata/config/valid/extend_rule_allowlist_base.toml" + +[[rules]] + id = "aws-secret-key-again-again" +[[rules.allowlists]] + commits = ['''abcdefg1'''] + regexes = ['''foo.+bar'''] + regexTarget = "line" + paths = ['''ignore\.xaml'''] + stopwords = ['''example'''] diff --git a/testdata/config/valid/extend_rule_keywords_base.toml b/testdata/config/valid/extend_rule_keywords_base.toml new file mode 100644 index 0000000..6811d4a --- /dev/null +++ b/testdata/config/valid/extend_rule_keywords_base.toml @@ -0,0 +1,12 @@ +title = "gitleaks extended 3" + +## This should not be loaded since we can only extend configs to a depth of 3 + +[[rules]] + id = "aws-secret-key-again-again" + description = "AWS Secret Key" + regex = '''(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}''' + tags = ["key", "AWS"] + keywords = ["AWS"] +[[rules.allowlists]] + stopwords = ["fake"] diff --git a/testdata/config/valid/extend_rule_new.toml b/testdata/config/valid/extend_rule_new.toml new file mode 100644 index 0000000..e465b64 --- /dev/null +++ b/testdata/config/valid/extend_rule_new.toml @@ -0,0 +1,8 @@ +title = "gitleaks extended 3" + +[extend] +path="../testdata/config/valid/extend_rule_keywords_base.toml" + +[[rules]] + id = "aws-rule-that-is-not-in-base" + keywords = ["CMS"] diff --git a/testdata/config/valid/extend_rule_no_regexpath.toml b/testdata/config/valid/extend_rule_no_regexpath.toml new file mode 100644 index 0000000..af1f495 --- /dev/null +++ b/testdata/config/valid/extend_rule_no_regexpath.toml @@ -0,0 +1,8 @@ +[extend] +path="../testdata/config/valid/extend_base_3.toml" + +[[rules]] +id = "aws-secret-key-again-again" +[[rules.allowlists]] +description = "False positive. Keys used for colors match the rule, and should be excluded." +paths = ['''something.py'''] diff --git a/testdata/config/valid/extend_rule_override_description.toml b/testdata/config/valid/extend_rule_override_description.toml new file mode 100644 index 0000000..59af87f --- /dev/null +++ b/testdata/config/valid/extend_rule_override_description.toml @@ -0,0 +1,8 @@ +title = "override a built-in rule's description" + +[extend] +path = "../testdata/config/simple.toml" + +[[rules]] +id = "aws-access-key" +description = "Puppy Doggy" diff --git a/testdata/config/valid/extend_rule_override_entropy.toml b/testdata/config/valid/extend_rule_override_entropy.toml new file mode 100644 index 0000000..4703222 --- /dev/null +++ b/testdata/config/valid/extend_rule_override_entropy.toml @@ -0,0 +1,8 @@ +title = "override a built-in rule's entropy" + +[extend] +path = "../testdata/config/simple.toml" + +[[rules]] +id = "aws-access-key" +entropy = 999 diff --git a/testdata/config/valid/extend_rule_override_keywords.toml b/testdata/config/valid/extend_rule_override_keywords.toml new file mode 100644 index 0000000..51082a1 --- /dev/null +++ b/testdata/config/valid/extend_rule_override_keywords.toml @@ -0,0 +1,8 @@ +title = "override a built-in rule's keywords" + +[extend] +path = "../testdata/config/simple.toml" + +[[rules]] +id = "aws-access-key" +keywords = ["puppy"] diff --git a/testdata/config/valid/extend_rule_override_path.toml b/testdata/config/valid/extend_rule_override_path.toml new file mode 100644 index 0000000..dfc4535 --- /dev/null +++ b/testdata/config/valid/extend_rule_override_path.toml @@ -0,0 +1,8 @@ +title = "override a built-in rule's path" + +[extend] +path = "../testdata/config/simple.toml" + +[[rules]] +id = "aws-access-key" +path = '''(?:puppy)''' diff --git a/testdata/config/valid/extend_rule_override_regex.toml b/testdata/config/valid/extend_rule_override_regex.toml new file mode 100644 index 0000000..8cf0111 --- /dev/null +++ b/testdata/config/valid/extend_rule_override_regex.toml @@ -0,0 +1,8 @@ +title = "override a built-in rule's regex" + +[extend] +path = "../testdata/config/simple.toml" + +[[rules]] +id = "aws-access-key" +regex = '''(?:a)''' diff --git a/testdata/config/valid/extend_rule_override_secret_group.toml b/testdata/config/valid/extend_rule_override_secret_group.toml new file mode 100644 index 0000000..f2ddeaf --- /dev/null +++ b/testdata/config/valid/extend_rule_override_secret_group.toml @@ -0,0 +1,9 @@ +title = "override a built-in rule's secretGroup" + +[extend] +path = "../testdata/config/simple.toml" + +[[rules]] +id = "aws-access-key" +regex = '''(a)(a)''' +secretGroup = 2 diff --git a/testdata/config/valid/extend_rule_override_tags.toml b/testdata/config/valid/extend_rule_override_tags.toml new file mode 100644 index 0000000..73dde82 --- /dev/null +++ b/testdata/config/valid/extend_rule_override_tags.toml @@ -0,0 +1,8 @@ +title = "override a built-in rule's tags" + +[extend] +path = "../testdata/config/simple.toml" + +[[rules]] +id = "aws-access-key" +tags = ["puppy"] diff --git a/testdata/config/valid/rule_entropy_group.toml b/testdata/config/valid/rule_entropy_group.toml new file mode 100755 index 0000000..c463c93 --- /dev/null +++ b/testdata/config/valid/rule_entropy_group.toml @@ -0,0 +1,6 @@ +[[rules]] +id = "discord-api-key" +description = "Discord API key" +regex = '''(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{64})['\"]''' +secretGroup = 3 +entropy = 3.5 diff --git a/testdata/config/valid/rule_path_only.toml b/testdata/config/valid/rule_path_only.toml new file mode 100644 index 0000000..e782999 --- /dev/null +++ b/testdata/config/valid/rule_path_only.toml @@ -0,0 +1,4 @@ +[[rules]] +description = "Python Files" +id = "python-files-only" +path = '''.py''' diff --git a/testdata/config/valid/rule_regex_escaped_character_group.toml b/testdata/config/valid/rule_regex_escaped_character_group.toml new file mode 100644 index 0000000..4e1f99d --- /dev/null +++ b/testdata/config/valid/rule_regex_escaped_character_group.toml @@ -0,0 +1,5 @@ +[[rules]] + id = "pypi-upload-token" + description = "PyPI upload token" + regex = '''pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\-_]{50,1000}''' + tags = ["key", "pypi"] diff --git a/testdata/expected/git/small-branch-foo.txt b/testdata/expected/git/small-branch-foo.txt new file mode 100644 index 0000000..b3554c7 --- /dev/null +++ b/testdata/expected/git/small-branch-foo.txt @@ -0,0 +1,17 @@ +import ( + "fmt" + "os" +) + // seems safer + aws_token := os.Getenv("AWS_TOKEN") +package foo + +import "fmt" + +func Foo() { + fmt.Println("foo") + + // seems safe + aws_token := "AKIALALEMEL33243OLIA" + fmt.Println(aws_token) +} diff --git a/testdata/expected/git/small.txt b/testdata/expected/git/small.txt new file mode 100644 index 0000000..7235dd3 --- /dev/null +++ b/testdata/expected/git/small.txt @@ -0,0 +1,67 @@ +import ( + "fmt" + "os" +) + // seems safer + aws_token := os.Getenv("AWS_TOKEN") +package foo + +import "fmt" + +func Foo() { + fmt.Println("foo") + + // seems safe + aws_token := "AKIALALEMEL33243OLIA" + fmt.Println(aws_token) +} +package api + +import "fmt" + +func PrintHello() { + fmt.Println("hello") +} +import ( + "fmt" + "os" +) + var a = "initial" + fmt.Println(a) + var b, c int = 1, 2 + fmt.Println(b, c) + var d = true + fmt.Println(d) + var e int + fmt.Println(e) + // load secret via env + awsToken := os.Getenv("AWS_TOKEN") + + f := "apple" + fmt.Println(f) + + // opps I added a secret at line 20 + awsToken := "AKIALALEMEL33243OLIA" +package main + +import "fmt" + +func main() { + + var a = "initial" + fmt.Println(a) + + var b, c int = 1, 2 + fmt.Println(b, c) + + var d = true + fmt.Println(d) + + var e int + fmt.Println(e) + + f := "apple" + fmt.Println(f) +} +# test +This is a repo used for testing gitleaks diff --git a/testdata/expected/report/csv_simple.csv b/testdata/expected/report/csv_simple.csv new file mode 100644 index 0000000..71b68c9 --- /dev/null +++ b/testdata/expected/report/csv_simple.csv @@ -0,0 +1,2 @@ +RuleID,Commit,File,SymlinkFile,Secret,Match,StartLine,EndLine,StartColumn,EndColumn,Author,Message,Date,Email,Fingerprint,Tags +test-rule,0000000000000000,auth.py,,a secret,line containing secret,1,2,1,2,John Doe,opps,10-19-2003,johndoe@gmail.com,fingerprint,tag1 tag2 tag3 diff --git a/testdata/expected/report/empty.json b/testdata/expected/report/empty.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/testdata/expected/report/empty.json @@ -0,0 +1 @@ +[] diff --git a/testdata/expected/report/json_simple.json b/testdata/expected/report/json_simple.json new file mode 100644 index 0000000..21e3c60 --- /dev/null +++ b/testdata/expected/report/json_simple.json @@ -0,0 +1,22 @@ +[ + { + "RuleID": "test-rule", + "Description": "", + "StartLine": 1, + "EndLine": 2, + "StartColumn": 1, + "EndColumn": 2, + "Match": "line containing secret", + "Secret": "a secret", + "File": "auth.py", + "SymlinkFile": "", + "Commit": "0000000000000000", + "Entropy": 0, + "Author": "John Doe", + "Email": "johndoe@gmail.com", + "Date": "10-19-2003", + "Message": "opps", + "Tags": [], + "Fingerprint": "" + } +] diff --git a/testdata/expected/report/junit_empty.xml b/testdata/expected/report/junit_empty.xml new file mode 100644 index 0000000..45702c4 --- /dev/null +++ b/testdata/expected/report/junit_empty.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/testdata/expected/report/junit_simple.xml b/testdata/expected/report/junit_simple.xml new file mode 100644 index 0000000..1b39b39 --- /dev/null +++ b/testdata/expected/report/junit_simple.xml @@ -0,0 +1,11 @@ + + + + + { "RuleID": "test-rule", "Description": "Test Rule", "StartLine": 1, "EndLine": 2, "StartColumn": 1, "EndColumn": 2, "Match": "line containing secret", "Secret": "a secret", "File": "auth.py", "SymlinkFile": "", "Commit": "0000000000000000", "Entropy": 0, "Author": "John Doe", "Email": "johndoe@gmail.com", "Date": "10-19-2003", "Message": "opps", "Tags": [], "Fingerprint": "" } + + + { "RuleID": "test-rule", "Description": "Test Rule", "StartLine": 2, "EndLine": 3, "StartColumn": 1, "EndColumn": 2, "Match": "line containing secret", "Secret": "a secret", "File": "auth.py", "SymlinkFile": "", "Commit": "", "Entropy": 0, "Author": "", "Email": "", "Date": "", "Message": "", "Tags": [], "Fingerprint": "" } + + + \ No newline at end of file diff --git a/testdata/expected/report/sarif_simple.sarif b/testdata/expected/report/sarif_simple.sarif new file mode 100644 index 0000000..8197de3 --- /dev/null +++ b/testdata/expected/report/sarif_simple.sarif @@ -0,0 +1,69 @@ +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "gitleaks", + "semanticVersion": "v8.0.0", + "informationUri": "https://github.com/gitleaks/gitleaks", + "rules": [ + { + "id": "aws-access-key", + "shortDescription": { + "text": "AWS Access Key" + } + }, + { + "id": "pypi", + "shortDescription": { + "text": "PyPI upload token" + } + } + ] + } + }, + "results": [ + { + "message": { + "text": "test-rule has detected secret for file auth.py at commit 0000000000000000." + }, + "ruleId": "test-rule", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "auth.py" + }, + "region": { + "startLine": 1, + "startColumn": 1, + "endLine": 2, + "endColumn": 2, + "snippet": { + "text": "a secret" + } + } + } + } + ], + "partialFingerprints": { + "commitSha": "0000000000000000", + "email": "johndoe@gmail.com", + "author": "John Doe", + "date": "10-19-2003", + "commitMessage": "opps" + }, + "properties": { + "tags": [ + "tag1", + "tag2", + "tag3" + ] + } + } + ] + } + ] +} diff --git a/testdata/expected/report/template_jsonextra.json b/testdata/expected/report/template_jsonextra.json new file mode 100644 index 0000000..93fb746 --- /dev/null +++ b/testdata/expected/report/template_jsonextra.json @@ -0,0 +1,23 @@ +[ + { + "Description": "A test rule", + "StartLine": 1, + "EndLine": 2, + "StartColumn": 1, + "EndColumn": 2, + "Line": "whole line containing secret", + "Match": "line containing secret", + "Secret": "a secret", + "File": "auth.py", + "SymlinkFile": "", + "Commit": "0000000000000000", + "Entropy": 0, + "Author": "John Doe", + "Email": "johndoe@gmail.com", + "Date": "10-19-2003", + "Message": "opps", + "Tags": ["tag1","tag2","tag3"], + "RuleID": "test-rule", + "Fingerprint": "" + } +] diff --git a/testdata/expected/report/template_markdown.md b/testdata/expected/report/template_markdown.md new file mode 100644 index 0000000..4f791b2 --- /dev/null +++ b/testdata/expected/report/template_markdown.md @@ -0,0 +1,3 @@ +| File | Line | Secret | +|:-----|-----:|--------| +| auth.py | 1 | "a secret" | diff --git a/testdata/gitleaksignore/.windowspaths b/testdata/gitleaksignore/.windowspaths new file mode 100644 index 0000000..6c2c01b --- /dev/null +++ b/testdata/gitleaksignore/.windowspaths @@ -0,0 +1,4 @@ +foo\bar\gitleaks-false-positive.yaml:aws-access-token:4 +b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data\test_local_repo_three_leaks.json:aws-access-token:73 +foo/bar/gitleaks-false-positive.yaml:aws-access-token:5 +# comment diff --git a/testdata/report/jsonextra.tmpl b/testdata/report/jsonextra.tmpl new file mode 100644 index 0000000..33f38d3 --- /dev/null +++ b/testdata/report/jsonextra.tmpl @@ -0,0 +1,25 @@ +[{{ $lastFinding := (sub (len . ) 1) }} +{{- range $i, $finding := . }}{{with $finding}} + { + "Description": {{ quote .Description }}, + "StartLine": {{ .StartLine }}, + "EndLine": {{ .EndLine }}, + "StartColumn": {{ .StartColumn }}, + "EndColumn": {{ .EndColumn }}, + "Line": {{ quote .Line }}, + "Match": {{ quote .Match }}, + "Secret": {{ quote .Secret }}, + "File": "{{ .File }}", + "SymlinkFile": {{ quote .SymlinkFile }}, + "Commit": {{ quote .Commit }}, + "Entropy": {{ .Entropy }}, + "Author": {{ quote .Author }}, + "Email": {{ quote .Email }}, + "Date": {{ quote .Date }}, + "Message": {{ quote .Message }}, + "Tags": [{{ $lastTag := (sub (len .Tags ) 1) }}{{ range $j, $tag := .Tags }}{{ quote . }}{{ if ne $j $lastTag }},{{ end }}{{ end }}], + "RuleID": {{ quote .RuleID }}, + "Fingerprint": {{ quote .Fingerprint }} + }{{ if ne $i $lastFinding }},{{ end }} +{{- end}}{{ end }} +] diff --git a/testdata/report/markdown.tmpl b/testdata/report/markdown.tmpl new file mode 100644 index 0000000..9b064aa --- /dev/null +++ b/testdata/report/markdown.tmpl @@ -0,0 +1,5 @@ +| File | Line | Secret | +|:-----|-----:|--------| +{{ range . -}} +| {{ .File }} | {{ .StartLine }} | {{ quote .Secret }} | +{{ end -}} diff --git a/testdata/repos/archives/.gitleaksignore b/testdata/repos/archives/.gitleaksignore new file mode 100644 index 0000000..e69de29 diff --git a/testdata/repos/archives/README.md b/testdata/repos/archives/README.md new file mode 100644 index 0000000..ff3366d --- /dev/null +++ b/testdata/repos/archives/README.md @@ -0,0 +1,10 @@ +# Archives + +This repo has some archive files in its history! + +Commits: + +``` +07d2bd71800f1abf0421abe9bc4a83a6fdca1f68 nested.tar.gz +db8789716fc664dbce0ed2d492570e92abf717a5 main.go.zst +``` diff --git a/testdata/repos/archives/dotGit/HEAD b/testdata/repos/archives/dotGit/HEAD new file mode 100644 index 0000000..b870d82 --- /dev/null +++ b/testdata/repos/archives/dotGit/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/testdata/repos/archives/dotGit/ORIG_HEAD b/testdata/repos/archives/dotGit/ORIG_HEAD new file mode 100644 index 0000000..ba9f361 --- /dev/null +++ b/testdata/repos/archives/dotGit/ORIG_HEAD @@ -0,0 +1 @@ +15fa60c13dccec6add267b7baa065977a6cc748a diff --git a/testdata/repos/archives/dotGit/config b/testdata/repos/archives/dotGit/config new file mode 100644 index 0000000..afcd943 --- /dev/null +++ b/testdata/repos/archives/dotGit/config @@ -0,0 +1,13 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + +[remote "origin"] + url = git@github.com:gitleaks/test.git + fetch = +refs/heads/*:refs/remotes/origin/* + +[branch "main"] + remote = origin + merge = refs/heads/main diff --git a/testdata/repos/archives/dotGit/description b/testdata/repos/archives/dotGit/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/testdata/repos/archives/dotGit/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/testdata/repos/archives/dotGit/index b/testdata/repos/archives/dotGit/index new file mode 100644 index 0000000..6696469 Binary files /dev/null and b/testdata/repos/archives/dotGit/index differ diff --git a/testdata/repos/archives/dotGit/info/exclude b/testdata/repos/archives/dotGit/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/testdata/repos/archives/dotGit/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/testdata/repos/archives/dotGit/info/refs b/testdata/repos/archives/dotGit/info/refs new file mode 100644 index 0000000..0124828 --- /dev/null +++ b/testdata/repos/archives/dotGit/info/refs @@ -0,0 +1 @@ +15fa60c13dccec6add267b7baa065977a6cc748a refs/heads/main diff --git a/testdata/repos/archives/dotGit/objects/info/commit-graph b/testdata/repos/archives/dotGit/objects/info/commit-graph new file mode 100644 index 0000000..b9f985a Binary files /dev/null and b/testdata/repos/archives/dotGit/objects/info/commit-graph differ diff --git a/testdata/repos/archives/dotGit/objects/info/packs b/testdata/repos/archives/dotGit/objects/info/packs new file mode 100644 index 0000000..e4d4bc7 --- /dev/null +++ b/testdata/repos/archives/dotGit/objects/info/packs @@ -0,0 +1,2 @@ +P pack-9d774732f0e985d717a26e126e6574d089375b0d.pack + diff --git a/testdata/repos/archives/dotGit/objects/pack/pack-9d774732f0e985d717a26e126e6574d089375b0d.idx b/testdata/repos/archives/dotGit/objects/pack/pack-9d774732f0e985d717a26e126e6574d089375b0d.idx new file mode 100644 index 0000000..7b73441 Binary files /dev/null and b/testdata/repos/archives/dotGit/objects/pack/pack-9d774732f0e985d717a26e126e6574d089375b0d.idx differ diff --git a/testdata/repos/archives/dotGit/objects/pack/pack-9d774732f0e985d717a26e126e6574d089375b0d.pack b/testdata/repos/archives/dotGit/objects/pack/pack-9d774732f0e985d717a26e126e6574d089375b0d.pack new file mode 100644 index 0000000..3a00d86 Binary files /dev/null and b/testdata/repos/archives/dotGit/objects/pack/pack-9d774732f0e985d717a26e126e6574d089375b0d.pack differ diff --git a/testdata/repos/archives/dotGit/objects/pack/pack-9d774732f0e985d717a26e126e6574d089375b0d.rev b/testdata/repos/archives/dotGit/objects/pack/pack-9d774732f0e985d717a26e126e6574d089375b0d.rev new file mode 100644 index 0000000..73da054 Binary files /dev/null and b/testdata/repos/archives/dotGit/objects/pack/pack-9d774732f0e985d717a26e126e6574d089375b0d.rev differ diff --git a/testdata/repos/archives/dotGit/packed-refs b/testdata/repos/archives/dotGit/packed-refs new file mode 100644 index 0000000..1f922fa --- /dev/null +++ b/testdata/repos/archives/dotGit/packed-refs @@ -0,0 +1,2 @@ +# pack-refs with: peeled fully-peeled sorted +15fa60c13dccec6add267b7baa065977a6cc748a refs/heads/main diff --git a/testdata/repos/archives/dotGit/refs/.gitkeep b/testdata/repos/archives/dotGit/refs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/testdata/repos/archives/main.go.zst b/testdata/repos/archives/main.go.zst new file mode 100644 index 0000000..80917d9 Binary files /dev/null and b/testdata/repos/archives/main.go.zst differ diff --git a/testdata/repos/nogit/.env.prod b/testdata/repos/nogit/.env.prod new file mode 100644 index 0000000..472c30e --- /dev/null +++ b/testdata/repos/nogit/.env.prod @@ -0,0 +1,6 @@ +DB_HOST=example.com +DB_PORT=443 +DB_USERNAME=postgres +DB_PASSWORD=8ae31cacf141669ddfb5da +DB_NAME=best_db +DB_SSL=true \ No newline at end of file diff --git a/testdata/repos/nogit/.gitleaksignore b/testdata/repos/nogit/.gitleaksignore new file mode 100644 index 0000000..981ab84 --- /dev/null +++ b/testdata/repos/nogit/.gitleaksignore @@ -0,0 +1 @@ +../testdata/repos/nogit/api.go:aws-access-key:20 diff --git a/testdata/repos/nogit/api.go b/testdata/repos/nogit/api.go new file mode 100644 index 0000000..acbef43 --- /dev/null +++ b/testdata/repos/nogit/api.go @@ -0,0 +1,24 @@ +package main + +import "fmt" + +func main() { + + var a = "initial" + fmt.Println(a) + + var b, c int = 1, 2 + fmt.Println(b, c) + + var d = true + fmt.Println(d) + + var e int + fmt.Println(e) + + // opps I added a secret at line 20 + awsToken := "AKIALALEMEL33243OLIA" + + f := "apple" + fmt.Println(f) +} diff --git a/testdata/repos/nogit/main.go b/testdata/repos/nogit/main.go new file mode 100644 index 0000000..acbef43 --- /dev/null +++ b/testdata/repos/nogit/main.go @@ -0,0 +1,24 @@ +package main + +import "fmt" + +func main() { + + var a = "initial" + fmt.Println(a) + + var b, c int = 1, 2 + fmt.Println(b, c) + + var d = true + fmt.Println(d) + + var e int + fmt.Println(e) + + // opps I added a secret at line 20 + awsToken := "AKIALALEMEL33243OLIA" + + f := "apple" + fmt.Println(f) +} diff --git a/testdata/repos/small/.gitleaksignore b/testdata/repos/small/.gitleaksignore new file mode 100644 index 0000000..880403d --- /dev/null +++ b/testdata/repos/small/.gitleaksignore @@ -0,0 +1,3 @@ +api/ignoreGlobal.go:aws-access-key:20 +# test comment +53cd7a3c6eb4937f413e3c25e4a9f39289afa69e:api/ignoreCommit.go:aws-access-key:20 diff --git a/testdata/repos/small/README.md b/testdata/repos/small/README.md new file mode 100644 index 0000000..5cc9baf --- /dev/null +++ b/testdata/repos/small/README.md @@ -0,0 +1,2 @@ +# test +This is a repo used for testing gitleaks diff --git a/testdata/repos/small/api/api.go b/testdata/repos/small/api/api.go new file mode 100644 index 0000000..d832479 --- /dev/null +++ b/testdata/repos/small/api/api.go @@ -0,0 +1,7 @@ +package api + +import "fmt" + +func PrintHello() { + fmt.Println("hello") +} diff --git a/testdata/repos/small/api/ignoreCommit.go b/testdata/repos/small/api/ignoreCommit.go new file mode 100644 index 0000000..acbef43 --- /dev/null +++ b/testdata/repos/small/api/ignoreCommit.go @@ -0,0 +1,24 @@ +package main + +import "fmt" + +func main() { + + var a = "initial" + fmt.Println(a) + + var b, c int = 1, 2 + fmt.Println(b, c) + + var d = true + fmt.Println(d) + + var e int + fmt.Println(e) + + // opps I added a secret at line 20 + awsToken := "AKIALALEMEL33243OLIA" + + f := "apple" + fmt.Println(f) +} diff --git a/testdata/repos/small/api/ignoreGlobal.go b/testdata/repos/small/api/ignoreGlobal.go new file mode 100644 index 0000000..acbef43 --- /dev/null +++ b/testdata/repos/small/api/ignoreGlobal.go @@ -0,0 +1,24 @@ +package main + +import "fmt" + +func main() { + + var a = "initial" + fmt.Println(a) + + var b, c int = 1, 2 + fmt.Println(b, c) + + var d = true + fmt.Println(d) + + var e int + fmt.Println(e) + + // opps I added a secret at line 20 + awsToken := "AKIALALEMEL33243OLIA" + + f := "apple" + fmt.Println(f) +} diff --git a/testdata/repos/small/dotGit/COMMIT_EDITMSG b/testdata/repos/small/dotGit/COMMIT_EDITMSG new file mode 100644 index 0000000..940da73 --- /dev/null +++ b/testdata/repos/small/dotGit/COMMIT_EDITMSG @@ -0,0 +1 @@ +add .gitleaksignore test files diff --git a/testdata/repos/small/dotGit/FETCH_HEAD b/testdata/repos/small/dotGit/FETCH_HEAD new file mode 100644 index 0000000..66c1c77 --- /dev/null +++ b/testdata/repos/small/dotGit/FETCH_HEAD @@ -0,0 +1 @@ +2e1db472eeba53f06c4026ae4566ea022e36598e branch 'main' of github.com:gitleaks/test diff --git a/testdata/repos/small/dotGit/HEAD b/testdata/repos/small/dotGit/HEAD new file mode 100644 index 0000000..b870d82 --- /dev/null +++ b/testdata/repos/small/dotGit/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/testdata/repos/small/dotGit/ORIG_HEAD b/testdata/repos/small/dotGit/ORIG_HEAD new file mode 100644 index 0000000..b837830 --- /dev/null +++ b/testdata/repos/small/dotGit/ORIG_HEAD @@ -0,0 +1 @@ +cada78a9bf157ec05573f19e682d211f811c2e2d diff --git a/testdata/repos/small/dotGit/config b/testdata/repos/small/dotGit/config new file mode 100644 index 0000000..374df60 --- /dev/null +++ b/testdata/repos/small/dotGit/config @@ -0,0 +1,13 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true +[remote "origin"] + url = git@github.com:gitleaks/test.git + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "main"] + remote = origin + merge = refs/heads/main diff --git a/testdata/repos/small/dotGit/description b/testdata/repos/small/dotGit/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/testdata/repos/small/dotGit/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/testdata/repos/small/dotGit/index b/testdata/repos/small/dotGit/index new file mode 100644 index 0000000..3cde921 Binary files /dev/null and b/testdata/repos/small/dotGit/index differ diff --git a/testdata/repos/small/dotGit/info/exclude b/testdata/repos/small/dotGit/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/testdata/repos/small/dotGit/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/testdata/repos/small/dotGit/logs/HEAD b/testdata/repos/small/dotGit/logs/HEAD new file mode 100644 index 0000000..e22c1ce --- /dev/null +++ b/testdata/repos/small/dotGit/logs/HEAD @@ -0,0 +1,17 @@ +0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896329 -0500 clone: from github.com:gitleaks/test.git +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896362 -0500 checkout: moving from main to remove-secrets +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 906335481df9a4b48906c90318b4fac76b67fe73 Zach Rice 1635896426 -0500 commit: load token via env var +906335481df9a4b48906c90318b4fac76b67fe73 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896518 -0500 commit: add api package +a122b33c6bad3ee54724f52f2caad385ab1982ab a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896543 -0500 checkout: moving from remove-secrets to api-pkg +a122b33c6bad3ee54724f52f2caad385ab1982ab 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896644 -0500 checkout: moving from api-pkg to main +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896648 -0500 pull origin main: Fast-forward +2e1db472eeba53f06c4026ae4566ea022e36598e 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896716 -0500 checkout: moving from main to foo +2e1db472eeba53f06c4026ae4566ea022e36598e 491504d5a31946ce75e22554cc34203d8e5ff3ca Zach Rice 1635896886 -0500 commit: adding foo package with secret +491504d5a31946ce75e22554cc34203d8e5ff3ca f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635896931 -0500 commit: removing secret from foo package +f1b58b97808f8e744f6a23c693859df5b5968901 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635897009 -0500 checkout: moving from foo to main +2e1db472eeba53f06c4026ae4566ea022e36598e f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635897062 -0500 checkout: moving from main to foo +f1b58b97808f8e744f6a23c693859df5b5968901 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635897508 -0500 checkout: moving from foo to main +2e1db472eeba53f06c4026ae4566ea022e36598e 4f77f1b3cc39d4b17e4cf4ba0a38f5daed9875b4 Richard Gomez 1681920523 -0400 commit: add .gitleaksignore test +4f77f1b3cc39d4b17e4cf4ba0a38f5daed9875b4 cada78a9bf157ec05573f19e682d211f811c2e2d Richard Gomez 1681920658 -0400 commit (amend): add .gitleaksignore test +cada78a9bf157ec05573f19e682d211f811c2e2d 2e1db472eeba53f06c4026ae4566ea022e36598e Richard Gomez 1681920730 -0400 reset: moving to HEAD~ +2e1db472eeba53f06c4026ae4566ea022e36598e 53cd7a3c6eb4937f413e3c25e4a9f39289afa69e Richard Gomez 1681920759 -0400 commit: add .gitleaksignore test files diff --git a/testdata/repos/small/dotGit/logs/refs/heads/api-pkg b/testdata/repos/small/dotGit/logs/refs/heads/api-pkg new file mode 100644 index 0000000..18e1cff --- /dev/null +++ b/testdata/repos/small/dotGit/logs/refs/heads/api-pkg @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896543 -0500 branch: Created from HEAD diff --git a/testdata/repos/small/dotGit/logs/refs/heads/foo b/testdata/repos/small/dotGit/logs/refs/heads/foo new file mode 100644 index 0000000..0588ad5 --- /dev/null +++ b/testdata/repos/small/dotGit/logs/refs/heads/foo @@ -0,0 +1,3 @@ +0000000000000000000000000000000000000000 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896716 -0500 branch: Created from HEAD +2e1db472eeba53f06c4026ae4566ea022e36598e 491504d5a31946ce75e22554cc34203d8e5ff3ca Zach Rice 1635896886 -0500 commit: adding foo package with secret +491504d5a31946ce75e22554cc34203d8e5ff3ca f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635896931 -0500 commit: removing secret from foo package diff --git a/testdata/repos/small/dotGit/logs/refs/heads/main b/testdata/repos/small/dotGit/logs/refs/heads/main new file mode 100644 index 0000000..456f8e9 --- /dev/null +++ b/testdata/repos/small/dotGit/logs/refs/heads/main @@ -0,0 +1,6 @@ +0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896329 -0500 clone: from github.com:gitleaks/test.git +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896648 -0500 pull origin main: Fast-forward +2e1db472eeba53f06c4026ae4566ea022e36598e 4f77f1b3cc39d4b17e4cf4ba0a38f5daed9875b4 Richard Gomez 1681920523 -0400 commit: add .gitleaksignore test +4f77f1b3cc39d4b17e4cf4ba0a38f5daed9875b4 cada78a9bf157ec05573f19e682d211f811c2e2d Richard Gomez 1681920658 -0400 commit (amend): add .gitleaksignore test +cada78a9bf157ec05573f19e682d211f811c2e2d 2e1db472eeba53f06c4026ae4566ea022e36598e Richard Gomez 1681920730 -0400 reset: moving to HEAD~ +2e1db472eeba53f06c4026ae4566ea022e36598e 53cd7a3c6eb4937f413e3c25e4a9f39289afa69e Richard Gomez 1681920759 -0400 commit: add .gitleaksignore test files diff --git a/testdata/repos/small/dotGit/logs/refs/heads/remove-secrets b/testdata/repos/small/dotGit/logs/refs/heads/remove-secrets new file mode 100644 index 0000000..58344a3 --- /dev/null +++ b/testdata/repos/small/dotGit/logs/refs/heads/remove-secrets @@ -0,0 +1,3 @@ +0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896362 -0500 branch: Created from HEAD +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 906335481df9a4b48906c90318b4fac76b67fe73 Zach Rice 1635896426 -0500 commit: load token via env var +906335481df9a4b48906c90318b4fac76b67fe73 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896518 -0500 commit: add api package diff --git a/testdata/repos/small/dotGit/logs/refs/remotes/origin/HEAD b/testdata/repos/small/dotGit/logs/refs/remotes/origin/HEAD new file mode 100644 index 0000000..a2076e5 --- /dev/null +++ b/testdata/repos/small/dotGit/logs/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896329 -0500 clone: from github.com:gitleaks/test.git diff --git a/testdata/repos/small/dotGit/logs/refs/remotes/origin/api-pkg b/testdata/repos/small/dotGit/logs/refs/remotes/origin/api-pkg new file mode 100644 index 0000000..9c8e059 --- /dev/null +++ b/testdata/repos/small/dotGit/logs/refs/remotes/origin/api-pkg @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896552 -0500 update by push diff --git a/testdata/repos/small/dotGit/logs/refs/remotes/origin/foo b/testdata/repos/small/dotGit/logs/refs/remotes/origin/foo new file mode 100644 index 0000000..f6aed26 --- /dev/null +++ b/testdata/repos/small/dotGit/logs/refs/remotes/origin/foo @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635896935 -0500 update by push diff --git a/testdata/repos/small/dotGit/logs/refs/remotes/origin/main b/testdata/repos/small/dotGit/logs/refs/remotes/origin/main new file mode 100644 index 0000000..530a789 --- /dev/null +++ b/testdata/repos/small/dotGit/logs/refs/remotes/origin/main @@ -0,0 +1 @@ +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896648 -0500 pull origin main: fast-forward diff --git a/testdata/repos/small/dotGit/objects/02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 b/testdata/repos/small/dotGit/objects/02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 new file mode 100644 index 0000000..dab8999 --- /dev/null +++ b/testdata/repos/small/dotGit/objects/02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 @@ -0,0 +1 @@ +xU1 0`ܯ8nJ.:*(8\ɕ$ w3 Nox{$6f1~wF'0YbF TBpND|*]uCST kL>a#(Jm(sԴ]=>03 \ No newline at end of file diff --git a/testdata/repos/small/dotGit/objects/15/2888a42422b2ff5868b8d003d626120a9cb738 b/testdata/repos/small/dotGit/objects/15/2888a42422b2ff5868b8d003d626120a9cb738 new file mode 100644 index 0000000..f9ada07 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/15/2888a42422b2ff5868b8d003d626120a9cb738 differ diff --git a/testdata/repos/small/dotGit/objects/2e/1db472eeba53f06c4026ae4566ea022e36598e b/testdata/repos/small/dotGit/objects/2e/1db472eeba53f06c4026ae4566ea022e36598e new file mode 100644 index 0000000..5dd33df Binary files /dev/null and b/testdata/repos/small/dotGit/objects/2e/1db472eeba53f06c4026ae4566ea022e36598e differ diff --git a/testdata/repos/small/dotGit/objects/32/bfaec6697c1a693f71b183d3389ad2547bbb3c b/testdata/repos/small/dotGit/objects/32/bfaec6697c1a693f71b183d3389ad2547bbb3c new file mode 100644 index 0000000..045c095 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/32/bfaec6697c1a693f71b183d3389ad2547bbb3c differ diff --git a/testdata/repos/small/dotGit/objects/49/1504d5a31946ce75e22554cc34203d8e5ff3ca b/testdata/repos/small/dotGit/objects/49/1504d5a31946ce75e22554cc34203d8e5ff3ca new file mode 100644 index 0000000..754d739 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/49/1504d5a31946ce75e22554cc34203d8e5ff3ca differ diff --git a/testdata/repos/small/dotGit/objects/4f/77f1b3cc39d4b17e4cf4ba0a38f5daed9875b4 b/testdata/repos/small/dotGit/objects/4f/77f1b3cc39d4b17e4cf4ba0a38f5daed9875b4 new file mode 100644 index 0000000..292c67b Binary files /dev/null and b/testdata/repos/small/dotGit/objects/4f/77f1b3cc39d4b17e4cf4ba0a38f5daed9875b4 differ diff --git a/testdata/repos/small/dotGit/objects/53/cd7a3c6eb4937f413e3c25e4a9f39289afa69e b/testdata/repos/small/dotGit/objects/53/cd7a3c6eb4937f413e3c25e4a9f39289afa69e new file mode 100644 index 0000000..d0b42fb Binary files /dev/null and b/testdata/repos/small/dotGit/objects/53/cd7a3c6eb4937f413e3c25e4a9f39289afa69e differ diff --git a/testdata/repos/small/dotGit/objects/5c/547e4215d9594c3935bdfefdf4f500016a4112 b/testdata/repos/small/dotGit/objects/5c/547e4215d9594c3935bdfefdf4f500016a4112 new file mode 100644 index 0000000..5bddb82 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/5c/547e4215d9594c3935bdfefdf4f500016a4112 differ diff --git a/testdata/repos/small/dotGit/objects/78/9ba677976d5db481de55c799d67acbf8e3f16a b/testdata/repos/small/dotGit/objects/78/9ba677976d5db481de55c799d67acbf8e3f16a new file mode 100644 index 0000000..93acde0 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/78/9ba677976d5db481de55c799d67acbf8e3f16a differ diff --git a/testdata/repos/small/dotGit/objects/8d/c20a62e189d3446cfb6ec328dbd379d64feb20 b/testdata/repos/small/dotGit/objects/8d/c20a62e189d3446cfb6ec328dbd379d64feb20 new file mode 100644 index 0000000..740ed69 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/8d/c20a62e189d3446cfb6ec328dbd379d64feb20 differ diff --git a/testdata/repos/small/dotGit/objects/90/6335481df9a4b48906c90318b4fac76b67fe73 b/testdata/repos/small/dotGit/objects/90/6335481df9a4b48906c90318b4fac76b67fe73 new file mode 100644 index 0000000..ce4a269 --- /dev/null +++ b/testdata/repos/small/dotGit/objects/90/6335481df9a4b48906c90318b4fac76b67fe73 @@ -0,0 +1,3 @@ +xM +0F]s%t +"I2E۔ >x_2w@;z㑈ءCX@6 5)M&F:l'FTHďFF1iPSm4cNo;ݷV{]ߗT`=aZw d}fuKK \ No newline at end of file diff --git a/testdata/repos/small/dotGit/objects/98/1ab8417104c91bb9a4657f5d436c760c0aff50 b/testdata/repos/small/dotGit/objects/98/1ab8417104c91bb9a4657f5d436c760c0aff50 new file mode 100644 index 0000000..d4a9239 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/98/1ab8417104c91bb9a4657f5d436c760c0aff50 differ diff --git a/testdata/repos/small/dotGit/objects/9a/932e37eaa9fb64b09e47e5e859c9b2c8cb47ad b/testdata/repos/small/dotGit/objects/9a/932e37eaa9fb64b09e47e5e859c9b2c8cb47ad new file mode 100644 index 0000000..5e51e39 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/9a/932e37eaa9fb64b09e47e5e859c9b2c8cb47ad differ diff --git a/testdata/repos/small/dotGit/objects/a1/22b33c6bad3ee54724f52f2caad385ab1982ab b/testdata/repos/small/dotGit/objects/a1/22b33c6bad3ee54724f52f2caad385ab1982ab new file mode 100644 index 0000000..fbcf357 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/a1/22b33c6bad3ee54724f52f2caad385ab1982ab differ diff --git a/testdata/repos/small/dotGit/objects/a5/caae6d742e49a33982f1fdc608ce861ea59be5 b/testdata/repos/small/dotGit/objects/a5/caae6d742e49a33982f1fdc608ce861ea59be5 new file mode 100644 index 0000000..8be258a Binary files /dev/null and b/testdata/repos/small/dotGit/objects/a5/caae6d742e49a33982f1fdc608ce861ea59be5 differ diff --git a/testdata/repos/small/dotGit/objects/a9/aa0c942dcef669a94f207a77426106b25efd1a b/testdata/repos/small/dotGit/objects/a9/aa0c942dcef669a94f207a77426106b25efd1a new file mode 100644 index 0000000..9221b3c Binary files /dev/null and b/testdata/repos/small/dotGit/objects/a9/aa0c942dcef669a94f207a77426106b25efd1a differ diff --git a/testdata/repos/small/dotGit/objects/ac/bef43fdb053ec01e8e16697536d47853460cbc b/testdata/repos/small/dotGit/objects/ac/bef43fdb053ec01e8e16697536d47853460cbc new file mode 100644 index 0000000..c4ff909 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/ac/bef43fdb053ec01e8e16697536d47853460cbc differ diff --git a/testdata/repos/small/dotGit/objects/bc/f47ef84f29bb7ed6e653d61fccd30d0ecce886 b/testdata/repos/small/dotGit/objects/bc/f47ef84f29bb7ed6e653d61fccd30d0ecce886 new file mode 100644 index 0000000..ec618b7 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/bc/f47ef84f29bb7ed6e653d61fccd30d0ecce886 differ diff --git a/testdata/repos/small/dotGit/objects/ca/da78a9bf157ec05573f19e682d211f811c2e2d b/testdata/repos/small/dotGit/objects/ca/da78a9bf157ec05573f19e682d211f811c2e2d new file mode 100644 index 0000000..f64d385 --- /dev/null +++ b/testdata/repos/small/dotGit/objects/ca/da78a9bf157ec05573f19e682d211f811c2e2d @@ -0,0 +1 @@ +xAj0u@h$e!{5E-+(ʦ @v>xs-%w >zS$:YFy",Gkbbզ:Ipw#*xpaj+ϫZέ,q.EvkhG@>#/9o$%8-o*ϼ)t}v L \ No newline at end of file diff --git a/testdata/repos/small/dotGit/objects/d8/32479114dc6be7207edc7c37ce91dd11b93161 b/testdata/repos/small/dotGit/objects/d8/32479114dc6be7207edc7c37ce91dd11b93161 new file mode 100644 index 0000000..0bd9a37 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/d8/32479114dc6be7207edc7c37ce91dd11b93161 differ diff --git a/testdata/repos/small/dotGit/objects/da/2622b4d97e32c5801511244b809144b6b3ea78 b/testdata/repos/small/dotGit/objects/da/2622b4d97e32c5801511244b809144b6b3ea78 new file mode 100644 index 0000000..d4a8c3d Binary files /dev/null and b/testdata/repos/small/dotGit/objects/da/2622b4d97e32c5801511244b809144b6b3ea78 differ diff --git a/testdata/repos/small/dotGit/objects/da/eb160a13200a3422e73296a6892784a113e0d6 b/testdata/repos/small/dotGit/objects/da/eb160a13200a3422e73296a6892784a113e0d6 new file mode 100644 index 0000000..d8c9377 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/da/eb160a13200a3422e73296a6892784a113e0d6 differ diff --git a/testdata/repos/small/dotGit/objects/df/7eaa59fc89b3e7258167605db2e582f1a78e6f b/testdata/repos/small/dotGit/objects/df/7eaa59fc89b3e7258167605db2e582f1a78e6f new file mode 100644 index 0000000..e0b6154 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/df/7eaa59fc89b3e7258167605db2e582f1a78e6f differ diff --git a/testdata/repos/small/dotGit/objects/e5/c0849a65c586eab87dcfc31fec74f0fd7c62cb b/testdata/repos/small/dotGit/objects/e5/c0849a65c586eab87dcfc31fec74f0fd7c62cb new file mode 100644 index 0000000..53b83ef Binary files /dev/null and b/testdata/repos/small/dotGit/objects/e5/c0849a65c586eab87dcfc31fec74f0fd7c62cb differ diff --git a/testdata/repos/small/dotGit/objects/f1/b58b97808f8e744f6a23c693859df5b5968901 b/testdata/repos/small/dotGit/objects/f1/b58b97808f8e744f6a23c693859df5b5968901 new file mode 100644 index 0000000..a7bf901 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/f1/b58b97808f8e744f6a23c693859df5b5968901 differ diff --git a/testdata/repos/small/dotGit/objects/fa/468655086f149d41a8e7db14ed054bebec7687 b/testdata/repos/small/dotGit/objects/fa/468655086f149d41a8e7db14ed054bebec7687 new file mode 100644 index 0000000..9cae784 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/fa/468655086f149d41a8e7db14ed054bebec7687 differ diff --git a/testdata/repos/small/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.idx b/testdata/repos/small/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.idx new file mode 100644 index 0000000..3cdb2bd Binary files /dev/null and b/testdata/repos/small/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.idx differ diff --git a/testdata/repos/small/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.pack b/testdata/repos/small/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.pack new file mode 100644 index 0000000..975d371 Binary files /dev/null and b/testdata/repos/small/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.pack differ diff --git a/testdata/repos/small/dotGit/packed-refs b/testdata/repos/small/dotGit/packed-refs new file mode 100644 index 0000000..859b8c5 --- /dev/null +++ b/testdata/repos/small/dotGit/packed-refs @@ -0,0 +1,2 @@ +# pack-refs with: peeled fully-peeled sorted +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 refs/remotes/origin/main diff --git a/testdata/repos/small/dotGit/refs/heads/api-pkg b/testdata/repos/small/dotGit/refs/heads/api-pkg new file mode 100644 index 0000000..31b6c78 --- /dev/null +++ b/testdata/repos/small/dotGit/refs/heads/api-pkg @@ -0,0 +1 @@ +a122b33c6bad3ee54724f52f2caad385ab1982ab diff --git a/testdata/repos/small/dotGit/refs/heads/foo b/testdata/repos/small/dotGit/refs/heads/foo new file mode 100644 index 0000000..57d5848 --- /dev/null +++ b/testdata/repos/small/dotGit/refs/heads/foo @@ -0,0 +1 @@ +f1b58b97808f8e744f6a23c693859df5b5968901 diff --git a/testdata/repos/small/dotGit/refs/heads/main b/testdata/repos/small/dotGit/refs/heads/main new file mode 100644 index 0000000..80ff7e8 --- /dev/null +++ b/testdata/repos/small/dotGit/refs/heads/main @@ -0,0 +1 @@ +53cd7a3c6eb4937f413e3c25e4a9f39289afa69e diff --git a/testdata/repos/small/dotGit/refs/heads/remove-secrets b/testdata/repos/small/dotGit/refs/heads/remove-secrets new file mode 100644 index 0000000..31b6c78 --- /dev/null +++ b/testdata/repos/small/dotGit/refs/heads/remove-secrets @@ -0,0 +1 @@ +a122b33c6bad3ee54724f52f2caad385ab1982ab diff --git a/testdata/repos/small/dotGit/refs/remotes/origin/HEAD b/testdata/repos/small/dotGit/refs/remotes/origin/HEAD new file mode 100644 index 0000000..4b0a875 --- /dev/null +++ b/testdata/repos/small/dotGit/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +ref: refs/remotes/origin/main diff --git a/testdata/repos/small/dotGit/refs/remotes/origin/api-pkg b/testdata/repos/small/dotGit/refs/remotes/origin/api-pkg new file mode 100644 index 0000000..31b6c78 --- /dev/null +++ b/testdata/repos/small/dotGit/refs/remotes/origin/api-pkg @@ -0,0 +1 @@ +a122b33c6bad3ee54724f52f2caad385ab1982ab diff --git a/testdata/repos/small/dotGit/refs/remotes/origin/foo b/testdata/repos/small/dotGit/refs/remotes/origin/foo new file mode 100644 index 0000000..57d5848 --- /dev/null +++ b/testdata/repos/small/dotGit/refs/remotes/origin/foo @@ -0,0 +1 @@ +f1b58b97808f8e744f6a23c693859df5b5968901 diff --git a/testdata/repos/small/dotGit/refs/remotes/origin/main b/testdata/repos/small/dotGit/refs/remotes/origin/main new file mode 100644 index 0000000..98f12e9 --- /dev/null +++ b/testdata/repos/small/dotGit/refs/remotes/origin/main @@ -0,0 +1 @@ +2e1db472eeba53f06c4026ae4566ea022e36598e diff --git a/testdata/repos/small/main.go b/testdata/repos/small/main.go new file mode 100644 index 0000000..9a932e3 --- /dev/null +++ b/testdata/repos/small/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "os" +) + +func main() { + + var a = "initial" + fmt.Println(a) + + var b, c int = 1, 2 + fmt.Println(b, c) + + var d = true + fmt.Println(d) + + var e int + fmt.Println(e) + + // load secret via env + awsToken := os.Getenv("AWS_TOKEN") + + f := "apple" + fmt.Println(f) +} diff --git a/testdata/repos/staged/.gitleaksignore b/testdata/repos/staged/.gitleaksignore new file mode 100644 index 0000000..770453c --- /dev/null +++ b/testdata/repos/staged/.gitleaksignore @@ -0,0 +1 @@ +api/api.go:aws-access-key:6 \ No newline at end of file diff --git a/testdata/repos/staged/README.md b/testdata/repos/staged/README.md new file mode 100644 index 0000000..5cc9baf --- /dev/null +++ b/testdata/repos/staged/README.md @@ -0,0 +1,2 @@ +# test +This is a repo used for testing gitleaks diff --git a/testdata/repos/staged/api/api.go b/testdata/repos/staged/api/api.go new file mode 100644 index 0000000..b16d768 --- /dev/null +++ b/testdata/repos/staged/api/api.go @@ -0,0 +1,10 @@ +package api + +import "fmt" + +func PrintHello() { + aws_token := "AKIALALEMEL33243OLIA" // fingerprint of that secret is added to .gitleaksignore + aws_token2 := "AKIALALEMEL33243OLIA" // this one is not + fmt.Println(aws_token) + fmt.Println(aws_token2) +} diff --git a/testdata/repos/staged/dotGit/COMMIT_EDITMSG b/testdata/repos/staged/dotGit/COMMIT_EDITMSG new file mode 100644 index 0000000..b83ad53 --- /dev/null +++ b/testdata/repos/staged/dotGit/COMMIT_EDITMSG @@ -0,0 +1 @@ +add .gitleaksignore file diff --git a/testdata/repos/staged/dotGit/FETCH_HEAD b/testdata/repos/staged/dotGit/FETCH_HEAD new file mode 100644 index 0000000..66c1c77 --- /dev/null +++ b/testdata/repos/staged/dotGit/FETCH_HEAD @@ -0,0 +1 @@ +2e1db472eeba53f06c4026ae4566ea022e36598e branch 'main' of github.com:gitleaks/test diff --git a/testdata/repos/staged/dotGit/HEAD b/testdata/repos/staged/dotGit/HEAD new file mode 100644 index 0000000..b870d82 --- /dev/null +++ b/testdata/repos/staged/dotGit/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/testdata/repos/staged/dotGit/ORIG_HEAD b/testdata/repos/staged/dotGit/ORIG_HEAD new file mode 100644 index 0000000..96321cc --- /dev/null +++ b/testdata/repos/staged/dotGit/ORIG_HEAD @@ -0,0 +1 @@ +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 diff --git a/testdata/repos/staged/dotGit/config b/testdata/repos/staged/dotGit/config new file mode 100644 index 0000000..374df60 --- /dev/null +++ b/testdata/repos/staged/dotGit/config @@ -0,0 +1,13 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true +[remote "origin"] + url = git@github.com:gitleaks/test.git + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "main"] + remote = origin + merge = refs/heads/main diff --git a/testdata/repos/staged/dotGit/description b/testdata/repos/staged/dotGit/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/testdata/repos/staged/dotGit/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/testdata/repos/staged/dotGit/index b/testdata/repos/staged/dotGit/index new file mode 100644 index 0000000..42a3c44 Binary files /dev/null and b/testdata/repos/staged/dotGit/index differ diff --git a/testdata/repos/staged/dotGit/info/exclude b/testdata/repos/staged/dotGit/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/testdata/repos/staged/dotGit/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/testdata/repos/staged/dotGit/logs/HEAD b/testdata/repos/staged/dotGit/logs/HEAD new file mode 100644 index 0000000..928095a --- /dev/null +++ b/testdata/repos/staged/dotGit/logs/HEAD @@ -0,0 +1,14 @@ +0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896329 -0500 clone: from github.com:gitleaks/test.git +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896362 -0500 checkout: moving from main to remove-secrets +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 906335481df9a4b48906c90318b4fac76b67fe73 Zach Rice 1635896426 -0500 commit: load token via env var +906335481df9a4b48906c90318b4fac76b67fe73 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896518 -0500 commit: add api package +a122b33c6bad3ee54724f52f2caad385ab1982ab a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896543 -0500 checkout: moving from remove-secrets to api-pkg +a122b33c6bad3ee54724f52f2caad385ab1982ab 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896644 -0500 checkout: moving from api-pkg to main +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896648 -0500 pull origin main: Fast-forward +2e1db472eeba53f06c4026ae4566ea022e36598e 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896716 -0500 checkout: moving from main to foo +2e1db472eeba53f06c4026ae4566ea022e36598e 491504d5a31946ce75e22554cc34203d8e5ff3ca Zach Rice 1635896886 -0500 commit: adding foo package with secret +491504d5a31946ce75e22554cc34203d8e5ff3ca f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635896931 -0500 commit: removing secret from foo package +f1b58b97808f8e744f6a23c693859df5b5968901 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635897009 -0500 checkout: moving from foo to main +2e1db472eeba53f06c4026ae4566ea022e36598e f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635897062 -0500 checkout: moving from main to foo +f1b58b97808f8e744f6a23c693859df5b5968901 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635897508 -0500 checkout: moving from foo to main +2e1db472eeba53f06c4026ae4566ea022e36598e bf3f24164d7256b4021575cbdb2f97b98e6f057e Rafael Figueiredo 1679239434 -0300 commit: add .gitleaksignore file diff --git a/testdata/repos/staged/dotGit/logs/refs/heads/api-pkg b/testdata/repos/staged/dotGit/logs/refs/heads/api-pkg new file mode 100644 index 0000000..18e1cff --- /dev/null +++ b/testdata/repos/staged/dotGit/logs/refs/heads/api-pkg @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896543 -0500 branch: Created from HEAD diff --git a/testdata/repos/staged/dotGit/logs/refs/heads/foo b/testdata/repos/staged/dotGit/logs/refs/heads/foo new file mode 100644 index 0000000..0588ad5 --- /dev/null +++ b/testdata/repos/staged/dotGit/logs/refs/heads/foo @@ -0,0 +1,3 @@ +0000000000000000000000000000000000000000 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896716 -0500 branch: Created from HEAD +2e1db472eeba53f06c4026ae4566ea022e36598e 491504d5a31946ce75e22554cc34203d8e5ff3ca Zach Rice 1635896886 -0500 commit: adding foo package with secret +491504d5a31946ce75e22554cc34203d8e5ff3ca f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635896931 -0500 commit: removing secret from foo package diff --git a/testdata/repos/staged/dotGit/logs/refs/heads/main b/testdata/repos/staged/dotGit/logs/refs/heads/main new file mode 100644 index 0000000..c4bd6cb --- /dev/null +++ b/testdata/repos/staged/dotGit/logs/refs/heads/main @@ -0,0 +1,3 @@ +0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896329 -0500 clone: from github.com:gitleaks/test.git +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896648 -0500 pull origin main: Fast-forward +2e1db472eeba53f06c4026ae4566ea022e36598e bf3f24164d7256b4021575cbdb2f97b98e6f057e Rafael Figueiredo 1679239434 -0300 commit: add .gitleaksignore file diff --git a/testdata/repos/staged/dotGit/logs/refs/heads/remove-secrets b/testdata/repos/staged/dotGit/logs/refs/heads/remove-secrets new file mode 100644 index 0000000..58344a3 --- /dev/null +++ b/testdata/repos/staged/dotGit/logs/refs/heads/remove-secrets @@ -0,0 +1,3 @@ +0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896362 -0500 branch: Created from HEAD +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 906335481df9a4b48906c90318b4fac76b67fe73 Zach Rice 1635896426 -0500 commit: load token via env var +906335481df9a4b48906c90318b4fac76b67fe73 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896518 -0500 commit: add api package diff --git a/testdata/repos/staged/dotGit/logs/refs/remotes/origin/HEAD b/testdata/repos/staged/dotGit/logs/refs/remotes/origin/HEAD new file mode 100644 index 0000000..a2076e5 --- /dev/null +++ b/testdata/repos/staged/dotGit/logs/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896329 -0500 clone: from github.com:gitleaks/test.git diff --git a/testdata/repos/staged/dotGit/logs/refs/remotes/origin/api-pkg b/testdata/repos/staged/dotGit/logs/refs/remotes/origin/api-pkg new file mode 100644 index 0000000..9c8e059 --- /dev/null +++ b/testdata/repos/staged/dotGit/logs/refs/remotes/origin/api-pkg @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896552 -0500 update by push diff --git a/testdata/repos/staged/dotGit/logs/refs/remotes/origin/foo b/testdata/repos/staged/dotGit/logs/refs/remotes/origin/foo new file mode 100644 index 0000000..f6aed26 --- /dev/null +++ b/testdata/repos/staged/dotGit/logs/refs/remotes/origin/foo @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635896935 -0500 update by push diff --git a/testdata/repos/staged/dotGit/logs/refs/remotes/origin/main b/testdata/repos/staged/dotGit/logs/refs/remotes/origin/main new file mode 100644 index 0000000..530a789 --- /dev/null +++ b/testdata/repos/staged/dotGit/logs/refs/remotes/origin/main @@ -0,0 +1 @@ +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896648 -0500 pull origin main: fast-forward diff --git a/testdata/repos/staged/dotGit/objects/02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 b/testdata/repos/staged/dotGit/objects/02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 new file mode 100644 index 0000000..dab8999 --- /dev/null +++ b/testdata/repos/staged/dotGit/objects/02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 @@ -0,0 +1 @@ +xU1 0`ܯ8nJ.:*(8\ɕ$ w3 Nox{$6f1~wF'0YbF TBpND|*]uCST kL>a#(Jm(sԴ]=>03 \ No newline at end of file diff --git a/testdata/repos/staged/dotGit/objects/15/2888a42422b2ff5868b8d003d626120a9cb738 b/testdata/repos/staged/dotGit/objects/15/2888a42422b2ff5868b8d003d626120a9cb738 new file mode 100644 index 0000000..f9ada07 Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/15/2888a42422b2ff5868b8d003d626120a9cb738 differ diff --git a/testdata/repos/staged/dotGit/objects/2e/1db472eeba53f06c4026ae4566ea022e36598e b/testdata/repos/staged/dotGit/objects/2e/1db472eeba53f06c4026ae4566ea022e36598e new file mode 100644 index 0000000..5dd33df Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/2e/1db472eeba53f06c4026ae4566ea022e36598e differ diff --git a/testdata/repos/staged/dotGit/objects/46/18d7e4512b6b0b1dab85cf846d9f43474ec8be b/testdata/repos/staged/dotGit/objects/46/18d7e4512b6b0b1dab85cf846d9f43474ec8be new file mode 100644 index 0000000..92ce09d Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/46/18d7e4512b6b0b1dab85cf846d9f43474ec8be differ diff --git a/testdata/repos/staged/dotGit/objects/49/1504d5a31946ce75e22554cc34203d8e5ff3ca b/testdata/repos/staged/dotGit/objects/49/1504d5a31946ce75e22554cc34203d8e5ff3ca new file mode 100644 index 0000000..754d739 Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/49/1504d5a31946ce75e22554cc34203d8e5ff3ca differ diff --git a/testdata/repos/staged/dotGit/objects/5c/547e4215d9594c3935bdfefdf4f500016a4112 b/testdata/repos/staged/dotGit/objects/5c/547e4215d9594c3935bdfefdf4f500016a4112 new file mode 100644 index 0000000..5bddb82 Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/5c/547e4215d9594c3935bdfefdf4f500016a4112 differ diff --git a/testdata/repos/staged/dotGit/objects/65/83d6db4a57bbeda62d50fc91649036d499418d b/testdata/repos/staged/dotGit/objects/65/83d6db4a57bbeda62d50fc91649036d499418d new file mode 100644 index 0000000..b6fec1e Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/65/83d6db4a57bbeda62d50fc91649036d499418d differ diff --git a/testdata/repos/staged/dotGit/objects/66/bc70d0c0bfbb6468b3f90c3f1e9f2ddba02b43 b/testdata/repos/staged/dotGit/objects/66/bc70d0c0bfbb6468b3f90c3f1e9f2ddba02b43 new file mode 100644 index 0000000..f6622df Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/66/bc70d0c0bfbb6468b3f90c3f1e9f2ddba02b43 differ diff --git a/testdata/repos/staged/dotGit/objects/78/9ba677976d5db481de55c799d67acbf8e3f16a b/testdata/repos/staged/dotGit/objects/78/9ba677976d5db481de55c799d67acbf8e3f16a new file mode 100644 index 0000000..93acde0 Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/78/9ba677976d5db481de55c799d67acbf8e3f16a differ diff --git a/testdata/repos/staged/dotGit/objects/90/6335481df9a4b48906c90318b4fac76b67fe73 b/testdata/repos/staged/dotGit/objects/90/6335481df9a4b48906c90318b4fac76b67fe73 new file mode 100644 index 0000000..ce4a269 --- /dev/null +++ b/testdata/repos/staged/dotGit/objects/90/6335481df9a4b48906c90318b4fac76b67fe73 @@ -0,0 +1,3 @@ +xM +0F]s%t +"I2E۔ >x_2w@;z㑈ءCX@6 5)M&F:l'FTHďFF1iPSm4cNo;ݷV{]ߗT`=aZw d}fuKK \ No newline at end of file diff --git a/testdata/repos/staged/dotGit/objects/9a/932e37eaa9fb64b09e47e5e859c9b2c8cb47ad b/testdata/repos/staged/dotGit/objects/9a/932e37eaa9fb64b09e47e5e859c9b2c8cb47ad new file mode 100644 index 0000000..5e51e39 Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/9a/932e37eaa9fb64b09e47e5e859c9b2c8cb47ad differ diff --git a/testdata/repos/staged/dotGit/objects/a1/22b33c6bad3ee54724f52f2caad385ab1982ab b/testdata/repos/staged/dotGit/objects/a1/22b33c6bad3ee54724f52f2caad385ab1982ab new file mode 100644 index 0000000..fbcf357 Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/a1/22b33c6bad3ee54724f52f2caad385ab1982ab differ diff --git a/testdata/repos/staged/dotGit/objects/a5/caae6d742e49a33982f1fdc608ce861ea59be5 b/testdata/repos/staged/dotGit/objects/a5/caae6d742e49a33982f1fdc608ce861ea59be5 new file mode 100644 index 0000000..8be258a Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/a5/caae6d742e49a33982f1fdc608ce861ea59be5 differ diff --git a/testdata/repos/staged/dotGit/objects/a9/aa0c942dcef669a94f207a77426106b25efd1a b/testdata/repos/staged/dotGit/objects/a9/aa0c942dcef669a94f207a77426106b25efd1a new file mode 100644 index 0000000..9221b3c Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/a9/aa0c942dcef669a94f207a77426106b25efd1a differ diff --git a/testdata/repos/staged/dotGit/objects/b1/6d768dd595a59f947abe087901183d219d7e54 b/testdata/repos/staged/dotGit/objects/b1/6d768dd595a59f947abe087901183d219d7e54 new file mode 100644 index 0000000..6ba6dea Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/b1/6d768dd595a59f947abe087901183d219d7e54 differ diff --git a/testdata/repos/staged/dotGit/objects/bc/f47ef84f29bb7ed6e653d61fccd30d0ecce886 b/testdata/repos/staged/dotGit/objects/bc/f47ef84f29bb7ed6e653d61fccd30d0ecce886 new file mode 100644 index 0000000..ec618b7 Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/bc/f47ef84f29bb7ed6e653d61fccd30d0ecce886 differ diff --git a/testdata/repos/staged/dotGit/objects/bf/3f24164d7256b4021575cbdb2f97b98e6f057e b/testdata/repos/staged/dotGit/objects/bf/3f24164d7256b4021575cbdb2f97b98e6f057e new file mode 100644 index 0000000..897417b --- /dev/null +++ b/testdata/repos/staged/dotGit/objects/bf/3f24164d7256b4021575cbdb2f97b98e6f057e @@ -0,0 +1,2 @@ +xAn!sXɲ|5X[*uj[d1:@"1-9q,1ct%preB(6lw)GX +KlEgP 's8>ӷF_uQ;rS/jozH[&Yu;OM;;fhtXؠ?Ϻ[_]U \ No newline at end of file diff --git a/testdata/repos/staged/dotGit/objects/d8/32479114dc6be7207edc7c37ce91dd11b93161 b/testdata/repos/staged/dotGit/objects/d8/32479114dc6be7207edc7c37ce91dd11b93161 new file mode 100644 index 0000000..0bd9a37 Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/d8/32479114dc6be7207edc7c37ce91dd11b93161 differ diff --git a/testdata/repos/staged/dotGit/objects/da/2622b4d97e32c5801511244b809144b6b3ea78 b/testdata/repos/staged/dotGit/objects/da/2622b4d97e32c5801511244b809144b6b3ea78 new file mode 100644 index 0000000..d4a8c3d Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/da/2622b4d97e32c5801511244b809144b6b3ea78 differ diff --git a/testdata/repos/staged/dotGit/objects/e5/c0849a65c586eab87dcfc31fec74f0fd7c62cb b/testdata/repos/staged/dotGit/objects/e5/c0849a65c586eab87dcfc31fec74f0fd7c62cb new file mode 100644 index 0000000..53b83ef Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/e5/c0849a65c586eab87dcfc31fec74f0fd7c62cb differ diff --git a/testdata/repos/staged/dotGit/objects/f1/b58b97808f8e744f6a23c693859df5b5968901 b/testdata/repos/staged/dotGit/objects/f1/b58b97808f8e744f6a23c693859df5b5968901 new file mode 100644 index 0000000..a7bf901 Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/f1/b58b97808f8e744f6a23c693859df5b5968901 differ diff --git a/testdata/repos/staged/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.idx b/testdata/repos/staged/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.idx new file mode 100644 index 0000000..3cdb2bd Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.idx differ diff --git a/testdata/repos/staged/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.pack b/testdata/repos/staged/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.pack new file mode 100644 index 0000000..975d371 Binary files /dev/null and b/testdata/repos/staged/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.pack differ diff --git a/testdata/repos/staged/dotGit/packed-refs b/testdata/repos/staged/dotGit/packed-refs new file mode 100644 index 0000000..859b8c5 --- /dev/null +++ b/testdata/repos/staged/dotGit/packed-refs @@ -0,0 +1,2 @@ +# pack-refs with: peeled fully-peeled sorted +1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 refs/remotes/origin/main diff --git a/testdata/repos/staged/dotGit/refs/heads/api-pkg b/testdata/repos/staged/dotGit/refs/heads/api-pkg new file mode 100644 index 0000000..31b6c78 --- /dev/null +++ b/testdata/repos/staged/dotGit/refs/heads/api-pkg @@ -0,0 +1 @@ +a122b33c6bad3ee54724f52f2caad385ab1982ab diff --git a/testdata/repos/staged/dotGit/refs/heads/foo b/testdata/repos/staged/dotGit/refs/heads/foo new file mode 100644 index 0000000..57d5848 --- /dev/null +++ b/testdata/repos/staged/dotGit/refs/heads/foo @@ -0,0 +1 @@ +f1b58b97808f8e744f6a23c693859df5b5968901 diff --git a/testdata/repos/staged/dotGit/refs/heads/main b/testdata/repos/staged/dotGit/refs/heads/main new file mode 100644 index 0000000..a06d4d3 --- /dev/null +++ b/testdata/repos/staged/dotGit/refs/heads/main @@ -0,0 +1 @@ +bf3f24164d7256b4021575cbdb2f97b98e6f057e diff --git a/testdata/repos/staged/dotGit/refs/heads/remove-secrets b/testdata/repos/staged/dotGit/refs/heads/remove-secrets new file mode 100644 index 0000000..31b6c78 --- /dev/null +++ b/testdata/repos/staged/dotGit/refs/heads/remove-secrets @@ -0,0 +1 @@ +a122b33c6bad3ee54724f52f2caad385ab1982ab diff --git a/testdata/repos/staged/dotGit/refs/remotes/origin/HEAD b/testdata/repos/staged/dotGit/refs/remotes/origin/HEAD new file mode 100644 index 0000000..4b0a875 --- /dev/null +++ b/testdata/repos/staged/dotGit/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +ref: refs/remotes/origin/main diff --git a/testdata/repos/staged/dotGit/refs/remotes/origin/api-pkg b/testdata/repos/staged/dotGit/refs/remotes/origin/api-pkg new file mode 100644 index 0000000..31b6c78 --- /dev/null +++ b/testdata/repos/staged/dotGit/refs/remotes/origin/api-pkg @@ -0,0 +1 @@ +a122b33c6bad3ee54724f52f2caad385ab1982ab diff --git a/testdata/repos/staged/dotGit/refs/remotes/origin/foo b/testdata/repos/staged/dotGit/refs/remotes/origin/foo new file mode 100644 index 0000000..57d5848 --- /dev/null +++ b/testdata/repos/staged/dotGit/refs/remotes/origin/foo @@ -0,0 +1 @@ +f1b58b97808f8e744f6a23c693859df5b5968901 diff --git a/testdata/repos/staged/dotGit/refs/remotes/origin/main b/testdata/repos/staged/dotGit/refs/remotes/origin/main new file mode 100644 index 0000000..98f12e9 --- /dev/null +++ b/testdata/repos/staged/dotGit/refs/remotes/origin/main @@ -0,0 +1 @@ +2e1db472eeba53f06c4026ae4566ea022e36598e diff --git a/testdata/repos/staged/main.go b/testdata/repos/staged/main.go new file mode 100644 index 0000000..9a932e3 --- /dev/null +++ b/testdata/repos/staged/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "os" +) + +func main() { + + var a = "initial" + fmt.Println(a) + + var b, c int = 1, 2 + fmt.Println(b, c) + + var d = true + fmt.Println(d) + + var e int + fmt.Println(e) + + // load secret via env + awsToken := os.Getenv("AWS_TOKEN") + + f := "apple" + fmt.Println(f) +} diff --git a/testdata/repos/symlinks/file_symlink/symlinked_id_ed25519 b/testdata/repos/symlinks/file_symlink/symlinked_id_ed25519 new file mode 120000 index 0000000..fd0203d --- /dev/null +++ b/testdata/repos/symlinks/file_symlink/symlinked_id_ed25519 @@ -0,0 +1 @@ +../source_file/id_ed25519 \ No newline at end of file diff --git a/testdata/repos/symlinks/source_file/id_ed25519 b/testdata/repos/symlinks/source_file/id_ed25519 new file mode 100644 index 0000000..9f4ff61 --- /dev/null +++ b/testdata/repos/symlinks/source_file/id_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACA8YWKYztuuvxUIMomc3zv0OdXCT57Cc2cRYu3TMbX9XAAAAJDiKO3C4ijt +wgAAAAtzc2gtZWQyNTUxOQAAACA8YWKYztuuvxUIMomc3zv0OdXCT57Cc2cRYu3TMbX9XA +AAAECzmj8DGxg5YHtBK4AmBttMXDQHsPAaCyYHQjJ4YujRBTxhYpjO266/FQgyiZzfO/Q5 +1cJPnsJzZxFi7dMxtf1cAAAADHJvb3RAZGV2aG9zdAE= +-----END OPENSSH PRIVATE KEY----- diff --git a/version/version.go b/version/version.go new file mode 100644 index 0000000..15fc788 --- /dev/null +++ b/version/version.go @@ -0,0 +1,5 @@ +package version + +// these two gotta be the same +var DefaultMsg = "version is set by build process" +var Version = "version is set by build process"