CLI · commit 3fe3dc9

secretsweep

Secret leak scanner for code & git history

Download secretsweep.zip
Quickstart
unzip secretsweep.zip && cd secretsweep
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/python -m pytest -q

secretsweep

A secret leak scanner for working trees, staged changes and the full git history, built for CI. It ships 54 provider rules defined in TOML, a context-aware entropy detector, checksum and structure validation that turns matches into confidence levels, baselines so legacy repositories can adopt it without a flag day, and SARIF output for GitHub code scanning. Secrets are redacted in every output unless you ask otherwise, and nothing leaves your machine unless you explicitly pass --verify.

Features

Install

uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
source .venv/bin/activate

Requires Python 3.11+ and git on the PATH.

Quickstart

examples/make_leaky_repo.sh /tmp/leaky          # build a repo with planted FAKE secrets
cd /tmp/leaky
secretsweep scan .                              # working tree
secretsweep scan --history                      # every commit on every ref
secretsweep baseline create --history           # accept today's findings
secretsweep scan --history --format sarif -o secretsweep.sarif
secretsweep install-hook                        # block leaks at commit time

Usage

secretsweep scan [PATH] [--staged | --history] [--format table|json|sarif|junit] [-o FILE]
                 [--baseline FILE | --no-baseline] [--rules FILE]... [--disable RULE]...
                 [--no-entropy] [--max-size BYTES] [--jobs N] [--archives]
                 [--min-confidence low|medium|high] [--fail-on low|medium|high|critical]
                 [--show-secrets] [--verify] [--quiet]
secretsweep baseline create [PATH] [--history | --staged] [-o FILE]
secretsweep install-hook [--repo DIR] [--force]
secretsweep rewrite-guide FINGERPRINT [--repo DIR | --report report.json] [--show-secrets]
secretsweep rules list [--rules FILE]...

Exit codes: 0 clean (or everything baselined / below --fail-on), 1 findings, 2 usage or runtime error.

Working tree

$ secretsweep scan .
secretsweep: 11 finding(s)
┏━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ Severity ┃ Conf.  ┃ Rule                             ┃ Location                  ┃ Secret               ┃
...
13 files scanned, 1 skipped in 0.01s

Files in .gitignore (such as .env.local), vendor/** from .secretsweepignore, the line marked secretsweep:allow, the lockfile hashes, UUIDs and the base64 PNG produce nothing.

Git history

Real output against the example repository:

$ secretsweep scan --history
secretsweep: 17 finding(s)
┏━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━┓
┃ Severity ┃ Conf.  ┃ Rule                          ┃ Location                   ┃ Secret               ┃ Introduced                ┃ HEAD ┃
┡━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━┩
│ critical │ high   │ stripe-secret-key             │ .env:2                     │ sk_l**** (40 chars)  │ 50b48dcf Bob 2024-02-02   │ no   │
│ critical │ high   │ aws-access-key-id             │ app/config.py:1            │ AKIA**** (20 chars)  │ 83d68bc1 Alice 2024-01-10 │ no   │
│ critical │ high   │ aws-secret-access-key         │ app/config.py:2            │ wJal**** (40 chars)  │ 83d68bc1 Alice 2024-01-10 │ no   │
│ critical │ high   │ azure-storage-connection-stri │ config/settings.json:5     │ nmr6**** (88 chars)  │ 00acf7c7 Dave 2024-04-20  │ yes  │
│ critical │ high   │ private-key-pem               │ deploy/id_rsa:1            │ ----**** (476 chars) │ afafd502 Erin 2024-05-05  │ no   │
│ critical │ high   │ openai-api-key                │ notebooks/analysis.ipynb:3 │ sk-p**** (88 chars)  │ f1a549f4 Carol 2024-03-15 │ yes  │
│ high     │ high   │ github-token                  │ .env:1                     │ ghp_**** (40 chars)  │ 50b48dcf Bob 2024-02-02   │ no   │
│ high     │ high   │ database-url-credentials      │ .env:3                     │ Sup3**** (13 chars)  │ 50b48dcf Bob 2024-02-02   │ no   │
...
8 commits in 0.04s

Secrets that were added and later deleted (.env, deploy/id_rsa, the first AWS keys) are found, the commit that introduced each one is named, and HEAD = yes marks the ones still live in the current tree. Identical findings across commits are collapsed to the first sighting (JSON includes occurrences).

Baseline workflow

$ secretsweep baseline create --history
Baseline with 17 finding(s) written to .secretsweep-baseline.json
$ secretsweep scan --history
No secrets found (8 commits in 0.03s, 17 baselined)
$ echo "K=sk_live_..." > new_leak.env && secretsweep scan; echo $?
...  new_leak.env:1  stripe-secret-key ...
1

The baseline stores only fingerprints (sha256(rule + secret + path), truncated), never secrets, and is auto-discovered at <root>/.secretsweep-baseline.json. Fingerprints ignore line numbers, so moving a baselined secret within its file does not resurrect it. Commit the baseline file; when you rotate a secret, its fingerprint changes and the old entry is simply unused.

SARIF, JSON, JUnit

secretsweep scan --history --format sarif -o secretsweep.sarif   # upload with github/codeql-action/upload-sarif
secretsweep scan --format json | jq '.findings[] | {rule_id, path, line}'
secretsweep scan --format junit -o secretsweep-junit.xml

The SARIF run on the example repository validates against the bundled official SARIF 2.1.0 schema (tests/data/sarif-schema-2.1.0.json) with 17 results. Each result carries partialFingerprints (for stable alert tracking), a security-severity rating on the rule, and for history scans the introducing commit in properties.

Redaction

Output shows the first 4 characters plus the length (AKIA**** (20 chars); secrets shorter than 12 characters show 2 characters). This applies to the table, JSON, SARIF and JUnit alike. --show-secrets prints full values.

Allowing findings

EXAMPLE_KEY = "AKIAIOSFODNN7EXAMPLE"  # secretsweep:allow documented AWS example key

.secretsweepignore uses gitignore syntax, applies to tree, staged and history scans, and lives in the scan root:

vendor/**
docs/examples/*.md

Pre-commit hook

$ secretsweep install-hook
Installed pre-commit hook at /path/to/repo/.git/hooks/pre-commit

The hook runs secretsweep scan --staged --quiet, which scans the staged blob contents and exits 1 to abort the commit. It will not overwrite a hook it did not write unless you pass --force, and honours core.hooksPath.

Live verification (--verify)

Off by default and never implied. With --verify, GitHub tokens (ghp_, github_pat_ ...) are sent to https://api.github.com/user and Slack tokens to https://slack.com/api/auth.test, which are read-only "who am I" calls, and the result (valid, invalid, unknown) is attached to the finding. No other secret type is ever transmitted, and each token goes only to its own provider. A notice is printed on stderr whenever the flag is used. The test suite stubs the HTTP layer and never touches the network.

Remediation

$ secretsweep rewrite-guide 238d26722d60
Remediation for stripe-secret-key (critical) in .env:2
Introduced by 50b48dcfc4 (Bob, 2024-02-02); no longer at HEAD, but still in history.

1. ROTATE FIRST. ...
4. Purge it from history (requires https://github.com/newren/git-filter-repo). ...
     git filter-repo --path '.env' --invert-paths

The fingerprint comes from any report. See docs/remediation.md.

Configuration

Architecture

Module Role
rules.py, builtin_rules.toml Rule schema, loading, user overrides
detect.py Scanner.scan_text: keyword prefilter, regex, allowlist, entropy floor, validators, pairing, inline allow
entropy.py Shannon entropy and the context-aware detector
validators.py GitHub CRC32, JWT, PEM, Docker auth
walk.py File listing (via git ls-files, falling back to .gitignore walking), archives, process-pool scanning
gitscan.py Streaming git log -p parser for history, staged scanning, HEAD-presence check
baseline.py, ignore.py Baseline files and .secretsweepignore
engine.py Orchestrates a scan: rules, mode, baseline, verification
report.py Table, JSON, SARIF, JUnit
verify.py, hook.py, guide.py, cli.py Verification, pre-commit hook, remediation text, CLI

See docs/design.md for how the history scan and validation work.

Tests

.venv/bin/pytest -q

175 tests: every rule has a true-positive and a false-positive fixture (enforced by a test that fails if a rule lacks one), entropy and checksum maths, history scans that locate deleted secrets and their introducing commit on real temporary git repositories, baselines, inline allow, staged mode, archives, parallel/serial equivalence, SARIF validated against the official schema with jsonschema, JUnit, redaction, the pre-commit hook end to end, and verification against a stubbed HTTP layer. All fixture secrets are generated fake values.

Performance

Measured on a 12-core Linux machine with 5,000 synthetic Python files (about 5 KB each, 120 lines): about 1,900 files/s with the process pool and about 350 files/s single-process (--jobs 1). The pool is only used from 300 files up. Real repositories are faster because most files contain none of the rule keywords. The example repository's 8-commit history scans in about 40 ms.

Limitations