Changelog
v2.2.0 — SAST vulnerability engine + structural secret detection
Added — SAST vulnerability & misconfiguration engine
--misconfig— in addition to secrets, scan source for vulnerabilities and misconfigurations.--misconfig-only— scan for vulnerabilities/misconfigurations only, skipping secret detection.- New SAST engine detects (with taint/context gating for low false positives): SQLi, NoSQLi, command injection, code injection, SSTI, XXE, insecure deserialization, LFI/path traversal, LDAP and XPath injection, SSRF, open redirect, CORS misconfig, CSRF-disabled, prototype pollution, XSS across 11 templating engines plus DOM sinks, weak crypto (DES/RC4/ECB/static-IV/weak-RSA), insecure RNG, JWT flaws (alg:none/confusion/hardcoded secret/verify-off), TLS bypasses, timing-unsafe compares, SAML sig-off, hardcoded credentials, sensitive-data logging, and IaC/config misconfig (Terraform, Kubernetes, Dockerfile, GitHub Actions, ASP.NET web.config, WCF/SOAP).
- 17 languages/formats: python, node/js/ts, react, php, ruby, go, java, kotlin, csharp/.NET, sql, XML/WSDL, JSP, terraform, kubernetes yaml, dockerfile.
- Each vulnerability finding carries: Vulnerability Name, Severity, Description, Evidence (file:line), Vulnerable Code, Secure Code, Remediation, Technical Impact, Business Impact, CWE, and OWASP mapping.
- 223 new vulnerability/misconfiguration rules, bringing the total rule count from 179 to 416 (193 secret + 223 vulnerability/misconfiguration).
Added — context-aware / structural secret detection
- Whole-file pass catching secrets in nested XML tags, split
<key>/<value>pairs, JSON key/value objects, multi-line YAML/properties, and Base64-encoded secrets. - Entropy-gate fix: credential-named assignments (e.g.
AM_CLIENT_SECRET=…) are now caught on the name signal with no entropy floor, so real low-entropy secrets are no longer dropped.
Added — new secret token types
- Slack app/user tokens, Dropbox, PlanetScale, PostHog, Supabase, Figma, GitLab runner/trigger tokens, Stripe test keys, Google OAuth refresh, Twitch, ngrok.
Changed — reporting
- HTML report is now collapsible — each finding is an expandable card (summary: severity + name + file:line + CWE; expand: full record incl. vulnerable/secure code, remediation, impacts) with a filter box, severity/file/name sort, and expand/collapse-all. Theme-aware, self-contained.
- JSON, CSV, SARIF, Excel, and PDF carry all fields, including the vulnerability record.
Examples
scan4secrets --path ./src --misconfig
scan4secrets --path . --misconfig-only --report html --output report
v2.1.3 — Branded banner + live crawl progress
Added
- Startup banner with byline
by m14r41 - github.com/m14r41printed inside a rounded panel. [+]/[*]status markers for every stage: rules loaded, SAST scan start/end, DAST target, threads/max_urls/timeout, wordlist seeding, deduplication, generic suppression, verification, report writes.- Live DAST progress bar (spinner +
M/N URLscounter + elapsed time) driven by the existingprogress_cbhook incrawl_and_scan. Auto-suppressed under--quiet.
v2.1.2 — Bundling, normalization, and PoC-friendly defaults
Breaking: Secret values are now shown in full by default in every output (terminal, CSV, JSON, JSONL, HTML, Excel, PDF, SARIF). The --unsafe-show flag is removed and replaced by --mask, which inverts the behaviour for screenshots or shared transcripts. Rationale: this tool is used to produce vendor PoCs where the raw value is the proof; redacted-by-default forced an extra flag on every real run.
Changed
--maskflag replaces--unsafe-show. Default is unmasked across the terminal table and every file reporter.normalize_urlaccepts every common shape: bare host, host:port, IPv4, IPv4:port, IPv6[::1],[::1]:port, scheme-less paths, accidental://prefixtypos, whitespace, trailing slashes.:443stays HTTPS; other ports and raw IPs default to HTTP. Unsupported schemes (ftp://,ws://,file://) are rejected with a clear error.
Fixed
- PyInstaller binary bundling: Linux and Windows binaries now ship
scan4secrets/config/rules.yamlvia explicit--add-data. Previous--collect-allalone did not pick up package-data under--onefile, causingFileNotFoundError: '/tmp/_MEIxxx/scan4secrets/config/rules.yaml'on first run. - BeautifulSoup XML warning:
XMLParsedAsHTMLWarningis silenced at crawler load (engine/crawler.py). Non-fatal noise on XML/RSS responses no longer pollutes the terminal.
v2.1.0 — DAST defaults + noise reduction
Changed
- Wordlist seeding is now ON by default for any
--urlrun. All 15 bundled wordlists (1279 unique paths) seed every DAST scan. --wordlist FILE [FILE ...]now takes user-supplied wordlist file paths and REPLACES the bundled set for that run (was: a boolean toggle).- Use
--wordlist-only NAME ...to restrict to specific bundled stems. - Use
--no-wordlistto disable seeding entirely. - Generic-rule suppression runs by default — when a vendor-specific rule fires on a secret value, generic
contextual-*/generic-high-entropy-*findings for the same value are dropped. Use--keep-genericto disable. - Response-header dedup is now global across the crawl — the same
X-Powered-Byleak is scanned once, not once per URL.
Added
- Inline
<script>URL string-literal extraction in HTML responses. - Inline
<style>and standalone CSSurl(...),@import,/*# sourceMappingURL=*.css.map */extraction. .css.mapsourcesContent parsing (matching.js.map).<meta http-equiv="refresh">URL following.- Binary content-type skip (
image/*,video/*,audio/*,font/*,application/pdf, etc.). - Connection pool sized to 64 (was default 10) — no more "pool is full" warnings on
--threads > 10.
v2.0.0 — Rule engine rewrite + DAST overhaul
Breaking: complete rewrite. Old core/, output/, ui/, config/patterns.json are moved to legacy_* directories and no longer imported. The CLI shape changes (main.py becomes a shim around scan4secrets.cli:main).
Why v2 exists
Empirical test on a benign Express repo: v1 produced 27 false positives, v2 produces 0. On a deliberately seeded leaky-repo, v2 catches the SSH private keys, PEM blocks, and Docker registry credentials that v1 was structurally incapable of detecting. The defect was the detection model itself (keyword-name match vs vendor value-shape match). v2 fixes the model.
New — detection engine
- YAML rules at
scan4secrets/config/rules.yamlreplacing the keyword-namepatterns.json - Vendor value-shape regexes for 170+ rule IDs across cloud, CDN/edge, SCM, CI/CD, payments, e-commerce, messaging, carriers, AI/ML, email, monitoring, registries, auth, productivity, mobile/push, data, mapping, Web3, storage, VPN, QA, DB connection URIs, webhooks, and crypto
- Aho-Corasick keyword pre-filter (
pyahocorasick) for O(N) scanning across hundreds of rules - Shannon-entropy gate per rule (kills
secret = "changeme"style noise) - Per-rule allowlists (regex against full line + path globs against file path)
- Contextual fallback rules for hex tokens, UUIDs, and quoted/unquoted high-entropy values near credential-sounding names
- Severity model: info / low / medium / high / critical, with a
--fail-onCI gate - Deduplication keyed on
(file, line, sha256(secret), rule_id)across SAST + DAST
New — DAST crawler
- Concurrent crawl via
ThreadPoolExecutor(--threads, default 16) — replaces v1's recursive single-threadedrequests.get - Scope control: same-eTLD+1 by default,
--strict-hostfor exact host - JS source-map parsing — fetches
*.js.mapand scans embeddedsourcesContent(no other open-source tool does this) - JS endpoint extraction — string literals in
.jsfiles added to the queue - HTTP header scanning — secrets sometimes leak in
X-Powered-By,Server, custom headers - Authenticated DAST:
--cookie,--header K:V(repeatable),--proxy,--insecure - Custom UA with sensible default — v1's default
python-requests/XUA was blocked by most WAFs - Caps:
--max-urls,--max-depth,--timeout
New — verification
--verifyflag runs one HTTP probe per finding whose rule has averify:block, setsverified=true|false|null- Built-in probes for GitHub (classic + fine-grained PAT), Slack, Stripe, OpenAI
- Trivial to add new vendors — edit the YAML rule
New — reporters
- SARIF 2.1.0 — GitHub Code Scanning, GitLab Security, Sonar, Defect Dojo
- JSON + JSONL — pipelines, SIEM ingest
- HTML v2 — severity badges, sortable columns, live filter, dark theme
- Excel via openpyxl
- PDF via fpdf2 (UTF-8 safe; v1's fpdf v1 crashed on non-ASCII)
- CSV with full schema (rule_id, severity, verified, file, line, entropy, redacted, sha256, description, excerpt)
- Redaction by default:
abcd****wxyz.--unsafe-showto include raw values (use carefully)
New — packaging & CI
pyproject.toml—pip install .works,scan4secretsconsole entry on PATH- Dockerfile — multi-stage build, non-root user
.pre-commit-hooks.yaml— drop-in forpre-commitframework.github/workflows/release.yml— Windows EXE + Linux EXE binaries, Docker image to GHCR, wheel build.github/workflows/ci.yml— pytest across Python 3.9-3.12
Empirical results
| Repo | v1 findings | gitleaks | v2 findings |
|---|---|---|---|
| Plazmaz/leaky-repo (seeded) | 35 (mixed TP/FP) | 22 | 17 (all real, incl. SSH/PEM/Docker keys v1 missed) |
| expressjs/express (clean) | 27 (all FP) | 0 | 0 |
v2 achieves 0% FP rate on benign code (vs v1's per-file FP rate of ~12.7%) and detects high-value secret classes (private keys, Docker registry auth) that v1's regex shape could never match.
v1.x (legacy)
Original keyword-name detection model. Source available under legacy_* directories for archival reference. Not recommended for production use.