Source: trufflesecurity/trufflehog at commit ba0a524
How It Works
TruffleHog is a secret scanning engine built around a source → chunk → decode → detect → verify → dispatch pipeline. Sources (Git, GitHub, GitLab, S3, Docker, filesystem, syslog, etc.) enumerate data and emit chunks — bounded byte slices with metadata about where they came from. Each chunk passes through a decoder stage (UTF-8, Base64, UTF-16, HTML entities, escaped Unicode) that can chain iteratively up to a configurable depth (default 5). This means a Base64 blob containing UTF-16-encoded credentials gets decoded across multiple passes, and every intermediate form is scanned.
The decoded chunks then hit an Aho-Corasick keyword trie built from all detector keywords. This is the critical pre-filter: rather than running every detector’s regex against every chunk, the engine first checks whether the chunk contains any keyword associated with a detector. Only matching detectors process the chunk, and only on the byte span around the keyword match. This reduces detector regex invocations from O(chunks × detectors) to O(chunks × matching-detectors), which is a massive performance win given 800+ detectors.
Detectors that find candidate secrets can optionally verify them by making live API calls to the service (e.g., an AWS STS call to check if access keys are active). Verification results are cached in-memory to avoid redundant network calls for identical credentials. Results are deduplicated via an LRU cache keyed on detector type, raw secret, and source metadata, then dispatched to the configured output format (plain text, JSON, GitHub Actions).
Architecture
The codebase lives under pkg/ with these key modules:
| Module | Role |
|---|---|
pkg/engine/ | Core scanning pipeline — worker pools, chunk processing, result dispatch |
pkg/sources/ | Source implementations (Git, GitHub, S3, Docker, etc.) and the SourceManager |
pkg/detectors/ | ~887 detector packages, each a single secret type |
pkg/engine/ahocorasick/ | Aho-Corasick trie for keyword pre-filtering with span calculation |
pkg/engine/defaults/ | Detector registration — a single defaults.go file listing all detectors |
pkg/decoders/ | Encoding decoders (UTF-8, Base64, UTF-16, HTML, escaped Unicode) |
pkg/pb/ | Protocol buffer definitions for sources, detectors, metadata |
pkg/analyzer/ | Post-detection analysis — deep inspection of verified credentials |
pkg/config/ | YAML configuration parsing |
pkg/output/ | Result formatting (plain, JSON, legacy JSON, GitHub Actions) |
The entry point is main.go, which uses kingpin for CLI parsing and overseer for zero-downtime self-updates. The CLI supports 20+ subcommands (git, github, gitlab, s3, docker, filesystem, syslog, jenkins, etc.) plus a TUI mode for interactive use.
The Spine
A scan flows through these stages:
- CLI parses command →
run()inmain.goconstructs anengine.Configwith detectors, concurrency, and source manager engine.NewEngine()initializes: builds the Aho-Corasick trie from all detector keywords, sets up LRU dedupe cache, creates worker channelsengine.Start()spawns four worker pools:- Scanner workers (N = concurrency) — read chunks from source manager, decode, run Aho-Corasick, dispatch to detector workers
- Detector workers (N × 8) — run detector regexes and verification on matched spans
- Verification overlap workers (N × 1) — handle chunks matched by multiple detectors (disables verification to prevent credential confirmation attacks via overlapping detectors)
- Notifier workers (N × 1) — deduplicate and dispatch results to output
- Source-specific scan method (e.g.,
eng.ScanGit()) feeds chunks into the source manager, which streams them to scanner workers engine.Finish()drains the pipeline: waits for sources → scanner workers → overlap workers → detector workers → notifier workers
Source → SourceManager → [chunks chan]
→ ScannerWorker: decode → Aho-Corasick → [detectableChunksChan]
→ DetectorWorker: regex + verify → [results chan]
→ NotifierWorker: dedupe + dispatch → Printer
The channel buffer sizes are multiples of runtime.NumCPU(), with detector workers getting the largest buffer (50× CPU) since they’re I/O-bound on verification calls.
Key Patterns
Detector registration is a static list in pkg/engine/defaults/defaults.go — a ~1800-line file that imports every detector package and constructs them in DefaultDetectors(). Each detector implements the detectors.Detector interface:
type Detector interface {
FromData(ctx context.Context, verify bool, data []byte) ([]Result, error)
Keywords() []string
Type() detector_typepb.DetectorType
Description() string
}Optional interfaces (Versioner, EndpointCustomizer, MaxSecretSizeProvider, StartOffsetProvider, CustomResultsCleaner) let detectors declare capabilities without changing the core interface. The engine checks for these via type assertions at initialization or runtime.
Aho-Corasick pre-filtering is the performance backbone. Each detector provides Keywords() — short strings like "abstract", "PMAK-", "AKIA" that uniquely identify credential types. The trie matches all keywords in a single pass over the chunk. For each match, the engine extracts a span (byte range around the keyword) and only runs the detector on that span, not the full chunk. Span size can be customized per-detector via MaxSecretSizeProvider and StartOffsetProvider.
Iterative decoding chains decoders: a Base64 decoder output is re-run through UTF-16 and HTML decoders, up to maxDecodeDepth. The PLAIN decoder is skipped after depth 0 to avoid redundant work. Each intermediate decoded form is independently scanned.
Verification overlap detection is a safety mechanism: when multiple detectors match the same chunk, the engine first runs all detectors without verification. If two different detectors find the same (or Levenshtein-similar) secret, verification is permanently disabled for that finding — preventing a malicious detector from using verification calls to confirm stolen credentials.
Non-Obvious Details
defaults.gois the coupling pointAdding a detector requires editing
pkg/engine/defaults/defaults.goto import and register it. There’s no auto-registration or plugin system. This is intentional — it keeps the detector list explicit and auditable — but it means the file is ~1800 lines of imports and struct literals.
Verification can make network calls to third-party services
When verification is enabled, TruffleHog makes HTTP requests to the services that issued the credentials (e.g.,
https://exchange-rates.abstractapi.com/v1/live/?api_key=...). This is by design but has security implications: the tool sends extracted secrets over the network. The--no-verificationflag disables this.
Pre-commit hook detection
The engine detects when running as a pre-commit hook and automatically overrides flags: sets
BaseRef = "HEAD"to scan only staged changes, filters to verified+unknown results only, and enables--failto block the commit. This is a clean UX touch.
Self-update via overseer. The binary uses jpillora/overseer (forked to trufflesecurity/overseer) for in-process binary replacement. On startup, it checks for a newer version and re-execs itself. The --no-update flag disables this, and dev builds skip it entirely.
Detector timeout enforcement. Each detector call gets a context.WithTimeout, plus a safety time.AfterFunc that logs if a detector ignores the context cancellation — catching misbehaved detectors that could stall the pipeline.
Shannon entropy filtering. The --filter-entropy flag applies a Shannon entropy threshold to unverified results, discarding low-entropy matches that are likely false positives (e.g., test_key = "aaa").
Assessment
Strengths:
- The Aho-Corasick pre-filter + span extraction is a genuinely clever optimization that scales to 800+ detectors without O(N×M) regex explosion
- Iterative decoding handles real-world obfuscation (Base64 inside UTF-16, HTML-encoded secrets) without detector authors needing to think about it
- Verification overlap detection prevents a subtle attack vector where overlapping detectors could be weaponized
- Clean source/detector separation — adding a new source (e.g., a new CI platform) doesn’t touch detector code
- Pre-commit hook auto-detection shows attention to real developer workflows
Concerns:
main.gois ~1300 lines with massive flag declaration blocks — the CLI layer is monolithicdefaults.goas a single 1800-line registration file doesn’t scale well as a contribution surface- The
overseerauto-update mechanism is a supply chain risk (forked dependency, binary replacement at runtime) - Verification makes outbound network requests with extracted credentials — documented but worth flagging
Is the
overseerfork audited on every update?The self-update mechanism downloads and re-execs a binary. The code comments mention a planned
PreUpgradesignature check but it appears unimplemented.
Recommendations:
- Consider code-generating
defaults.gofrom detector metadata to reduce merge conflicts and boilerplate - Extract CLI subcommand definitions into per-source files (the pattern exists for
analyzer) - Add an option to restrict verification to an allowlist of detector types, reducing the blast radius of outbound calls