Static analysis of your own code works the same with or without a network. Dependency scanning does not. The moment a scanner checks a lockfile against known vulnerabilities, it needs advisory data, and advisory data changes every day. On an air-gapped machine nobody is going to fetch it for you.
This is where many "offline" tools quietly fail. They scan offline, but their vulnerability database is whatever shipped with the install, months ago, and the report gives no hint of that. A clean dependency report built on stale data is worse than no report, because people trust it.
This post covers how an air-gapped team keeps CVE and advisory data current: what "offline" has to mean, how signed bundles carry data across the gap, how to verify them on the inside, and how to set a staleness policy that fails loudly.
What "offline" must mean
For an air-gapped or regulated environment, "offline" is not "works without internet most of the time". It means the tool makes no outbound network call at all, and you can prove it.
That rules out a few common behaviours:
- A background licence check or usage heartbeat.
- A version check on startup.
- A live advisory lookup that sends dependency names to a public API.
- A sign-in step that needs a portal.
In Vulkro, one switch turns every outbound call off:
export VULKRO_OFFLINE=1 # every command, every run
vulkro scan . --offline # or one runCommands that need the network (sign-in, fetching updates, secret validation, notifications) are refused with a message saying what to do instead. Do not take that on trust: run the scan in a network namespace with no interfaces and watch for connect() calls.
unshare -rn strace -f -e trace=connect vulkro scan . --offlineThe same variable is honoured by vulkro-sf, the VS Code extension and the install scripts.
Licensing without a sign-in
A machine with no network cannot sign in, so the entitlement has to arrive as a file. Vulkro uses a signed licence file:
vulkro activate <file.lic>A .lic needs no online refresh. In CI, set VULKRO_LICENSE_B64 to the base64 of the file instead of running activate.
Carrying advisory data across the gap
The data travels the same way the binary does: as a file you copy in. What matters is that the file proves where it came from and that nothing changed on the way.
What is in a bundle
A Vulkro .vkbundle is a gzipped tar archive with three parts:
manifest.json: snapshot id, creation time, ecosystems, record counts and a SHA-256 for every data file.manifest.json.sig: an Ed25519 signature over the manifest.data/: one advisory file per ecosystem, plus optional rule packs and compliance frameworks.
The bundler collects advisory data from OSV, NVD, CISA KEV and EPSS. Current bundles cover npm, PyPI, Maven, Go, crates.io, RubyGems, Packagist and NuGet.
A transfer process that holds up to review
A workable routine:
- Fetch on the connected side. Download the current
.vkbundlefrom the customer portal, or the file named bycurrent.urlinhttps://dist.vulkro.com/cve/manifest.json. For anything custom, write to license@vulkro.com. - Verify before it crosses. Run
vulkro update --verifyon the connected side too, so a bad file is caught before it reaches the transfer medium. - Move it through your approved channel: data diode, scanned removable media, or a cross-domain transfer service. The bundle needs no special handling; it is one file.
- Verify and apply on the inside.
- Record it. Log the snapshot id and date in your change record. Auditors ask how current the data was on the day of a scan.
Verification on import
vulkro update --verify ./vulkro-cve-2026-10-21.vkbundle # check only, writes nothing
vulkro update --bundle ./vulkro-cve-2026-10-21.vkbundle # check, then apply--bundle checks the manifest signature against public keys built into the binary, then checks every file's SHA-256 against the manifest. Only then does it write: files are staged in a temporary directory and swapped into ~/.vulkro/data/ in one step. Any failure stops before the swap, so the data directory is never half-updated, and the command exits 2.
Two properties matter for a security review. There is no trust on first use and no "warn but allow": a signature that matches no built-in key is refused. And the trust list lives in the binary, so whoever controls a mirror or a USB stick cannot name their own key.
Mirroring and re-signing internally
Larger estates usually run an internal mirror rather than carrying files to every machine:
export VULKRO_CDN_BASE_URL="https://artifacts.internal/vulkro-cve"
vulkro updateThe mirror serves the same layout as dist.vulkro.com/cve, and bundles are still verified against the built-in keys. If your process re-signs data internally, add your own public key with --trust-key. It is added to the built-in list, never substituted for it.
A staleness policy that fails loudly
Signed and verified data can still be old. The policy has to make age visible and, where it matters, blocking.
Make age visible on every scan. Each table-mode scan prints a footer with the bundle date and age, and JSON, SARIF and SBOM output carry a cve_bundle field with newest_generated_at, age_hours and ecosystems. Keep that field with the report so the evidence carries its own date.
Fail CI when data is too old:
vulkro scan --max-bundle-age 168 # warn past 7 days
vulkro scan --max-bundle-age 168 --max-bundle-age-fail # exit 2 past 7 days, or if missingWatch for coverage gaps, not just age. If a project has dependencies in an ecosystem the applied bundle does not cover, the scan says so on stderr and states those dependencies were not checked. That is the difference between "no known vulnerabilities" and "not checked".
Rescan without the source. If the code lives in a different enclave from the scanner, generate an SBOM once and re-match it after each import:
vulkro match-cve --sbom sbom.json --format tableA weekly import with a seven-day CI limit is a sensible default. Teams under tighter rules shorten both; the point is that the limit is written down and enforced by the pipeline, not remembered.
FAQ
Can a SAST tool run fully air-gapped?
Yes, if it makes no outbound calls at all, takes its licence as a file and takes vulnerability data as signed bundles you import. Verify the first claim yourself by running a scan with no network interface.
How do I update CVE data on an offline machine?
Download the signed bundle on a connected machine, verify it, carry it across your approved channel, then run vulkro update --bundle <file>. The signature and every file hash are checked before anything is written.
How old can offline vulnerability data be?
Set a limit and enforce it. --max-bundle-age with --max-bundle-age-fail makes a scan exit 2 when the bundle is older than the limit or missing. Seven days is a common starting point.
Read more on the CVE bundle and Vulkro as an air-gapped SAST tool, or Start a 14-day trial. Related: air-gapped static analysis and deterministic offline scans an auditor can rerun.