Docs › Deployment & Data Sovereignty
For security, procurement, and data-protection reviewers — especially EU, government, defence, and regulated-financial buyers who must know exactly what leaves their environment. This page is a plain, verifiable account of where PullGuard runs and what data it handles.
PullGuard runs inside your CI and never receives your source code. The scanner analyses your code in place, on your runners, and emits results-only outputs — reports, SARIF, JSON — that you control. In its air-gapped configuration it makes zero external calls: no telemetry, no source upload, no phone-home. When run online, the only thing that leaves is license validation (an org name + token) — never your code.
One precise disclosure: free-tier scans running in
GitHub Actions may attach an anonymous adoption signal (the public numeric GitHub
repository/owner ids — no code, no findings, no PII) to the rules fetch, with opt-out via
telemetry: false, PULLGUARD_TELEMETRY=off, or
DO_NOT_TRACK=1. Paid, offline, and air-gapped scans
never send any telemetry, ever.
Unlike SaaS scanners that upload your code to their cloud for analysis, PullGuard analyses in place and only ever produces a report that stays with you.
| Mode | Source code | Findings / results | License check | Rule updates | Dependency lookups |
|---|---|---|---|---|---|
| Air-gapped self-hosted runner + mirrored image + offline key |
✗ never leaves | ✗ stays in your CI | ✗ validated locally (signed offline key, no call) | ✗ uses the image's embedded rules; the Tier-2 zero-day threat rules run only from a signed bundle file you carry in (PULLGUARD_RULES_BUNDLE_PATH) |
✗ local CVE database only (PULLGUARD_OFFLINE=true) |
| Online GitHub-hosted runner + GHCR + online token |
✗ never leaves | ✗ stays in your CI / your git host | ✓ org + token → our licensing service (no source) | ✓ signed rule bundle fetched (read-only) | ✓ package names + versions queried against OSV and the public registries (npm / PyPI / Go / Maven / RubyGems) for CVE + freshness checks — internal package names are visible to those services in this mode |
Your code is never uploaded to PullGuard in either mode. The licensing
service only validates subscriptions; it never receives scan data. One documented exception
exists and it is opt-in and off by default: the Action's report-to-app input
POSTs the scan report (findings, including the source-line excerpts findings quote)
to the PullGuard App backend so results render as native Check Run annotations — enable
it only if that flow fits your data posture; PULLGUARD_OFFLINE=true always wins
over it.
For sovereign / classified / no-egress environments, PullGuard runs with no external dependency at scan time:
ghcr.io/pullguard-dev/pullguard:<tag>).PULLGUARD_OFFLINE=true in the scan environment — the one switch that keeps every remaining lookup local: the rule-bundle fetch stays on the image's signed embedded catalog, CVE lookups use only your local database, the registry freshness sweep is skipped, and dependency licences are read only from your own manifests (no calls to deps.dev, ClearlyDefined or package registries) — the licence report then states how many components it could not resolve, so a partial answer can never read as a clean one. Without it, a scan still attempts those calls (they fail closed on an air-gapped network, but attempted egress is not zero egress).curl -o rules-bundle.json https://pullguard.dev/api/rules, copy the file across, and set PULLGUARD_RULES_BUNDLE_PATH to its path. The scanner verifies it with the signing key it already embeds and makes no network call; a modified, unsigned, oversized or unreadable file is refused and the scan falls back to the embedded catalog and says so. Refresh the file when you refresh the image.Result: the scan reads your code, writes a report, and makes zero network calls. CVE scanning stays available offline via the local vulnerability database — update it on a connected machine and copy the directory across your air-gap (the directory is the database; there is no archive step). Most SAST tools cannot operate this way.
The published image already carries a database (scanner 1.5.15 and later), current to the day the image was built, so a mirrored image checks CVEs offline with no extra step. Every report states which data answered the scan and how old it is; refresh the database (or mirror a newer image) when you need advisories published after that date. The requirement below applies when you run the scanner outside the published image.
Populating that database is a required step. It is the only
CVE source on an air-gapped runner — without it there is nothing to check against, and
PullGuard says so rather than implying your dependencies are clean: the scan is marked
limitedCoverage with builtin/dependency-vulnerabilities in
incompleteAnalyzers (visible in the JSON, the SARIF run properties, and the GitHub
Security tab), plus a log warning. Zero CVE findings without a database means
not checked, not clean.
See Air-gapped mode for the setup and how to verify it.
The published scanner image ships SLSA build provenance and an SBOM. Your security team can verify how the image was built (source commit, builder, materials) and what it contains — before it ever runs in your environment:
docker buildx imagetools inspect ghcr.io/pullguard-dev/pullguard:<tag>
The image is also cryptographically signed with keyless cosign (Sigstore) — verify it came authentically from PullGuard's build pipeline, independent of the registry and enforceable by signed-images-only admission controllers (Sigstore policy-controller, Kyverno, Connaisseur):
cosign verify ghcr.io/pullguard-dev/pullguard:<tag> \
--certificate-identity-regexp '^https://github.com/pullguard-dev/PullGuard/\.github/workflows/publish-image\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Windows (Git Bash): the shell rewrites the backslashes in
--certificate-identity-regexp, breaking the pattern. Prefix the command with
MSYS2_ARG_CONV_EXCL='*', or use the exact
--certificate-identity 'https://github.com/pullguard-dev/PullGuard/.github/workflows/publish-image.yml@refs/tags/<tag>'
(no regex). Linux, macOS, and CI need no change.
Release tags (e.g. v1.4.1, the first cosign-signed scanner release) are immutable and preserved forever; pin an exact tag
or a sha256: digest (the image-pin
workflow input) for change-controlled
environments.
Want a live, multi-repo dashboard without giving anyone your data? The self-hosted PullGuard server (Enterprise) runs inside your boundary, on your domain. Your CI POSTs scan results only — findings, scores, grades — never source code; the ingest endpoint actively rejects source-looking payloads. The server validates its licence offline and makes no call home, so it too runs fully air-gapped.
Don't take our word for it — every claim above is testable in your own environment:
docker buildx imagetools inspect ghcr.io/pullguard-dev/pullguard:<tag> — confirm the SLSA build provenance + SBOM.Questions from a procurement or data-protection review are welcome — hello@pullguard.dev.