Scanners

gitleaks

On this page
  • Control: secrets
  • Tool: Gitleaks, https://github.com/gitleaks/gitleaks
  • Status: ✅ implemented
  • Target: source repository (RepositoryTarget), checked out at the scanned revision
  • License / terms: MIT (permissive). Run via exec.

What it does

Checks out the component's repository, then runs gitleaks dir <dir> --report-format sarif --report-path <file> --exit-code 0 --no-banner --log-level trace to find leaked credentials, API keys, tokens, private keys, in the working tree. See the secret-detection glossary entry. A second run with --ignore-gitleaks-allow records what gitleaks:allow comments set aside; see Exclusions. The trace level is what names the files Gitleaks skipped; see Skipped.

--exit-code 0 keeps the process successful even when secrets are found; findings live in the SARIF report, not the exit code. The secrets controller decides severity.

Saga options

config:
  controls:
    secrets:
      gitleaks:
        config: security/gitleaks.toml   # relative to where Draugr runs
        history: true                    # the commit history as well as the tree
OptionWhat it does
configPath to a Gitleaks TOML rules file, for a ruleset shared across repositories. It replaces a repository's own .gitleaks.toml; Draugr's own rules are still added. See Repository rules.
historyScan the commit history as well as the tree. Off by default.

history: true adds a second gitleaks git pass alongside the gitleaks dir pass, and switches the checkout from a shallow clone to a full one. Both, together: gitleaks git over a shallow clone walks a single commit and reports clean, which is indistinguishable from a repository whose history holds nothing. It also turns off the sparse and partial-clone optimizations, because those leave historical blobs unfetched, a scan that walks commits it cannot read finds nothing in them.

The tree pass is kept rather than replaced. gitleaks git reports the path a secret had in the commit that introduced it, so a file since renamed is reported under a directory that no longer exists. Findings from the history pass are marked historical in the report, and the tree pass is what names the path a live secret is at now. A history finding keeps the commit it was found at; the commit's author, email, date and message, which Gitleaks reports beside it, are left out.

A secret still in the tree is found by both passes and reported once, as the tree finding. A history finding is the same secret as a tree finding when both have the same rule, the same path and the same secret value. The line is not compared, so a secret that has moved within the file still matches. A secret at another path, or an older value replaced at the same path, is still reported from history.

The history pass reads every commit in the repository, because git history cannot be checked out by subtree. A history finding is kept only when the path it names would have been checked out under the component's paths and ignore. Two components sharing a repository each see the secrets committed under their own paths, and a secret committed under another component's paths is reported there.

Repository rules

A .gitleaks.toml at the root of a scanned repository applies to that repository's tree and history scans: its rules, its allowlist, and whether it extends Gitleaks' built-in rules ([extend] useDefault = true). Without that setting the built-in rules are not used. The file applies even when the component's paths do not include it. Each repository in a run is scanned with its own file, or with the built-in rules where it has none.

config replaces the repository's file, as --config does when Gitleaks runs by hand. Use it for an organization-wide ruleset that lives outside every repository.

A .gitleaks.toml that is a link is read only when it resolves to a file inside the repository. A link that points outside it or to nothing, or a directory, is not read; the repository is scanned with the built-in rules and the run logs a warning.

Draugr's own rules

Every scan runs a ruleset Draugr composes, which extends whatever the scan would otherwise have used: the file config names, the repository's .gitleaks.toml, or Gitleaks' built-in rules. Draugr's own rules are added to whichever it is.

Today that is one rule, for Draugr's ingest token: the credential a pipeline presents to publish a run. A tool that scans for other people's leaked credentials while issuing unrecognizable ones of its own is holding itself to the lower standard, so the token carries a fixed marker and this recognizes it. A match is reported only above 3.5 bits of entropy per character, so one character repeated after the marker is not reported.

Extended rather than replaced, in both directions. Replacing your ruleset would lose your rules; skipping ours when you have one would leave the token undetected in exactly the repositories that have been configured most carefully.

The composed file is written under ~/.draugr/data/gitleaks/. If it cannot be written, a machine with no writable home, the scan runs with the ruleset it would have extended. One rule is worth losing; the secrets control is not.

Exclusions

A secret on a line carrying a gitleaks:allow comment is reported suppressed with origin: tool. Gitleaks leaves such a line out of every report format, so each pass runs a second time with --ignore-gitleaks-allow, and a secret only the second run finds is the one the comment set aside. On a Gitleaks older than 8.18.1, which has no such flag, each pass runs once and the comment leaves no record.

A comment added to a line after its secret was committed covers the tree. The commit that introduced the secret without it is still reported by the history pass.

.gitleaksignore leaves no record. On the history pass, Gitleaks applies it and reports nothing of what it excluded. On the tree pass, the checkout is named by its absolute path and the file holds repository-relative fingerprints, so no entry matches and a secret it names is still reported. config.exclude keeps the finding and the reason.

Skipped

Gitleaks judges by a file's name, not its content, whether the file is an archive, and skips every file so named without reading it. A name counts when it contains an archive extension such as .zip, .tar, .gz or .br anywhere, so notes.zip.txt is skipped, and so is release.brief.md, for .br. A secret in either is not looked for, and the run reports no leaks. The rule is in Gitleaks' source at the version Draugr installs.

The SARIF report does not list these files. Gitleaks names each one in its log at trace level, so the tree pass runs with --log-level trace and each file named gets an unread-files caveat with the reason name has an archive extension. Where Gitleaks reports scanning 0 bytes of a tree that holds a file with content, and names no file it skipped, the repository root . gets no files read. The secrets control shows the caveat.

Rename notes.zip.txt to notes.txt and it is read. To fail the run on a skipped file, set config.gate.failOnCaveats: [unread-files].

A real archive such as bundle.zip is not opened either, so it is named too. A real archive whose name holds no archive extension is skipped as a binary file and is not named.

The history pass reads the text of each commit whatever the file is called, so with history: true the commits of a text file such as notes.zip.txt are searched. The tree pass still did not read the file, and the caveat stays. A binary file named as an archive is skipped by both passes.

Draugr does not relay Gitleaks' trace or debug lines, which can hold candidate secret values.

Notes

  • Integration mode: exec over a local checkout; Gitleaks + git must be on PATH.
  • Gitleaks' SARIF output omits level on results. Draugr's SARIF parser defaults an absent level to warning (per the SARIF 2.1.0 spec), and the secrets controller then escalates every finding to error. A leaked secret is always gate-failing.
  • Module path caveat: install via github.com/zricethezav/gitleaks/v8 (the module's declared path), not github.com/gitleaks/gitleaks/v8.

Data

Nothing. Gitleaks' rules are compiled into its binary, so an upgrade of the tool is the only thing that changes what it finds, and that is what the cache key records.