Air-gapped SAST: what offline code scanning really needs
Air-gapped SAST sounds simple: run the static analysis on a machine with no network. In practice, many tools that run locally still expect the internet somewhere. A licence check calls home, a dependency lookup goes to a public API, an explanation is fetched from a hosted model. On a classified or regulated network, any of those calls stops the tool from being approved.
The code that most needs review often sits on exactly those networks: defence programmes, government systems, payment and core banking platforms. This post sets out what offline scanning actually requires, how to keep vulnerability data current across the gap, and how Vulkro's command-line tools handle each part.
What does air-gapped SAST actually require?
"Runs locally" is not the bar. A scanner is fit for an air-gapped network when each of these is true:
- The analysis is local. Parsing, data-flow tracing and rule matching run on the machine. No code, path or finding is sent anywhere for "deeper" analysis.
- The scan makes no outbound calls. Not during the scan, and not as a side effect of it.
- There is one switch that refuses every call, and it fails closed. A command that would need the network stops with a message. It does not quietly carry on with stale behaviour.
- The licence works without the network. A licence that must refresh online fails the day the cable is unplugged.
- Vulnerability data arrives as a file. Dependency checks need advisory data, so it has to cross the gap in a form you can integrity-check before applying.
- Freshness is enforceable. You can refuse to scan with data older than your policy allows.
- You can test all of the above yourself. Run the scan with no network interface and see it finish.
A deterministic engine helps here. A scan that reads code with fixed rules needs no model endpoint, and the same input gives the same output on either side of the gap. We covered why that matters in deterministic analysis vs asking a model.
Why "offline mode" is often not offline
Most surprises come from calls that sit outside the scan itself. The usual suspects:
- a licence heartbeat or lease refresh,
- a live lookup of dependency versions against a public vulnerability API,
- secret validation, which sends a found key to its provider to see if it still works,
- AI-written explanations or fixes from a hosted endpoint,
- update checks and install-time analytics.
Each is reasonable on a connected laptop. Each is a problem on an isolated network, and the hardest ones to catch are those that fail silently, because the scan then "works" with missing data.
How do you stop every outbound call?
Vulkro's scan makes no outbound call. To make that a guarantee rather than a default, set one variable:
export VULKRO_OFFLINE=1 # every command, every run
vulkro scan . --offline # or just this run
vulkro-sf, the VS Code extension and the install scripts read the variable the same way. Under it, commands that would need the network are refused with a message that says what to do instead. From the offline mode docs:
With VULKRO_OFFLINE=1 | Behaviour |
|---|---|
vulkro scan | Runs normally, fully local |
| Heartbeat and lease refresh | Off |
vulkro login | Refused: use vulkro activate <file.lic> |
vulkro update | Refused, except --bundle and --verify |
scan --validate-secrets | Refused |
| A cloud AI endpoint | Refused; a local model on 127.0.0.1 still works |
One honest note for your security team: on a connected machine, Vulkro sends a small usage heartbeat by default. It carries usage fields only, never a path, a finding or a line of source, and the offline switch turns it off along with everything else. The network egress page lists every call the scanner can make, field by field.
Then check it yourself rather than taking our word for it:
# Linux: a network namespace with no interfaces
unshare -rn vulkro scan . --offline
Licensing a machine that never connects
Sign-in needs the network, so an isolated machine uses a licence file instead:
vulkro activate ./your-team.lic
activate checks the file's signature and stores the key locally. A licence file can be created on the Connect page of the customer portal from a connected machine, or issued by our team at license@vulkro.com for anything custom. On an air-gapped CI runner, set VULKRO_LICENSE_B64 to the base64 of the file instead of running activate. A licence file needs no refresh, so nothing on the isolated side ever tries to reach us. See vulkro activate.
Getting vulnerability data across the gap
Dependency checks are only as good as the advisory data behind them. Vulkro reads that data from a local directory, and on an isolated machine it arrives as a bundle file:
vulkro update --verify ./vulkro-cve-<date>.vkbundle # check it, write nothing
vulkro update --bundle ./vulkro-cve-<date>.vkbundle # check it and apply it
The bundle's manifest lists a SHA-256 for every file. --bundle checks each one before it writes anything, stages the files, and swaps them into place in one step. A failed check stops before the swap, so the data directory is never left half-updated. The bundle format page shows the layout.
If you run an internal artifact server, you can mirror the published layout there and point the scanner at it with VULKRO_CDN_BASE_URL, so vulkro update pulls from inside the network instead of from a person's USB stick.
How do you keep offline vulnerability data current?
The risk on an isolated network is not a missing feature. It is data that quietly ages. Treat the transfer as a scheduled process with an owner:
- On a set cadence (weekly is common), obtain the latest bundle on the connected side.
- Run
vulkro update --verifythere and record the snapshot date from the manifest. - Move it through your approved transfer route: a data diode, a cross-domain solution or controlled media, under your normal review.
- Verify again on the isolated side, then apply with
--bundle. - Enforce freshness in the pipeline, so a missed transfer is visible:
vulkro scan . --offline --max-bundle-age 168 --max-bundle-age-fail
With --max-bundle-age-fail, a bundle older than the limit (or missing) is an error, exit code 2, rather than a scan that passes on old data. Without it, the age check only warns.
Who needs air-gapped scanning?
Defence contractors and government agencies working on classified or sovereign networks are the obvious case. Banks and payment processors often isolate build systems for core platforms, and EU rules such as DORA push them to show their controls work, which we covered in why security tooling is no longer optional. Critical infrastructure operators and some healthcare and research environments run isolated networks for the same reason.
These teams still have to review code. NIST's Secure Software Development Framework (SP 800-218) includes a practice, PW.7, for reviewing and analysing human-readable code to find vulnerabilities. On an isolated network, the analysis has to come to the code.
What offline does not cover
Being precise about limits is part of the job:
- Vulnerability data is as current as the last bundle you carried in. Offline cannot fix a transfer that did not happen; the freshness flags make it visible.
- Secret validation needs the provider, so it is refused. Secrets are still found by format in code and history.
- Reading a live Salesforce org needs a route to the org. Source and retrieved metadata scan fully offline with
vulkro-sf. - Vulkro Cloud is a hosted service. Offline applies to the command-line tools and the VS Code extension.
A scan that runs is also not the same as a scan that read everything. Check the coverage report on every run, as described in a clean scan that did not read your code.
Checklist for an air-gapped SAST rollout
VULKRO_OFFLINE=1set system-wide on isolated machines and runners- Licence file installed with
vulkro activate, orVULKRO_LICENSE_B64in CI - A named owner and cadence for bundle transfers
--verifyon both sides of the gap, snapshot date recorded--max-bundle-age-failin the pipeline- A one-time test with no network interface, kept as evidence
Frequently asked questions
Can SAST run fully offline?
Yes, if the analysis, the licence and the vulnerability data are all local. Vulkro's scan makes no outbound call, and VULKRO_OFFLINE=1 refuses every other call so nothing can reach out by accident.
How do you update vulnerability data on an air-gapped network?
Carry a bundle file across on a schedule and apply it with vulkro update --bundle. Every file is checked against the manifest's SHA-256 before anything is written, and --max-bundle-age-fail stops the pipeline if the data is older than your policy.
Does an air-gapped scanner need a licence server?
Not with a licence file. vulkro activate <file.lic> installs it locally, and it needs no refresh, so the isolated machine never contacts a licence service.
Is offline scanning less accurate?
The code analysis is the same with or without a network, because the scan never used one. What can lag is dependency advisory data, which is why the transfer cadence and the freshness check matter.
Where to see it
The air-gapped use case and what offline SAST means here summarise the setup. The details are in the docs for offline mode, network egress and the bundle format. To get a licence file for an isolated machine, start here.
