Every knob PullGuard exposes. All fields are optional — the scanner works out of the box with sensible defaults. Override only what you need.
On this page
with: block on pullguard-action@v1.drift-baseline.json format and “only new findings” modesscan flags
Pass values via the with: block on
pullguard-dev/pullguard-action@v1. Every input is optional.
| Input | Default | What it does |
|---|---|---|
license-key |
none |
Your pg_live_* token from Stripe checkout. Unlocks Pro
(42 of 44 analyzers, 1 repo), Team (all 44, 10 repos), or Enterprise
(all 44, repo bands + 4 business-hour SLA + SSO/RBAC; audit-log on the roadmap).
Leave empty for the Free tier
(14 analyzers).
|
fail-on-severity |
empty |
Fail the workflow if any finding at this severity or above exists.
Values: info, minor, moderate,
major, critical. Empty (default) means PullGuard
only reports — never blocks merge.
|
hourly-rate |
150 |
Developer hourly rate in USD. Used to convert finding-effort estimates into dollar figures in the cost-of-change report. Set to your fully-loaded engineer rate for an accurate ROI signal. |
config |
auto-detect |
Path to a .driftrc.yml file relative to the repo root. If
not set, PullGuard looks for .driftrc.yml at the repo root.
|
path |
. |
Subdirectory to scan, relative to the repo root. Default scans the
whole repo. Useful for monorepos — run one PullGuard step per
package, each with its own path + config.
|
report-to-app |
false |
Enterprise
When true, post the report to the PullGuard GitHub App
so findings render as native Check Run annotations on the PR
Files-changed view. Requires the Marketplace App to be installed.
|
delta |
false |
Force “Clean as You Code” delta filtering — surface only
findings this change introduced (plus security criticals) — on
non-PR events (push, schedule, workflow_dispatch). On
pull_request this is already automatic.
|
base-ref |
PR target |
Git ref to diff against when delta filtering off-PR (e.g. a release tag
or origin/main). Ignored on pull_request
(the PR target is used). Requires a deep checkout
(fetch-depth: 0) so the ref is present.
|
image-pin |
1 |
Which scanner image the Action runs. 1 (default) = the
stable major line — always the newest signed v1.x release,
re-pointed only when a release is cut, never per-merge.
latest = the bleeding-edge image rebuilt on every merge.
Pin an exact release tag (v1.3.4) or a
sha256:<hex> digest to freeze one version for
change-controlled / air-gapped environments (release tags are
preserved forever).
|
update-baseline |
false |
On a push to your base branch, write/refresh .drift-baseline.json
from a full scan so future PR deltas match your tier and engine exactly.
Your workflow commits the file. Refuses to run on a PR/delta scan.
|
blame |
false |
Annotate each finding with its git-blame introduced date + author
in the HTML report and dashboard. Requires a deep
checkout — add fetch-depth: 0 to your
actions/checkout step; on a shallow checkout PullGuard
warns and leaves findings undated (it never fetches history on your behalf).
Adds scan latency; off by default.
|
collapse-preexisting-security |
false |
Relocate security findings already in your committed baseline into a counted,
collapsed section of the PR comment, so the inline list is just what
this PR introduced. New security still shows inline; nothing is hidden
from the full report, SARIF, or the fail-on-severity gate. Requires
a tier-matched baseline.
|
server-url |
none |
Enterprise
Base URL of your self-hosted PullGuard
server — when set (with server-token), scan
results (never source) upload to your central dashboard after each
scan. Best-effort: a failed upload never fails your build.
|
server-token |
none |
Enterprise
Ingest bearer token for your self-hosted
server (pair with server-url). Store it as a repository secret.
|
| Output | Type | What it contains |
|---|---|---|
steps.<id>.outputs.score |
integer 0–100 | Drift score (lower is better). Drives Grade. |
steps.<id>.outputs.grade |
A–F | Letter grade derived from score and severity mix. |
steps.<id>.outputs.findings |
integer | Total finding count (all severities). |
- id: pullguard
uses: pullguard-dev/pullguard-action@v1
with:
license-key: ${{ secrets.PULLGUARD_LICENSE_KEY }}
fail-on-severity: major
hourly-rate: 220
path: services/payments
- name: Post score to internal dashboard
if: always()
run: |
curl -X POST https://dashboard.example.com/scores \
-d "score=${{ steps.pullguard.outputs.score }}" \
-d "grade=${{ steps.pullguard.outputs.grade }}"
.driftrc.ymlPer-repository configuration file at the repo root. All fields are optional. An empty file is valid — the schema fills in defaults so you only specify what you want to override.
Schema validation runs on load. Invalid types or out-of-range values produce a clear error before the scan starts (no silent ignores).
| Field | Type | Default | Purpose |
|---|---|---|---|
exclude |
string[] |
standard ignores |
Glob patterns appended to the built-in exclude list
(node_modules, dist, build,
.git, etc.). User patterns add to defaults — you
cannot accidentally start scanning node_modules.
|
maxDepth |
number |
8 | Maximum directory depth to traverse. Guards against symlink loops. |
maxFiles |
number |
auto | Hard cap on files analyzed. Hit this in a monorepo → scan per package instead. |
analyzers |
map |
{} |
Per-analyzer enable / severity / options overrides. See Analyzers block. |
thresholds |
map |
see below | Numeric thresholds for complexity / duplication / monolithic-file / nesting / type-coverage. |
output |
map |
see below | Format, minimum severity, grouping, remediation toggle. |
plugins |
string[] |
[] |
Explicit plugin allowlist. Plugins are sandboxed; auto-discovery is disabled by design. |
baseline |
string |
none | Path to a baseline report — the scanner reports only NEW findings vs the baseline. |
cost |
{ enabled, hourlyRate } |
enabled: true, hourlyRate: 150 |
hourlyRate — centrally-managed dev rate for the
cost estimate (alternative to the workflow input).
enabled: false — hide the cost-of-fix
breakdown entirely (the “Est. fix cost” headline
and the Cost Breakdown section). Findings, severities, and counts
still render — useful for security-focused teams that prefer
no dollar figures.
|
compliance |
map |
5-framework summary | All 5 frameworks get a compact PASS/CONCERN/FAIL summary on every PR; set hipaa/pci-dss/nist/iso-27001 to true for full per-control detail tables. |
repo |
{ type } |
auto-detect | Override the repo-type heuristic that gates OSS-hygiene checks. |
taint |
map |
{} |
Custom taint sources, sinks, and sanitizers for proprietary frameworks. |
db |
map |
OSV remote | Local vulnerability database location and freshness policy. See Air-gapped mode. |
analyzers: — per-analyzer overrides
Keyed by analyzer ID (e.g. builtin/complexity,
builtin/security). Each entry can disable the analyzer,
override its default severity, or pass analyzer-specific options.
analyzers:
builtin/complexity:
enabled: true
severity: minor # downgrade complexity findings
builtin/duplication:
enabled: false # turn this analyzer off entirely
builtin/security:
enabled: true # security analyzers cannot be disabled
# (validation rejects this with a clear error)
builtin/naming:
severity: info
options:
ignorePatterns:
- "_test\\.ts$"
Note: security-category analyzers cannot be disabled or downgraded below their built-in floor — the schema rejects attempts to do so. This is the integrity guarantee enterprise buyers (CISO / SOC auditor) rely on.
thresholds: — numeric tuningthresholds:
complexity:
maxCyclomatic: 15 # default 10
maxCognitive: 20 # default 15
maxParameters: 6 # default 5
maxFunctionLength: 80 # default 50 (honest line count, not raw)
duplication:
minBlockSize: 6 # default 5 (lines)
maxDuplicationPercent: 5 # default 3 (% of total LoC)
monolithicFile:
maxFileLines: 600 # default per-language; this overrides for all
maxExports: 30 # default 20
nesting:
maxDepth: 5 # default 4
typeCoverage:
minCoveragePercent: 90 # default 85
maxAnyPerFile: 3 # default 5
Per-language defaults (Java 1000 / Go 400 / TS 600 etc.) apply
automatically. Override with a single thresholds.monolithicFile.maxFileLines
value to apply globally; per-language overrides are roadmap.
output: — format and noise controloutput:
format: markdown # text | json | sarif | markdown
minSeverity: moderate # info | minor | moderate | major | critical
groupBy: severity # category | severity | file
showRemediation: true
sarif:
toolName: "PullGuard"
toolVersion: "1.0.0"
Note: html and compliance are CLI
--format values, not config values — neither is a valid
output.format in .driftrc.yml (the schema rejects
them). Use pullguard scan . --format html for the HTML report and
the compliance: block below for framework opt-ins.
minSeverity filters the rendered output but does NOT change
what is scanned — the JSON artifact always contains every finding
for archive / audit purposes.
compliance: — framework opt-inscompliance:
hipaa: true
pci-dss: true
nist: false
iso-27001: false
Every PR shows a compact summary of all five frameworks
— SOC 2, HIPAA, PCI DSS, NIST 800-53, and ISO 27001 — one
PASS / CONCERN / FAIL line each, with no configuration required. Setting
a framework to true adds its full per-control
evidence table to the report (SOC 2's full grid always renders;
the keys are hipaa, pci-dss, nist,
iso-27001). Each section emits a "provides evidence for"
disclaimer — PullGuard helps auditors; it does not grant compliance.
The HIPAA / PCI / NIST / ISO control catalog is fetched at scan time
(signed and verified) and degrades to a minimal embedded set, flagged
"limited coverage", if your runner can't reach pullguard.dev.
repo.type: — hygiene-check gating
Some checks are appropriate for public OSS but irrelevant for private
customer-delivery repos (e.g. missing LICENSE or
SECURITY.md). PullGuard auto-detects via GitHub Actions env
+ local signals; override here when the heuristic is wrong.
repo:
type: customer-delivery # public-oss | private-enterprise |
# customer-delivery | internal-service |
# fork-research
taint: — custom sources, sinks, sanitizersEnterprise CMS platforms, internal RPC layers, and proprietary frameworks have their own taint surfaces that the built-in patterns do not know about. Add them here. Patterns are language-keyed regular expressions.
taint:
sources:
java:
- "\\bContentManagementData\\.get\\s*\\("
python:
- "\\binternal_rpc_call\\s*\\("
sinks:
java:
- "\\bTemplateEngine\\.render\\s*\\("
sanitizers:
- "\\bSecurityUtils\\.escape\\s*\\("
cost: — centrally-managed hourly ratecost:
hourlyRate: 220
Same effect as the hourly-rate workflow input but lives in
the repo so platform teams can manage rate centrally without editing
every workflow file.
plugins: — explicit allowlistplugins:
- "@acme/pullguard-plugin-payments-rules"
- "@acme/pullguard-plugin-internal-rpc"
Plugin auto-discovery is intentionally disabled. Each plugin must be explicitly named here AND installed in the runner. Plugins execute in a sandboxed context with a limited API surface — supply-chain defence by design.
.driftrc.yml reference
Every option, with defaults and a one-line comment each. All keys are
optional — an empty file is valid; set only what you
want to override. Copy this as a starting point. (.driftrc.yaml /
.driftrc.json are also accepted.) Baseline / delta options are covered under
the schema table above and on the
Reports & Dashboard page.
# .driftrc.yml — PullGuard configuration reference (every option, with defaults).
# Place at the root of the repo PullGuard scans. All keys are OPTIONAL — an empty
# file is valid. Set only what you want to override.
# ── File discovery ───────────────────────────────────────────────────────────
exclude: # APPENDED to built-ins (node_modules, dist, .git, target, build…)
- "**/generated/**"
- "**/*.min.js"
maxDepth: 15 # default 15 — max directory depth
maxFiles: 5000 # default 5000 — monorepo safety cap
# ── Per-analyzer overrides (key = analyzer ID; run `pullguard list-rules`) ─────
analyzers:
builtin/naming-conventions:
enabled: true # default true
severity: minor # escalate/downgrade (info|minor|moderate|major|critical)
builtin/duplication:
enabled: false # turn an analyzer off entirely
# Bring-your-own policy rules (Enterprise). pattern = "must NOT match";
# mustContain = "must match". `pullguard import-semgrep <rules.yml>` emits this.
builtin/custom-rules:
options:
rules:
- id: no-system-out
description: "Use the logger, not System.out.println"
pattern: '\bSystem\.out\.println\s*\(' # violation when present
severity: major
files: '**/*.java'
# ── Numeric thresholds ────────────────────────────────────────────────────────
thresholds:
complexity:
maxCyclomatic: 15 # default 15
maxCognitive: 20 # default 20
maxParameters: 5 # default 5
maxFunctionLength: 300 # default 300 (comments/blanks stripped first)
duplication:
minBlockSize: 6 # default 6 — min lines to count as a clone
maxDuplicationPercent: 5 # default 5
monolithicFile:
maxFileLines: 500 # default 500
maxExports: 20 # default 20
nesting:
maxDepth: 4 # default 4
typeCoverage:
minCoveragePercent: 80 # default 80
maxAnyPerFile: 10 # default 10
# ── Output ────────────────────────────────────────────────────────────────────
output:
format: markdown # text (default) | json | sarif | markdown
# ('html' and 'compliance' are CLI --format values, not config values)
minSeverity: moderate # default info — set 'moderate' to cut CI noise
groupBy: file # category (default) | severity | file
showRemediation: true # default true
sarif:
toolName: PullGuard # default PullGuard
toolVersion: "1.3.4"
# ── Cost-of-change estimate ───────────────────────────────────────────────────
cost:
enabled: true # default true — false suppresses all $ figures
hourlyRate: 150 # default 150 (USD)
# ── Over-time dashboard (pullguard dashboard → self-contained HTML) ───────────
dashboard:
retainScans: 50 # default 50 — per-scan findings kept for drill-down
# in .drift-scan-details.json; 0 = keep all scans
# ── SLA / aging gate (flag findings open past a per-severity age budget) ──────
# Age measured from firstSeenAt (.drift-history.json — commit it).
sla:
critical: 7 # days — flag a critical open longer than this
major: 30
moderate: 90 # all five severities are supported —
minor: 180 # omit a severity to set no SLA for it
info: 365
failBuild: false # default false — true = exit non-zero on any breach
# ── Compliance evidence (SOC 2 always on; these add framework tables) ─────────
compliance:
hipaa: false # default false
pci-dss: false # default false (note the hyphen)
nist: false # default false
iso-27001: false # default false
# ── Repo-type hint (overrides auto-detected hygiene gating) ───────────────────
repo:
type: private-enterprise # public-oss | private-enterprise | customer-delivery | internal-service | fork-research
# ── Custom taint: sources / sinks / sanitizers (appended to built-in 7-lang) ──
# Keyed by LANGUAGE; values are REGEX. Use single quotes so backslashes survive.
taint:
sources: # framework/internal APIs returning attacker-controlled data
java:
- '\bHttpServletRequest\.getParameter\s*\('
javascript: # covers .js AND .ts
- '\bctx\.request\.body\b'
sinks: # dangerous APIs that must not receive tainted data
java:
- '\bTemplateEngine\.render\s*\('
sanitizers: # escaping/validation that neutralises taint (any language)
- '\bSecurityUtils\.escapeHtml\s*\('
# ── AI-authorship provenance (EU AI Act Art. 50 evidence — opt-in) ────────────
aiProvenance:
enabled: false # default false
markers: # case-insensitive substrings in source comments
- "AI-generated"
- "Generated by GitHub Copilot"
# ── Baseline (report only NEW findings vs a saved report) ─────────────────────
baseline: null # default null — e.g. ".drift-baseline.json"
collapsePreexistingSecurity: false # default false — in delta mode, collapse baseline
# (pre-existing) security findings into a counted
# PR-comment section; nothing leaves the full
# report, SARIF, or the fail-on-severity gate
# ── Wasm hot-path (compute-heavy analysis acceleration) ───────────────────────
wasm:
enabled: true # default false — route supported analyzers through the
# bundled high-performance engine (degrades gracefully
# when unavailable)
# ── Plugins (explicit opt-in; sandboxed; no auto-discovery) ───────────────────
plugins: [] # e.g. ["@acme/pullguard-rules"]
# ── Local vuln DB (offline / air-gapped CVE scanning) ─────────────────────────
db:
path: "~/.drift-detector/db" # default ~/.drift-detector/db
maxAgeDays: 7 # default 7 — warn if DB is staler
sourceUrl: "https://osv-vulnerabilities.storage.googleapis.com" # OSV mirror override
By default a scan reports the whole codebase. On a busy repo that means every pull request shows the same long list and developers stop reading it. Baseline and delta modes fix that — they show only what changed, so a PR comment reads “this change introduced 3 findings” instead of “the repo has 200”.
| Mode | Shows | How |
|---|---|---|
| Full (default off-PR) | The entire inventory. | scan . or scan . --full |
| Baseline diff | Only findings new vs a committed snapshot. | scan . -b .drift-baseline.json |
| PR delta (“Clean as You Code”) | Only findings the pull request introduced. | scan . --delta — auto-on for PR events |
Resolution order when more than one applies: an explicit
--baseline <file> → the baseline: key in
.driftrc.yml → an auto-detected .drift-baseline.json
at the repo root. Opt out with --no-baseline.
Critical and security findings always surface
even in delta mode — an injection, secret, or RCE is never hidden just
because it is “old” or sits in an unchanged file.
.drift-baseline.json fileA fingerprinted snapshot of an accepted scan. Each finding carries a stable fingerprint that survives line-number shifts — an edit above a finding does not make it look “new”. Commit the file so every later scan can diff against it.
{
"version": "1.3.4",
"generatedAt": "2026-06-23T10:14:55.812Z",
"projectPath": "/work",
"score": 22,
"grade": "B",
"findingCount": 3,
"findings": [
{
"fingerprint": "kx9p2a_87",
"ruleId": "complexity",
"type": "high_complexity",
"file": "src/main/java/com/example/OrderReconciler.java",
"severity": "moderate",
"excerpt": "Function 'reconcile' has cyclomatic complexity 21 (threshold 15)"
},
{
"fingerprint": "3mf0wq_112",
"ruleId": "duplication",
"type": "code_duplication",
"file": "src/main/java/com/example/LegacyImporter.java",
"severity": "minor",
"excerpt": "Duplicated block (18 lines) also in BulkImporter.java"
},
{
"fingerprint": "9aa7zk_64",
"ruleId": "monolithic_file",
"type": "monolithic_file",
"file": "src/main/java/com/example/ContentService.java",
"severity": "moderate",
"excerpt": "File has 1240 lines (threshold 1000)"
}
]
}
Recommended (CI): let the scan that already
runs on your base branch maintain the baseline. Add
update-baseline: true to a run triggered on a push to your
integration branch — it writes .drift-baseline.json from
that scan’s full inventory, automatically at the right tier, image, and
context, so the baseline always matches what your PR scans compare against.
No separate key-handling step:
# .github/workflows/pullguard-baseline.yml — runs on the base branch
on:
push:
branches: [develop] # your integration branch
permissions:
contents: write
jobs:
baseline:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: pullguard-dev/pullguard-action@v1
with:
license-key: ${{ secrets.PULLGUARD_LICENSE_KEY }}
update-baseline: true
# commit the refreshed baseline
- run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
git add .drift-baseline.json
git diff --cached --quiet || git commit -m "chore: refresh PullGuard baseline"
git push
Your PR workflow needs no change — on a pull request the scan auto-detects
the committed .drift-baseline.json and shows only new findings.
Alternative (one-off / local): the dedicated command writes the same file directly.
pullguard baseline . --license-key "$KEY" # generate at your scan tier
# In Docker (offline / air-gapped form)
docker run --rm -v "$PWD:/work" -e PULLGUARD_LICENSE_KEY="$KEY" \
--entrypoint node ghcr.io/pullguard-dev/pullguard:1 \
/app/dist/bin/drift-detector.js baseline /work -o /work/.drift-baseline.json
git add .drift-baseline.json && git commit -m "chore: PullGuard baseline"
Refresh it the same way after your integration branch moves — regenerating is just a scan written to disk.
--license-key / PULLGUARD_LICENSE_KEY)
and generate it with the same Action/image you scan with — otherwise the
delta can’t match your scan’s findings and a PR shows a near-full
report. Keep .drift-baseline.json tracked (not git-ignored)
so CI can read it.
Security always surfaces. The delta hides
pre-existing tech-debt on untouched files, but security findings appear
on every PR by design. To keep a security-heavy repo’s PR view focused,
set collapsePreexistingSecurity: true (or the
collapse-preexisting-security Action input) — pre-existing
security findings move to a collapsed, counted section while new ones stay
inline; nothing is removed from the full report, SARIF, or the build gate.
Every comparison prints a one-line summary; the report body then contains only the new findings:
Baseline comparison: 3 new, 5 fixed, 42 unchanged (delta: -2)
The committed history files that power finding ages and the over-time
dashboard (.drift-history.json,
.drift-scan-details.json, .drift-trend.json) are
covered on the Reports & Dashboard page.
The Action wraps the same CLI shipped in the image. Run it directly as
pullguard <command> (or
node /app/dist/bin/drift-detector.js <command> inside the
container). --help works on any command.
| Command | What it does |
|---|---|
scan [path] | Analyze a project and output a report (text / json / sarif / markdown / html). |
init | Create a starter .driftrc.yml. |
baseline [path] | Write a fingerprinted snapshot of current findings (default .drift-baseline.json). |
dashboard [path] | Render the self-contained over-time HTML dashboard. --org <dir> renders a portfolio view across many repos. |
trend | Show the compliance-score trend over time. |
ignore <fingerprint> | Add a finding to .pullguardignore (--status wontfix|false_positive|acknowledged). |
list-rules | List every registered analyzer and its metadata. |
import-semgrep <file> | Convert the regex subset of a Semgrep ruleset into .driftrc.yml custom rules. Enterprise |
db update|status|export|import | Manage the local CVE database for offline / air-gapped scanning. |
scan options| Flag | Effect |
|---|---|
-f, --format <fmt> | text | json | sarif | markdown | compliance | html (default text). |
-o, --output <file> | Write the report to a file instead of stdout. |
-c, --config <path> | Path to a .driftrc.yml (auto-detected at the root otherwise). |
--min-severity <level> | info | minor | moderate | major | critical. |
-b, --baseline <file> | Show only findings new vs a baseline (auto-detects .drift-baseline.json). |
--no-baseline | Ignore any committed baseline — show the full inventory. |
--update-baseline | Write/refresh .drift-baseline.json from this scan (run a full scan on your base branch). Refuses on a PR/delta scan. |
--delta / --full | Force PR-delta filtering / force full output (delta auto-on for PR events). |
--blame | Annotate each finding with its git-blame “introduced” date. |
--base-ref <ref> | Git ref to diff against (default $GITHUB_BASE_REF → origin/main). |
--collapse-preexisting-security | In delta mode, collapse pre-existing (baseline) security findings into a separate counted section of the PR comment — nothing is removed from the full report, SARIF, or the gate. See Baseline & delta. |
--cost / --hourly-rate <n> | Show a technical-debt cost estimate (default rate 150). |
--input <file> | Reformat an existing JSON report (skip scanning). |
--license-key <key> | License key (or set PULLGUARD_LICENSE_KEY). |
--cache | Incremental analysis — cache results in .drift-cache.json and re-analyze only files changed since the last run. |
--no-color | Disable colored output (the NO_COLOR environment variable is also respected). |
-q, --quiet / -v, --verbose | Score line only / analyzer timing + debug. |
Full per-command help: pullguard scan --help.
.pullguardignore
Suppression file at the repo root for known-acceptable findings.
File-glob patterns suppress entire paths; rule: entries
suppress a specific rule globally or in a path.
# Comments start with #
# Suppress all findings in vendored / generated paths
vendor/**
src/generated/**
**/*.gen.ts
# Suppress a specific rule everywhere
rule:builtin/complexity:cyclomatic-too-high
# Suppress a specific rule in a specific path
rule:builtin/duplication:type-1-clone src/legacy/**
# Suppress an analyzer's findings on one file
rule:builtin/dead-code:* src/api/public-surface.ts
Hard floor: security-category analyzers cannot be
suppressed via rule: entries. The loader rejects any
suppression targeting a security rule with a clear error. File-glob
suppressions still apply (a path you do not scan is a path you do not
scan), but a security rule cannot be silently ignored on a file that
IS scanned.
Team
Enterprise
Comment /pullguard ignore <rule-id> on a PR. The
PullGuard App opens a follow-up PR adding the entry to
.pullguardignore with the original finding linked in the
body for audit purposes. Reviewer of that follow-up PR is the auditor
of record.
Each plan has a different scope. Tier limits are enforced at scan time via online validation; the scanner falls back to Free tier with a clear banner when a cap is hit, so a scan never silently breaks.
| Plan | Analyzers | Repositories | Contributors | Enforcement |
|---|---|---|---|---|
| Free | 14 | Unlimited public; 1 private | Unlimited | Hard — analyzers gated client + server side |
| Pro | 42 of 44 | 1 private (bound at first scan) | Unlimited | Hard — three-layer repo binding (Worker + scanner + Worker server-side) |
| Team | All 44 | Up to 10 private | Unlimited | Hard — repos accumulate on first scan; 11th repo runs Free until a slot is freed or the customer upgrades |
| Enterprise | All 44 | Unlimited | Unlimited | Contractual (no code-level cap) |
When a Team-tier license scans a repository, that repository is recorded against the license. The customer can scan up to 10 distinct repositories. Behaviour at the boundary:
Acme/API vs acme/api) count as the same repo.PullGuard does not cap contributors on any tier. A 5-developer team and a 50-developer team on the same set of repositories pay the same price. Repository count is the single tier dimension.
For runners behind a firewall or with no outbound internet, PullGuard ships a local vulnerability database. Update it from a connected machine, transfer the archive across your air-gap, and run scans with no network calls.
docker run --rm -v "$PWD/db:/db" \
ghcr.io/pullguard-dev/pullguard:latest \
db update --path /db
docker run --rm -v "$PWD/db:/db" -v "$PWD:/out" \
ghcr.io/pullguard-dev/pullguard:latest \
db export --path /db --to /out/pullguard-db.zip
docker run --rm -v "$PWD:/in" -v "$PWD/db:/db" \
ghcr.io/pullguard-dev/pullguard:latest \
db import --from /in/pullguard-db.zip --path /db
db:
path: /opt/pullguard/db # absolute path on the runner
maxAgeDays: 30 # warn (do not fail) when DB exceeds this
With db.path set, no outbound calls to OSV are made. The
archive is integrity-checked on import (SHA-256). On-disk files are
created with restrictive permissions.
| Secret name | Required for | Value |
|---|---|---|
PULLGUARD_LICENSE_KEY |
Pro / Team / Enterprise | Your pg_live_* token from Stripe checkout email. |
GITHUB_TOKEN |
All tiers | Auto-provided by GitHub Actions — you do not create this. |
PullGuard needs the following permissions: block at the
workflow or job level:
permissions:
contents: read # to read your code
pull-requests: write # to post the PR comment
checks: write # to write Check Runs (Enterprise: report-to-app)
If your organisation default permissions are restrictive (recommended), the block above is required. If your defaults are permissive (read-write), the workflow runs without an explicit block.
Repository-level is the CISO-friendly default — each repo gets its own secret, with explicit per-repo audit trail and no cross-repo blast radius. Organisation-level with a repo allowlist works for platform teams managing many repos. Both models are supported; the workflow file is identical.