A reusable workflow that runs three supply-chain security scans.
name: Security
on:
pull_request:
push:
branches: [main]
jobs:
security:
uses: QuickBirdEng/workflows/.github/workflows/qb-security.yml@mainNo secrets are required. All three scans run as parallel jobs inside the workflow.
Self-hosted runner:
with:
runs-on: 'self-hosted'Audit mode — report but do not fail:
with:
fail-on-found: falseScans git commits for verified secrets using TruffleHog OSS. Only verified secrets (credentials that TruffleHog can confirm are active against the real service) are reported, eliminating false positives from example keys or already-rotated credentials. Findings are emitted as inline PR annotations.
By default the full repository history is scanned on every run. Pass trufflehog-base to limit the scan to commits reachable from HEAD but not from the base — equivalent to git log <base>..HEAD:
with:
trufflehog-base: ${{ github.event.pull_request.base.sha || github.event.before }}github.event.pull_request.base.sha is the tip of the target branch on a pull_request event; github.event.before is the SHA before the push on a push event.
Exclude or restrict paths:
with:
trufflehog-exclude-paths: .trufflehog-exclude # file listing paths to skip
trufflehog-include-paths: .trufflehog-include # file listing paths to scanSelf-service ignore rules (no workflow change needed):
Create .qb/security/trufflehog-ignores.yaml in your repo to manage TruffleHog ignores without touching the central governance config or your workflow file:
# .qb/security/trufflehog-ignores.yaml
paths:
- test/fixtures/** # gitignore-style globs — these paths are excluded from scanning entirely
- vendor/**
acknowledged_findings:
- { commit: "abc123def456", path: test/fixtures/dummy-credentials.txt }paths works the same as trufflehog-exclude-paths — gitignore-style globs that prevent matching files from being scanned. The patterns are merged with any file you pass via trufflehog-exclude-paths, so both apply.
acknowledged_findings suppresses a specific finding that has been reviewed and accepted as non-actionable (e.g. a test fixture, a dummy credential, a rotated secret that still appears in history). Each entry must specify the exact commit SHA where the secret was introduced and the path of the file. Only that exact commit+path combination is suppressed — any new secret committed to the same file in a future commit will still be caught.
Both fields are applied in the PR scan (qb-security) and the nightly org-wide scan. Use acknowledged_findings sparingly: the right first response to a real secret is revocation, not acknowledgement.
Skip the scan entirely:
with:
trufflehog-enable: falseScans every source file for invisible or non-printing Unicode characters. Binary files are skipped automatically. Findings are emitted as inline PR annotations pointing to the exact file and line.
The following categories are detected:
| Category | Code points | Why it matters |
|---|---|---|
| VARIATION_SELECTOR | U+FE00–U+FE0E | GlassWorm attack — embeds invisible payload in commits; undetectable in editors, terminals, and GitHub diffs |
| VARIATION_SELECTOR_SUPPLEMENT | U+E0100–U+E01EF | Same GlassWorm attack vector, supplementary range |
| BIDI_CONTROL | U+200E–U+200F, U+202A–U+202E, U+2066–U+2069, U+061C | Trojan Source attack — visually reorders code during review so what a reviewer sees differs from what the compiler executes |
| ZERO_WIDTH | U+200B–U+200C, U+2060, U+180E | Zero-width spaces and formatting marks; can hide content between visible characters |
| BOM | U+FEFF | Byte-order mark; harmless in UTF-8 files but a common indicator of encoding tampering |
| TAGS_BLOCK | U+E0000–U+E007F | Deprecated tag characters; no legitimate use in source code |
| PUA_BMP | U+E000–U+F8FF | Private Use Area — legitimate in font files and icon CSS (e.g. Font Awesome); flag any unexpected occurrences in source code |
| PUA_SUPPLEMENTARY | U+F0000–U+10FFFF | Supplementary Private Use Areas A and B — same caveat as PUA_BMP |
Exclude paths:
with:
unicode-exclude: |
generated/**
vendored/**Skip the scan entirely:
with:
unicode-enable: falseWalks the repo, identifies every JS project by its lockfile (pnpm-lock.yaml / yarn.lock / package-lock.json), and verifies each project enforces the three protections described in Gajus' write-up:
minimumReleaseAge— quarantines newly published package versions (default: 3 days). Most malicious package versions are caught and yanked within 24–72 hours; quarantining new versions neutralises the window in which a compromised release reaches your CI. Natively enforced for pnpm 10+ and yarn 4.14+; yarn 1.x, older yarn-berry, and npm hard-fail with a migration prompt since these managers have no equivalent install-time setting.blockExoticSubdeps— rejects subdependencies pulled from anything other than the npm registry (git URLs, tarball URLs, github: shortcuts, file paths). For pnpm this is a setting; for all managers the action also scans the lockfile and reports each exotic resolution.allowBuilds(install-script allowlist) — install scripts run by default and have been the delivery mechanism for ua-parser-js, event-stream, nx, and many other attacks. The check requirespnpm.onlyBuiltDependencies(pnpm) /ignore-scripts=true(npm, yarn classic) /enableScripts: false(yarn berry). Specific packages can be whitelisted via thejs-allow-buildsinput.
Every finding emits both a PR annotation and a detailed log block with what was found, why it matters, and a manager-specific how to fix snippet. At the bottom of the log an ACTION REQUIRED section aggregates every concrete edit grouped by target file. If no JS lockfiles are present, the job logs that and exits cleanly.
Two managers have native install-time enforcement of the full policy:
| Manager | Minimum version | Settings honored |
|---|---|---|
| pnpm | 10.0+ | minimumReleaseAge, blockExoticSubdeps, onlyBuiltDependencies |
| yarn | 4.14+ | npmMinimalAgeGate (4.10+), approvedGitRepositories (4.14+), enableScripts: false |
Older versions parse the settings but silently ignore them. The action reads each project's packageManager field in package.json and emits a top-level error when a project pins a version below the threshold. The minimums are configurable via js-pnpm-min-version and js-yarn-min-version.
yarn 1.x (classic) and npm < 11.10 have no native equivalent. Projects on those managers hard-fail when js-minimum-release-age-minutes > 0 with a migration prompt offering yarn 4.14+ OR pnpm 10+ as the two viable paths.
Yarn 4.14+ (least disruptive for yarn projects):
corepack enable
yarn set version 4.14.0.yarnrc.yml:
npmMinimalAgeGate: 4320 # 3 days, in minutes
approvedGitRepositories: [] # empty = block all git/tarball subdeps
enableScripts: false # disable install scriptsPnpm 10+:
npx @pnpm/exe@latest import # imports yarn.lock / package-lock.json → pnpm-lock.yamlpackage.json:
.npmrc:
minimum-release-age=4320
block-exotic-subdeps=trueUpdate CI to pnpm install --frozen-lockfile and delete the old lockfile.
For projects that cannot migrate immediately:
with:
js-enforce-release-age-via-registry: trueThis scans every lockfile entry against registry.npmjs.org at PR time and fails on any version published less than the threshold ago. It gates merged code but does NOT protect a developer who runs yarn install on a local branch before opening the PR. Use as a stopgap during migration only.
Setting js-minimum-release-age-minutes: 0 disables the policy entirely.
# .npmrc
minimum-release-age=4320
block-exotic-subdeps=true// package.json
{
"pnpm": {
"onlyBuiltDependencies": []
}
}Exclude paths:
with:
js-exclude: |
generated/**
vendored/**Tighten the quarantine and whitelist install-script packages:
with:
js-minimum-release-age-minutes: 20160 # 14 days
js-allow-builds: |
esbuild
sharpDisable individual sub-checks:
with:
js-minimum-release-age-minutes: 0 # turn off the age gate
js-require-block-exotic-subdeps: false # turn off the exotic-subdeps check
js-check-install-scripts: false # turn off the install-scripts checkApprove an urgent security patch newer than the quarantine threshold:
The exemption lives in the project's native package-manager config — yarn / pnpm both honor it at install time, and the qb-security check trusts the project's decision. The exemption shows up in the PR diff and is reviewed alongside the lockfile change.
For yarn 4.10+:
# .yarnrc.yml (project)
npmPreapprovedPackages:
- lodash # CVE-2026-XXXX, reviewed by @<reviewer>For pnpm 10+:
# .npmrc (project)
minimum-release-age-exclude=lodashNo workflow-side change is needed. The optional registry-scan path (js-enforce-release-age-via-registry: true) honours the project's exempt list automatically.
Skip the scan entirely:
with:
js-enable: falseAll inputs are optional.
| Input | Type | Default | Description |
|---|---|---|---|
runs-on |
string | default-k8s-runner |
Runner label for all scan jobs |
fail-on-found |
boolean | true |
Fail the check when any scan reports findings |
trufflehog-enable |
boolean | true |
Set to false to skip the TruffleHog scan entirely |
trufflehog-base |
string | '' |
Base commit SHA or ref. Leave empty to scan the full git history |
trufflehog-exclude-paths |
string | '' |
Path to a file listing paths to exclude from the TruffleHog scan |
trufflehog-include-paths |
string | '' |
Path to a file listing paths to include in the TruffleHog scan |
unicode-enable |
boolean | true |
Set to false to skip the invisible-Unicode scan entirely |
unicode-exclude |
string | '' |
Newline- or comma-separated glob patterns to exclude from the Unicode scan (always excluded: .git/**, node_modules/**, .idea/**, build/**, dist/**, common binary types) |
js-enable |
boolean | true |
Set to false to skip the JS supply-chain scan entirely |
js-exclude |
string | '' |
Newline- or comma-separated glob patterns to exclude from the JS supply-chain scan (always excluded: .git/**, node_modules/**, .idea/**, build/**, dist/**) |
js-minimum-release-age-minutes |
number | 4320 |
Required quarantine for new package versions in minutes (3 days). Set to 0 to disable |
js-allow-builds |
string | '' |
Newline- or comma-separated list of packages allowed to run install scripts. Empty = none allowed |
js-check-install-scripts |
boolean | true |
Set to false to skip the install-scripts sub-check |
js-require-block-exotic-subdeps |
boolean | true |
Require blockExoticSubdeps and scan every lockfile for non-registry resolutions. Set to false to skip |
js-enforce-release-age-via-registry |
boolean | false |
CI-time band-aid: scan lockfile entries against the npm registry to enforce minimum release age. Does not protect developer machines |
js-pnpm-min-version |
string | 10.0.0 |
Minimum pnpm version where supply-chain settings take effect |
js-yarn-min-version |
string | 4.14.0 |
Minimum yarn version with native install-time enforcement (npmMinimalAgeGate + approvedGitRepositories) |
search-directory |
string | . |
Root directory for the Unicode and JS supply-chain scans |
{ "packageManager": "pnpm@10.x.y", "pnpm": { "onlyBuiltDependencies": [] } }