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:

ModuleRole
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:

  1. CLI parses command → run() in main.go constructs an engine.Config with detectors, concurrency, and source manager
  2. engine.NewEngine() initializes: builds the Aho-Corasick trie from all detector keywords, sets up LRU dedupe cache, creates worker channels
  3. engine.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
  4. Source-specific scan method (e.g., eng.ScanGit()) feeds chunks into the source manager, which streams them to scanner workers
  5. 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.go is the coupling point

Adding a detector requires editing pkg/engine/defaults/defaults.go to 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-verification flag 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 --fail to 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.go is ~1300 lines with massive flag declaration blocks — the CLI layer is monolithic
  • defaults.go as a single 1800-line registration file doesn’t scale well as a contribution surface
  • The overseer auto-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 overseer fork audited on every update?

The self-update mechanism downloads and re-execs a binary. The code comments mention a planned PreUpgrade signature check but it appears unimplemented.

Recommendations:

  • Consider code-generating defaults.go from 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