Skip to main content

secret-scan-onboarding

Why It Exists​

A repo can leak a credential into something it publishes (a mobile bundle, a web build, a package tarball) without anyone committing a secret on purpose. This skill wires a CI job that runs two scanners, gitleaks and trufflehog, over the artifact before the step that signs, uploads or deploys it, and fails the job on a real finding.

The scanners are installed from a small published package (@catesworks/next-starters) that carries the shared classification rules, a generated gitleaks config and two filter scripts. Everything the rules cannot tell apart from a real secret (test fixtures, documented placeholders) goes through an allowlist, and the allowlist is where this tooling can quietly stop protecting you.

What It Does​

  • Two job-block templates, one for mobile builds and one for Vercel deploys, plus the shared recipe both use: install the scripts into a scratch directory, install the scanners, run gitleaks then trufflehog with continue-on-error, gate on the filtered private-finding count, and make the deploy job needs: the scan.
  • A rule that matters: the raw scanner count is not the answer. Every match is re-classified, and only private-pattern findings fail the job. Raw values are never printed, only counts and file and line.
  • Exact version pins (no floating range), so a bad release cannot roll out fleet-wide on an unrelated CI run. A new release means bumping the pin in each consumer.

How To Use It​

  1. Pick the variant: mobile (fastlane lanes) or Vercel (git archive HEAD scan), or adapt the skeleton.
  2. Copy the job block, set the pinned package version, and grant the consumer repo read access to the package.
  3. Make the deploy job depend on the scan, then dispatch the workflow once and confirm the scan runs and the gate fails on a planted test secret.

Gotchas & Invariants​

Allowlist by path hides real secrets; bind exceptions to the line​

An early allowlist silenced two files by path. Those names (an environment.ts that declares an app's env schema, and a telemetry test fixture) exist in a dozen or more repos with different contents, though only one copy of each ever produced a finding. A real key pasted into any of the others would have been reported clean.

Exceptions are now bound to path and a hash of the full source line. The obvious alternative, hashing the scanner's matched value, does not work: the generated rules match only a prefix (a Stripe live-key prefix, a PEM private-key header), so the matched text is identical for every key sharing it and a real key would hash the same as the reviewed placeholder. The line hash changes whenever the reviewed line is edited or a new key is added, and the finding comes back.

The filter must not guess what it scanned​

The filter reads the line from the finding's own commit (git cat-file blob <Commit>:<File>), not the working tree, with replace refs ignored. Anything it cannot establish exactly is reported, never suppressed: a blank or malformed commit id, an unreadable file, a bad line range, a failing git. Repository detection ignores a stale inherited GIT_DIR, GIT_WORK_TREE and similar variables, and a scan is treated as "not a git repo" only if git says so, no .git exists in the working directory or any parent, and the caller set no repo-locating variable.

Scan the files you add​

A test file that contains secret-shaped fixtures trips the generated rules itself and turns the gate red after merge. Run the scanner over a change before opening the PR, including documentation: prose that spells out a key prefix or a private-key header is a finding too.

Local runs can differ from CI​

Some global git configs make gitleaks drop all commit metadata, so the reviewed lines show up as findings locally. To reproduce CI, run with GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null.