
Implementing Secret Scanning With Gitleaks
- 200 installs
- 27.3k repo stars
- Updated August 2, 2026
- mukul975/anthropic-cybersecurity-skills
Helps with ai & agent building tasks.
About
implementing-secret-scanning-with-gitleaks is a Claude Code skill in the AI & Agent Building category.
- implementing-secret-scanning-with-gitleaks
- AI & Agent Building
- AI-coding skill
Implementing Secret Scanning With Gitleaks by the numbers
- 200 all-time installs (skills.sh)
- +26 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #2,891 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mukul975/anthropic-cybersecurity-skills --skill implementing-secret-scanning-with-gitleaksAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 200 |
|---|---|
| repo stars | ★ 27.3k |
| Last updated | August 2, 2026 |
| Repository | mukul975/anthropic-cybersecurity-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Implementing Secret Scanning with Gitleaks
When to Use
- When developers may accidentally commit API keys, passwords, tokens, or private keys to repositories
- When establishing pre-commit gates that prevent secrets from entering the git history
- When scanning existing repository history for previously committed secrets that need rotation
- When compliance requirements mandate secret detection across all source code repositories
- When migrating from manual secret audits to automated continuous scanning
Do not use for detecting secrets in running applications or memory (use runtime secret detection), for managing secrets after detection (use Vault or AWS Secrets Manager), or for scanning container images (use Trivy or Grype).
Prerequisites
- Gitleaks v8.18+ installed via binary, Go install, or Docker
- Pre-commit framework installed for local hook integration
- Git repository with history to scan
- CI/CD platform access (GitHub Actions, GitLab CI, or equivalent)
Workflow
Step 1: Install and Run Initial Repository Scan
Perform a baseline scan of the repository to identify all existing secrets in the git history.
# Install Gitleaks
brew install gitleaks # macOS
# or download binary from https://github.com/gitleaks/gitleaks/releases
# Scan entire git history for secrets
gitleaks detect --source . --report-format json --report-path gitleaks-report.json -v
# Scan only staged changes (for pre-commit)
gitleaks protect --staged --report-format json --report-path gitleaks-staged.json
# Scan specific commit range
gitleaks detect --source . --log-opts="HEAD~10..HEAD" --report-format json
# Scan without git history (filesystem only)
gitleaks detect --source . --no-git --report-format jsonStep 2: Configure Pre-Commit Hook
Set up Gitleaks as a pre-commit hook to prevent secrets from being committed.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
name: gitleaks
description: Detect hardcoded secrets using Gitleaks
entry: gitleaks protect --staged --verbose --redact
language: golang
pass_filenames: false# Install pre-commit framework
pip install pre-commit
# Install hooks defined in .pre-commit-config.yaml
pre-commit install
# Run against all files (not just staged)
pre-commit run gitleaks --all-files
# Test the hook with a deliberate secret
echo 'AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"' >> test.txt
git add test.txt
git commit -m "test" # Should be blocked by gitleaksStep 3: Integrate into GitHub Actions
# .github/workflows/secret-scanning.yml
name: Secret Scanning
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
gitleaks:
name: Gitleaks Secret Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for comprehensive scanning
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} # Required for gitleaks-action v2
# Alternative: Run Gitleaks directly
- name: Install Gitleaks
run: |
wget -q https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz
tar -xzf gitleaks_8.21.2_linux_x64.tar.gz
chmod +x gitleaks
- name: Scan for secrets
run: |
if [ "${{ github.event_name }}" == "pull_request" ]; then
./gitleaks detect \
--source . \
--log-opts="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" \
--report-format sarif \
--report-path gitleaks.sarif \
--exit-code 1
else
./gitleaks detect \
--source . \
--report-format sarif \
--report-path gitleaks.sarif \
--exit-code 1 \
--baseline-path .gitleaks-baseline.json
fi
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: gitleaks.sarif
category: gitleaksStep 4: Author Custom Detection Rules
Create organization-specific rules for internal secret patterns.
# .gitleaks.toml
title = "Organization Gitleaks Configuration"
[extend]
useDefault = true # Include all default rules
# Custom rule for internal API tokens
[[rules]]
id = "internal-api-token"
description = "Internal API token for service-to-service auth"
regex = '''(?i)x-internal-token["\s:=]+["\']?([a-zA-Z0-9_\-]{40,})["\']?'''
entropy = 3.5
keywords = ["x-internal-token"]
tags = ["internal", "api"]
[[rules]]
id = "database-connection-string"
description = "Database connection string with embedded credentials"
regex = '''(?i)(postgres|mysql|mongodb|redis)://[^:]+:[^@]+@[^/]+/\w+'''
keywords = ["postgres://", "mysql://", "mongodb://", "redis://"]
tags = ["database", "credentials"]
[[rules]]
id = "jwt-secret"
description = "JWT signing secret"
regex = '''(?i)(jwt[_-]?secret|jwt[_-]?key)["\s:=]+["\']?([a-zA-Z0-9/+_\-]{32,})["\']?'''
entropy = 3.0
keywords = ["jwt_secret", "jwt-secret", "jwt_key", "jwt-key"]
# Allowlist for test files and known safe patterns
[allowlist]
description = "Global allowlist"
paths = [
'''(^|/)test(s)?/''',
'''(^|/)spec/''',
'''\.test\.(js|ts|py)$''',
'''\.spec\.(js|ts|py)$''',
'''__mocks__/''',
'''fixtures/''',
'''(^|/)vendor/''',
'''node_modules/'''
]
regexes = [
'''EXAMPLE''',
'''example\.com''',
'''test[-_]?(key|secret|token|password)''',
'''(?i)placeholder''',
'''000000+'''
]Step 5: Manage Baselines for Existing Repositories
Create a baseline of known findings to avoid blocking development while historical secrets are being rotated.
# Generate baseline from current state
gitleaks detect --source . --report-format json --report-path .gitleaks-baseline.json
# Subsequent scans compare against baseline (only new findings trigger failures)
gitleaks detect --source . --baseline-path .gitleaks-baseline.json --exit-code 1
# Review baseline periodically and remove entries as secrets are rotated
cat .gitleaks-baseline.json | python3 -m json.tool | head -50Step 6: Remediate Exposed Secrets
When a secret is detected, follow the rotation and history cleanup procedure.
# 1. Immediately rotate the exposed credential
# - Revoke the old API key/token in the service provider
# - Generate a new credential
# - Store the new credential in a secrets manager
# 2. Remove secret from git history using git-filter-repo
pip install git-filter-repo
# Create expressions file for secrets to remove
cat > /tmp/expressions.txt << 'EOF'
regex:AKIA[0-9A-Z]{16}==>REDACTED_AWS_KEY
regex:(?i)password\s*=\s*"[^"]*"==>password="REDACTED"
EOF
git filter-repo --replace-text /tmp/expressions.txt --force
# 3. Force-push the cleaned history (coordinate with team)
# git push --force --all # WARNING: Requires team coordination
# 4. Add the secret pattern to .gitleaks.toml rules
# 5. Update the baseline file to remove the resolved findingKey Concepts
| Term | Definition |
|---|---|
| Secret | Any credential, token, key, or sensitive string that should not appear in source code |
| Pre-commit Hook | Git hook that runs before a commit is created, blocking commits containing detected secrets |
| Entropy | Measure of randomness in a string; high-entropy strings are more likely to be secrets |
| Baseline | Snapshot of existing findings used to differentiate new secrets from pre-existing ones |
| Allowlist | Configuration specifying paths, patterns, or commits to exclude from detection |
| SARIF | Static Analysis Results Interchange Format for uploading findings to security dashboards |
| git-filter-repo | Tool for rewriting git history to remove sensitive data from all commits |
Tools & Systems
- Gitleaks: Open-source secret detection tool supporting pre-commit hooks, CI/CD, and historical scanning
- pre-commit: Framework for managing and maintaining multi-language pre-commit hooks
- git-filter-repo: History rewriting tool for removing secrets from git history
- TruffleHog: Alternative secret scanner with verified secret detection capabilities
- GitHub Secret Scanning: Native GitHub feature that detects secrets matching partner patterns
Common Scenarios
Scenario: Onboarding Secret Scanning on a Legacy Repository
Context: A 5-year-old repository has never been scanned. The team needs to enable secret scanning without blocking all development while historical secrets are rotated.
Approach: 1. Run gitleaks detect against full history and generate a baseline JSON file 2. Triage each finding: classify as active (needs rotation), inactive (already rotated), or false positive 3. Immediately rotate all active secrets and update consuming services 4. Commit the baseline file (excluding active secrets that have been fixed) 5. Enable pre-commit hooks for new development immediately 6. Add CI/CD scanning with the baseline to catch only new secrets 7. Progressively reduce the baseline as historical secrets are rotated
Pitfalls: Generating a baseline without triaging means accepting risk on unrotated secrets. Never assume a historical secret is inactive without verifying with the service provider. Running git-filter-repo on a shared repository without coordination will cause rebase conflicts for all team members.
Output Format
Gitleaks Secret Scanning Report
=================================
Repository: org/web-application
Scan Type: Full History
Commits Scanned: 4,523
Date: 2026-02-23
FINDINGS:
Total: 12
New (not in baseline): 3
Baseline (pre-existing): 9
NEW FINDINGS (blocking):
[1] AWS Access Key ID
Rule: aws-access-key-id
File: src/config/aws.py:23
Commit: a1b2c3d (2026-02-22, dev@company.com)
Secret: AKIA...REDACTED
Entropy: 3.8
[2] GitHub Personal Access Token
Rule: github-pat
File: scripts/deploy.sh:15
Commit: d4e5f6g (2026-02-21, ops@company.com)
Secret: ghp_...REDACTED
Entropy: 4.2
[3] Internal API Token
Rule: internal-api-token
File: src/services/auth.py:89
Commit: h7i8j9k (2026-02-20, dev@company.com)
QUALITY GATE: FAILED (3 new findings)
Action: Rotate exposed credentials immediately.Gitleaks Secret Scanning Templates
Pre-Commit Configuration
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
name: Detect secrets with Gitleaks
entry: gitleaks protect --staged --verbose --redact
language: golang
pass_filenames: falseOrganization Gitleaks Configuration
# .gitleaks.toml
title = "Organization Secret Scanning Rules"
[extend]
useDefault = true
# ─── Custom Rules ───
[[rules]]
id = "internal-service-token"
description = "Internal service-to-service authentication token"
regex = '''(?i)(service[_-]?token|internal[_-]?key)["\s:=]+["']?([A-Za-z0-9_\-]{36,})["']?'''
entropy = 3.5
keywords = ["service_token", "service-token", "internal_key", "internal-key"]
[[rules]]
id = "database-url-with-password"
description = "Database connection URL with embedded password"
regex = '''(?i)(DATABASE_URL|DB_URL|SQLALCHEMY_DATABASE_URI)\s*=\s*["']?(postgres|mysql|mongodb)\+?[a-z]*://[^:]+:[^@]+@'''
keywords = ["DATABASE_URL", "DB_URL", "SQLALCHEMY_DATABASE_URI"]
[[rules]]
id = "encryption-key-hex"
description = "Encryption key in hexadecimal format"
regex = '''(?i)(encryption[_-]?key|aes[_-]?key|secret[_-]?key)\s*=\s*["']?([0-9a-fA-F]{32,64})["']?'''
entropy = 3.0
keywords = ["encryption_key", "aes_key", "secret_key"]
# ─── Allowlist ───
[allowlist]
description = "Global allowlist for false positive reduction"
paths = [
'''(^|/)test(s)?/''',
'''(^|/)spec(s)?/''',
'''\.test\.(js|ts|py|go|rb)$''',
'''\.spec\.(js|ts|py|go|rb)$''',
'''(^|/)__tests__/''',
'''(^|/)__mocks__/''',
'''(^|/)fixtures/''',
'''(^|/)testdata/''',
'''(^|/)vendor/''',
'''(^|/)node_modules/''',
'''\.md$''',
'''CHANGELOG'''
]
regexes = [
'''(?i)EXAMPLE''',
'''(?i)PLACEHOLDER''',
'''(?i)CHANGEME''',
'''(?i)your[-_]?(api[-_]?key|secret|token|password)''',
'''(?i)test[-_]?(key|secret|token|password|credential)''',
'''example\.com''',
'''localhost''',
'''0{8,}''',
'''x{8,}'''
]
# Per-rule allowlists
[[rules.allowlist]]
description = "Allow AWS example keys"
regexes = ["AKIAIOSFODNN7EXAMPLE"]GitHub Actions Workflow
# .github/workflows/secret-scanning.yml
name: Secret Scanning
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * *'
jobs:
gitleaks:
name: Gitleaks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Gitleaks
run: |
GITLEAKS_VERSION="8.21.2"
wget -qO- "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar xz
sudo mv gitleaks /usr/local/bin/
- name: Run scan
run: |
if [ "${{ github.event_name }}" == "pull_request" ]; then
gitleaks detect \
--source . \
--log-opts="${{ github.event.pull_request.base.sha }}..${{ github.sha }}" \
--report-format sarif \
--report-path gitleaks.sarif \
--exit-code 1 \
--verbose
else
gitleaks detect \
--source . \
--baseline-path .gitleaks-baseline.json \
--report-format sarif \
--report-path gitleaks.sarif \
--exit-code 1 \
--verbose
fi
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: gitleaks.sarif
category: gitleaksIncident Response Template for Exposed Secrets
## Secret Exposure Incident Report
**Date Detected**: YYYY-MM-DD
**Detected By**: Gitleaks CI scan / Pre-commit hook / Manual review
**Repository**: org/repo-name
**Severity**: Critical / High
### Exposed Credential Details
- **Type**: [AWS Access Key | GitHub PAT | Database Password | etc.]
- **Rule ID**: [gitleaks rule that detected it]
- **File**: [path/to/file:line]
- **Commit**: [short SHA]
- **Author**: [email]
- **Date Committed**: [date]
- **Exposure Duration**: [time from commit to detection]
### Remediation Actions
- [ ] Credential revoked at service provider
- [ ] New credential generated and stored in secrets manager
- [ ] Consuming services updated to use new credential
- [ ] Service functionality verified
- [ ] Git history cleaned (if required)
- [ ] Baseline updated
- [ ] Root cause documented
### Root Cause
[Why was the secret committed? Missing pre-commit hook? Developer education gap?]
### Preventive Measures
[What changes prevent recurrence? Hook enforcement? Rule addition?]
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. Please do not remove or change
the license header comment from a contributed file except when
necessary.
Copyright 2026 mukul975
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
API Reference: Gitleaks Secret Scanning
Libraries Used
| Library | Purpose |
|---|---|
subprocess | Execute gitleaks CLI commands |
json | Parse gitleaks JSON report output |
pathlib | Handle repository and report file paths |
os | Read GITLEAKS_CONFIG environment variable |
Installation
# Install gitleaks binary
# macOS
brew install gitleaks
# Linux
curl -sSfL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_linux_x64 -o gitleaks
chmod +x gitleaks && sudo mv gitleaks /usr/local/bin/
# Docker
docker pull ghcr.io/gitleaks/gitleaks:latestCLI Commands
Scan a Git Repository
gitleaks git --source=/path/to/repo --report-format=json --report-path=results.jsonScan a Directory (Non-Git)
gitleaks dir --source=/path/to/code --report-format=json --report-path=results.jsonScan from stdin
echo "aws_secret_access_key=AKIAIOSFODNN7EXAMPLE" | gitleaks stdinKey CLI Flags
| Flag | Description |
|---|---|
--source | Path to repository or directory to scan |
--config, -c | Path to custom gitleaks.toml config |
--report-format, -f | Output format: json, csv, junit, sarif |
--report-path, -r | Path to write the report file |
--baseline-path | Ignore known findings from baseline file |
--exit-code | Exit code when leaks found (default: 1) |
--redact | Redact secrets in output (percent: 0-100) |
--verbose, -v | Show verbose scan output |
--no-git | Treat source as plain directory |
--log-level | Log level: trace, debug, info, warn, error |
--max-target-megabytes | Skip files larger than this size |
Custom Configuration (.gitleaks.toml)
title = "Custom Gitleaks Config"
[extend]
useDefault = true # Extend the default ruleset
[[rules]]
id = "custom-internal-token"
description = "Internal API token pattern"
regex = '''(?i)internal[_-]?token\s*[:=]\s*['"]?([a-zA-Z0-9]{32,})'''
tags = ["internal", "token"]
keywords = ["internal_token", "internal-token"]
[[rules]]
id = "custom-db-password"
description = "Database password in config"
regex = '''(?i)(db|database|mysql|postgres)[_-]?pass(word)?\s*[:=]\s*['"]?[^\s'"]{8,}'''
tags = ["database", "password"]
[rules.allowlist]
paths = ['''test/.*''', '''mock/.*''']
regexTarget = "line"
regexes = ['''(?i)example|placeholder|changeme|test''']
[[allowlist.paths]]
regex = '''vendor/.*'''
[[allowlist.commits]]
sha = "abc123def456"Python Integration
Run Gitleaks and Parse Results
import subprocess
import json
from pathlib import Path
def scan_repository(repo_path, config_path=None):
cmd = [
"gitleaks", "git",
"--source", str(repo_path),
"--report-format", "json",
"--report-path", "/tmp/gitleaks-report.json",
"--exit-code", "0",
]
if config_path:
cmd.extend(["--config", str(config_path)])
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
report_path = Path("/tmp/gitleaks-report.json")
if report_path.exists():
with open(report_path) as f:
findings = json.load(f)
return findings
return []Categorize Findings by Severity
HIGH_SEVERITY_RULES = {
"aws-access-key", "aws-secret-key", "gcp-api-key",
"github-pat", "private-key", "generic-api-key",
}
def categorize_findings(findings):
high, medium, low = [], [], []
for f in findings:
rule = f.get("RuleID", "")
if rule in HIGH_SEVERITY_RULES:
high.append(f)
elif "password" in rule or "token" in rule:
medium.append(f)
else:
low.append(f)
return {"high": high, "medium": medium, "low": low}GitHub Actions Integration
name: Gitleaks Secret Scan
on: [push, pull_request]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Output Format
[
{
"Description": "Detected a Generic API Key",
"StartLine": 42,
"EndLine": 42,
"StartColumn": 15,
"EndColumn": 55,
"Match": "REDACTED",
"Secret": "REDACTED",
"File": "config/settings.py",
"Commit": "a1b2c3d4e5f6",
"Author": "developer@example.com",
"Date": "2025-01-15T10:30:00Z",
"RuleID": "generic-api-key",
"Tags": ["api", "key"],
"Fingerprint": "a1b2c3d4:config/settings.py:generic-api-key:42"
}
]Standards Reference: Secret Scanning with Gitleaks
OWASP Top 10 - A07:2021 Identification and Authentication Failures
- Hardcoded credentials in source code enable unauthorized access
- Gitleaks detects API keys, passwords, tokens, and private keys before they reach repositories
- CWE-798: Use of Hard-coded Credentials is a direct mapping
NIST SSDF (SP 800-218)
PW.1: Design Software to Meet Security Requirements
- PW.1.1: Identify security requirements including credential management
- Secrets should never be stored in source code; use environment variables or secrets managers
PS.1: Protect All Forms of Code
- PS.1.1: Store code securely with access controls and secret scanning
- Implement pre-commit hooks to prevent secrets from entering version control
PS.2: Provide a Mechanism for Verifying Software Release Integrity
- Code signing and secret scanning ensure software releases do not contain embedded credentials
CIS Software Supply Chain Security Guide
Source Code Controls
- SC-1: Automated secret scanning on all commits
- SC-5: No hardcoded credentials in source repositories
- SC-6: Secrets detection integrated into CI/CD pipeline
OWASP SAMM - Secure Build
Maturity Level 1
- Scan repositories for common secret patterns using default rulesets
- Alert developers when secrets are detected in pull requests
Maturity Level 2
- Custom rules for organization-specific secret patterns
- Pre-commit hooks prevent secrets from entering history
- Baseline management for legacy codebases
Maturity Level 3
- Automated secret rotation workflow triggered by detection
- Correlation with secrets management systems for validation
- Historical scanning with git-filter-repo remediation
PCI DSS v4.0
- Requirement 6.3.1: Security vulnerabilities identified through a defined process including code analysis
- Requirement 8.6.1: Secrets stored in code or configuration files must be protected
- Requirement 8.6.3: Passwords/passphrases for application and system accounts are protected against misuse
SOC 2 Trust Service Criteria
- CC6.1: Logical access security over protected information assets including credential management
- CC6.7: Restrict transmission of data to authorized external parties (prevent credential leakage)
Workflow Reference: Secret Scanning with Gitleaks
Secret Detection Pipeline
Developer Workstation CI/CD Pipeline Security Response
│ │ │
▼ │ │
┌──────────────┐ │ │
│ Pre-commit │ │ │
│ Hook (local) │ │ │
└──────┬───────┘ │ │
│ │ │
┌────┴────┐ │ │
│ │ │ │
PASS FAIL │ │
│ (blocked) │ │
▼ │ │
Push to │ │
Remote │ │
│ ▼ │
│ ┌──────────────┐ │
└───────────────────────>│ Gitleaks CI │ │
│ Scan (PR) │ │
└──────┬───────┘ │
│ │
┌─────┴─────┐ │
│ │ │
PASS FAIL │
│ ┌─────┴──────┐ │
│ │ Block PR │ │
│ │ + Alert │──────────────>│
│ └────────────┘ ┌──────────┴──────┐
│ │ Rotate Secret │
│ │ Update Baseline │
│ │ Clean History │
│ └─────────────────┘
▼
Merge PermittedGitleaks Rule Configuration Deep Dive
Rule Anatomy
[[rules]]
id = "rule-unique-identifier" # Unique rule ID
description = "Human-readable desc" # What this rule detects
regex = '''pattern''' # Detection regex
entropy = 3.5 # Minimum entropy threshold (optional)
secretGroup = 1 # Regex capture group containing secret
keywords = ["key1", "key2"] # Fast pre-filter keywords
tags = ["aws", "credential"] # Categorization tags
path = '''\.env$''' # Path filter regex (optional)Built-in Rule Categories
| Category | Example Rules | Count |
|---|---|---|
| Cloud Provider Keys | aws-access-key-id, gcp-service-account | 15+ |
| API Tokens | github-pat, gitlab-pat, slack-token | 20+ |
| Private Keys | private-key, rsa-private-key | 5+ |
| Database Credentials | generic-password, connection-string | 10+ |
| Service Tokens | stripe-api-key, sendgrid-api-key | 30+ |
Entropy Scoring
- Entropy measures string randomness (Shannon entropy)
- Random-looking strings (API keys) have entropy > 3.5
- Regular English text has entropy around 2.0-3.0
- Setting entropy threshold reduces false positives on non-random strings
- Combine entropy with regex for highest accuracy
Remediation Process
Secret Rotation Checklist
1. Identify the exposed secret type and associated service 2. Log into the service provider and revoke the exposed credential 3. Generate a new credential with the same permissions 4. Store the new credential in a secrets manager (Vault, AWS SM, etc.) 5. Update all consuming services to use the new credential 6. Verify service functionality with the new credential 7. Update the Gitleaks baseline to remove the resolved finding 8. Optionally clean git history with git-filter-repo
History Cleanup Decision Matrix
| Factor | Clean History | Keep History |
|---|---|---|
| Secret is rotated | Optional | Acceptable |
| Repo is public | Required | Never |
| Compliance mandate | Required | Not compliant |
| Active contributor count | < 10 preferred | > 10 difficult |
| Secret exposure duration | Long (high risk) | Short (lower risk) |
#!/usr/bin/env python3
"""Gitleaks secret scanning agent.
Wraps the Gitleaks CLI to scan git repositories, directories, or
specific commits for hardcoded secrets, API keys, tokens, and
credentials. Parses JSON output into structured findings.
"""
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime, timezone
def find_gitleaks_binary():
"""Locate the gitleaks binary on the system."""
custom_path = os.environ.get("GITLEAKS_PATH")
if custom_path and os.path.isfile(custom_path):
return custom_path
for name in ["gitleaks", "gitleaks.exe"]:
for directory in os.environ.get("PATH", "").split(os.pathsep):
full_path = os.path.join(directory, name)
if os.path.isfile(full_path):
return full_path
print("[!] gitleaks binary not found. Install: https://github.com/gitleaks/gitleaks",
file=sys.stderr)
sys.exit(1)
def run_detect(gitleaks_bin, target, config=None, baseline=None,
log_opts=None, no_git=False, verbose=False):
"""Run gitleaks detect on a repository or directory."""
cmd = [gitleaks_bin, "detect"]
if no_git:
cmd.extend(["--no-git", "--source", target])
else:
cmd.extend(["--source", target])
cmd.extend(["--report-format", "json", "--report-path", "/dev/stdout"])
if config:
cmd.extend(["--config", config])
if baseline:
cmd.extend(["--baseline-path", baseline])
if log_opts:
cmd.extend(["--log-opts", log_opts])
if verbose:
cmd.append("--verbose")
cmd.extend(["--exit-code", "0"])
print(f"[*] Running: {' '.join(cmd)}")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=600,
)
if result.returncode not in (0, 1):
print(f"[!] Gitleaks error (exit {result.returncode}): {result.stderr}",
file=sys.stderr)
return result.stdout, result.stderr, result.returncode
def run_protect(gitleaks_bin, target, config=None, staged=False, verbose=False):
"""Run gitleaks protect for pre-commit scanning."""
cmd = [gitleaks_bin, "protect", "--source", target]
cmd.extend(["--report-format", "json", "--report-path", "/dev/stdout"])
if staged:
cmd.append("--staged")
if config:
cmd.extend(["--config", config])
if verbose:
cmd.append("--verbose")
cmd.extend(["--exit-code", "0"])
print(f"[*] Running protect mode: {' '.join(cmd)}")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=300,
)
return result.stdout, result.stderr, result.returncode
def parse_findings(raw_json):
"""Parse gitleaks JSON output into structured findings."""
findings = []
if not raw_json or not raw_json.strip():
return findings
try:
results = json.loads(raw_json)
except json.JSONDecodeError:
return findings
if not isinstance(results, list):
results = [results]
for item in results:
secret_val = item.get("Secret", "")
findings.append({
"rule_id": item.get("RuleID", "unknown"),
"description": item.get("Description", ""),
"secret": secret_val[:8] + "..." if secret_val else "",
"file": item.get("File", ""),
"line": item.get("StartLine", 0),
"commit": (item.get("Commit", "") or "")[:12],
"author": item.get("Author", ""),
"email": item.get("Email", ""),
"date": item.get("Date", ""),
"message": (item.get("Message", "") or "")[:80],
"entropy": item.get("Entropy", 0),
"fingerprint": item.get("Fingerprint", ""),
"tags": item.get("Tags", []),
})
return findings
def format_summary(findings, target):
"""Print human-readable summary of secrets found."""
print(f"\n{'='*60}")
print(f" Gitleaks Secret Scan Report")
print(f"{'='*60}")
print(f" Target : {target}")
print(f" Secrets : {len(findings)}")
if not findings:
print(f" Status : CLEAN - No secrets detected")
print(f"{'='*60}")
return
by_rule = {}
for f in findings:
rule = f["rule_id"]
by_rule.setdefault(rule, []).append(f)
print(f"\n Findings by Rule:")
for rule, items in sorted(by_rule.items(), key=lambda x: -len(x[1])):
print(f" {rule:40s}: {len(items)} occurrence(s)")
by_file = {}
for f in findings:
by_file.setdefault(f["file"], []).append(f)
print(f"\n Affected Files: {len(by_file)}")
for filepath, items in sorted(by_file.items(), key=lambda x: -len(x[1]))[:10]:
print(f" {filepath} ({len(items)} secret(s))")
print(f"\n Top Findings:")
for f in findings[:15]:
print(f" [{f['rule_id']:30s}] {f['file']}:{f['line']} "
f"(commit: {f['commit']}, secret: {f['secret']})")
def main():
parser = argparse.ArgumentParser(
description="Gitleaks secret scanning agent"
)
parser.add_argument("--target", required=True,
help="Repository path or directory to scan")
parser.add_argument("--mode", choices=["detect", "protect"], default="detect",
help="Scan mode: detect (full history) or protect (pre-commit)")
parser.add_argument("--config", help="Path to custom .gitleaks.toml config")
parser.add_argument("--baseline", help="Path to baseline file for known findings")
parser.add_argument("--log-opts", help="Git log options (e.g., '--since=2026-01-01')")
parser.add_argument("--no-git", action="store_true",
help="Scan files without git history")
parser.add_argument("--staged", action="store_true",
help="Only scan staged changes (protect mode)")
parser.add_argument("--output", "-o", help="Output JSON report path")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
gitleaks_bin = find_gitleaks_binary()
print(f"[*] Using gitleaks: {gitleaks_bin}")
if args.mode == "protect":
raw_json, stderr, exit_code = run_protect(
gitleaks_bin, args.target, args.config, args.staged, args.verbose
)
else:
raw_json, stderr, exit_code = run_detect(
gitleaks_bin, args.target, args.config, args.baseline,
args.log_opts, args.no_git, args.verbose
)
findings = parse_findings(raw_json)
format_summary(findings, args.target)
report = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"tool": "gitleaks",
"target": args.target,
"mode": args.mode,
"secrets_found": len(findings),
"findings": findings,
"risk_level": (
"CRITICAL" if len(findings) > 10
else "HIGH" if len(findings) > 0
else "LOW"
),
}
if args.output:
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
print(f"\n[+] Report saved to {args.output}")
elif args.verbose:
print(json.dumps(report, indent=2))
if findings:
print(f"\n[!] {len(findings)} secret(s) detected - remediation required")
else:
print(f"\n[+] No secrets detected")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Gitleaks Secret Scanning Pipeline Script
Runs Gitleaks scans, manages baselines, evaluates findings,
and generates remediation reports.
Usage:
python process.py --repo-path /path/to/repo --scan-type detect
python process.py --repo-path . --scan-type protect --staged
python process.py --repo-path . --baseline .gitleaks-baseline.json --output report.json
"""
import argparse
import json
import os
import subprocess
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
@dataclass
class SecretFinding:
rule_id: str
description: str
file: str
line: int
commit: str
author: str
date: str
secret: str
entropy: float
match: str
tags: list = field(default_factory=list)
is_new: bool = True
@dataclass
class ScanResult:
findings: list = field(default_factory=list)
new_findings: list = field(default_factory=list)
baseline_findings: list = field(default_factory=list)
commits_scanned: int = 0
scan_duration: float = 0.0
error: str = ""
def run_gitleaks(repo_path: str, scan_type: str = "detect",
baseline_path: Optional[str] = None,
commit_range: Optional[str] = None,
staged: bool = False,
config_path: Optional[str] = None) -> dict:
"""Execute Gitleaks and return JSON results."""
cmd = ["gitleaks", scan_type, "--source", repo_path,
"--report-format", "json", "--report-path", "/dev/stdout"]
if baseline_path and os.path.exists(baseline_path):
cmd.extend(["--baseline-path", baseline_path])
if commit_range:
cmd.extend(["--log-opts", commit_range])
if staged:
cmd.append("--staged")
if config_path:
cmd.extend(["--config", config_path])
cmd.append("--verbose")
try:
proc = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=300
)
findings = []
if proc.stdout.strip():
try:
findings = json.loads(proc.stdout)
except json.JSONDecodeError:
pass
return {
"findings": findings if isinstance(findings, list) else [],
"exit_code": proc.returncode,
"stderr": proc.stderr
}
except subprocess.TimeoutExpired:
return {"findings": [], "exit_code": -1, "stderr": "Scan timed out after 300s"}
except FileNotFoundError:
return {"findings": [], "exit_code": -1,
"stderr": "gitleaks not found. Install from https://github.com/gitleaks/gitleaks"}
def parse_findings(raw_findings: list) -> list:
"""Parse raw Gitleaks JSON findings into SecretFinding objects."""
findings = []
for f in raw_findings:
redacted_secret = redact_secret(f.get("Secret", ""))
findings.append(SecretFinding(
rule_id=f.get("RuleID", "unknown"),
description=f.get("Description", ""),
file=f.get("File", ""),
line=f.get("StartLine", 0),
commit=f.get("Commit", "")[:8],
author=f.get("Author", ""),
date=f.get("Date", ""),
secret=redacted_secret,
entropy=f.get("Entropy", 0.0),
match=f.get("Match", "")[:100],
tags=f.get("Tags", [])
))
return findings
def redact_secret(secret: str) -> str:
"""Redact a secret, showing only first 4 and last 4 characters."""
if len(secret) <= 12:
return "*" * len(secret)
return secret[:4] + "..." + secret[-4:]
def load_baseline(baseline_path: str) -> set:
"""Load baseline fingerprints for comparison."""
if not os.path.exists(baseline_path):
return set()
with open(baseline_path, "r") as f:
try:
baseline = json.load(f)
except json.JSONDecodeError:
return set()
fingerprints = set()
for entry in baseline:
fp = f"{entry.get('RuleID', '')}:{entry.get('File', '')}:{entry.get('Commit', '')}"
fingerprints.add(fp)
return fingerprints
def classify_findings(findings: list, baseline_fingerprints: set) -> tuple:
"""Separate findings into new and baseline (pre-existing)."""
new_findings = []
baseline_findings = []
for f in findings:
fp = f"{f.rule_id}:{f.file}:{f.commit}"
if fp in baseline_fingerprints:
f.is_new = False
baseline_findings.append(f)
else:
f.is_new = True
new_findings.append(f)
return new_findings, baseline_findings
def generate_report(scan_result: ScanResult, repo_path: str) -> dict:
"""Generate a structured scan report."""
rule_summary = {}
for f in scan_result.findings:
rule_summary[f.rule_id] = rule_summary.get(f.rule_id, 0) + 1
author_summary = {}
for f in scan_result.new_findings:
author_summary[f.author] = author_summary.get(f.author, 0) + 1
return {
"report_metadata": {
"repository": repo_path,
"scan_date": datetime.now(timezone.utc).isoformat(),
"duration_seconds": scan_result.scan_duration,
"commits_scanned": scan_result.commits_scanned
},
"summary": {
"total_findings": len(scan_result.findings),
"new_findings": len(scan_result.new_findings),
"baseline_findings": len(scan_result.baseline_findings),
"unique_rules_triggered": len(rule_summary),
"rules_breakdown": rule_summary,
"authors_with_new_findings": author_summary
},
"quality_gate": {
"passed": len(scan_result.new_findings) == 0,
"blocking_count": len(scan_result.new_findings)
},
"new_findings": [
{
"rule_id": f.rule_id,
"description": f.description,
"file": f.file,
"line": f.line,
"commit": f.commit,
"author": f.author,
"date": f.date,
"secret_preview": f.secret,
"entropy": f.entropy
}
for f in scan_result.new_findings
],
"remediation_steps": [
f"1. Rotate the {f.rule_id} credential found in {f.file}"
for f in scan_result.new_findings
]
}
def main():
parser = argparse.ArgumentParser(description="Gitleaks Secret Scanning Pipeline")
parser.add_argument("--repo-path", required=True, help="Path to git repository")
parser.add_argument("--scan-type", default="detect", choices=["detect", "protect"],
help="Scan type: detect (history) or protect (staged/pre-commit)")
parser.add_argument("--baseline", default=None, help="Path to baseline JSON file")
parser.add_argument("--commit-range", default=None,
help="Git log range (e.g., HEAD~10..HEAD)")
parser.add_argument("--staged", action="store_true",
help="Scan only staged changes (for pre-commit)")
parser.add_argument("--config", default=None, help="Path to .gitleaks.toml config")
parser.add_argument("--output", default="gitleaks-report.json", help="Output report path")
parser.add_argument("--fail-on-findings", action="store_true",
help="Exit non-zero on new findings")
parser.add_argument("--create-baseline", action="store_true",
help="Generate baseline from current findings")
args = parser.parse_args()
repo_path = os.path.abspath(args.repo_path)
start_time = datetime.now(timezone.utc)
print(f"[*] Running Gitleaks {args.scan_type} on {repo_path}")
raw_result = run_gitleaks(
repo_path,
scan_type=args.scan_type,
baseline_path=args.baseline,
commit_range=args.commit_range,
staged=args.staged,
config_path=args.config
)
if raw_result["exit_code"] == -1:
print(f"[ERROR] {raw_result['stderr']}")
sys.exit(2)
scan_result = ScanResult()
scan_result.findings = parse_findings(raw_result["findings"])
scan_result.scan_duration = (datetime.now(timezone.utc) - start_time).total_seconds()
if args.baseline:
baseline_fps = load_baseline(args.baseline)
scan_result.new_findings, scan_result.baseline_findings = classify_findings(
scan_result.findings, baseline_fps
)
else:
scan_result.new_findings = scan_result.findings
scan_result.baseline_findings = []
if args.create_baseline:
baseline_path = os.path.join(repo_path, ".gitleaks-baseline.json")
with open(baseline_path, "w") as f:
json.dump(raw_result["findings"], f, indent=2)
print(f"[*] Baseline created: {baseline_path}")
print(f" Contains {len(raw_result['findings'])} findings")
return
report = generate_report(scan_result, repo_path)
output_path = os.path.abspath(args.output)
with open(output_path, "w") as f:
json.dump(report, f, indent=2)
print(f"[*] Report: {output_path}")
print(f"\n[*] Total: {len(scan_result.findings)} | "
f"New: {len(scan_result.new_findings)} | "
f"Baseline: {len(scan_result.baseline_findings)}")
if scan_result.new_findings:
print("\n[!] New secrets detected:")
for f in scan_result.new_findings:
print(f" [{f.rule_id}] {f.file}:{f.line} (commit: {f.commit}, author: {f.author})")
print(f" Secret: {f.secret}")
if report["quality_gate"]["passed"]:
print("\n[PASS] No new secrets detected.")
else:
print(f"\n[FAIL] {len(scan_result.new_findings)} new secrets found. Rotate immediately.")
if args.fail_on_findings and not report["quality_gate"]["passed"]:
sys.exit(1)
if __name__ == "__main__":
main()