Skip to main content

Offline SAST for regulated teams: a scan an auditor can re-run

The Verizon 2026 Data Breach Investigations Report, covering incidents from 2024-11-01 to 2025-10-31, finds that 31% of breaches now start with a software vulnerability, overtaking stolen passwords, and 48% involve ransomware. The regulators have noticed. The EU Cyber Resilience Act has been in force since 2024-12-10, its reporting duty for actively exploited vulnerabilities applied from 2026-09-11, and its main obligations arrive on 2027-12-11. The ISO/IEC 27001:2022 transition ended on 2025-10-31, bringing 93 Annex A controls and a new one, A.8.28 Secure coding, that asks for security built into the development lifecycle.

Teams in finance, defence, healthcare and the public sector face a specific problem with most scanners: the tool is a cloud service with a command-line client in front of it. Source leaves the building to be analysed, results live in someone else's tenant, and the output changes between runs as rules update silently. None of that survives a question from an auditor who wants to reproduce the evidence.

This post is about three properties a scanner needs in that environment: zero egress you can test, vulnerability data that stays current without a connection, and output that is identical when re-run. It ends with the one honest caveat offline operation brings.

Zero egress is a property you can test

A claim of "no network calls" is only useful if you can check it. Vulkro has one switch for the whole binary: set VULKRO_OFFLINE=1, or pass --offline on any command, and every path that would reach the network is refused rather than attempted. The offline-mode docs list what that refuses: login, rules add, notify, probe, cloud, publish, install-extension and the --validate-secrets flag, which would otherwise check a found secret against its provider.

The refusal is the point. A scanner that quietly degrades when it cannot phone home is harder to trust than one that stops and says what it would have done. Under VULKRO_OFFLINE=1, a secret is reported as found, not as validated, and the report says which.

To prove it rather than trust it, the same page gives the Linux form: unshare -rn vulkro scan . --offline. That runs the scan in a network namespace with no interfaces. If the binary tried to reach anything, the call fails and the scan fails with it. The command is small enough to put in the audit notes next to the result.

Current vulnerability data without a connection

The usual argument against offline scanning is that dependency checks go stale. The fix is a signed bundle you move across the boundary yourself.

The air-gap flow is three commands: vulkro activate <file.lic>, then vulkro update --bundle ./vulkro-cve-<date>.vkbundle, then vulkro scan .. The update --bundle and --verify subcommands still work under VULKRO_OFFLINE=1, because reading a local file and checking its signature does not need a network. If your organisation mirrors artifacts internally, VULKRO_CDN_BASE_URL points the updater at that mirror instead of the public one.

Stale data is still a risk if nobody moves the bundle. The scanner can enforce freshness itself. vulkro scan --max-bundle-age <hours> reports when the CVE bundle is older than the limit, and adding --max-bundle-age-fail turns that into an exit code of 2, the error code, so a pipeline that forgot to refresh its bundle fails loudly instead of printing a clean dependency report on old data. The flags are on the scan reference.

Byte-identical output

An auditor who re-runs your scan and gets a different file has learned nothing about your code and something about your tool. Determinism is what makes a scan result evidence rather than a snapshot.

Two things get in the way: timestamps and caches. --no-timestamp pins the clock fields in the output so two runs on the same tree produce the same bytes. The cache is disabled with VULKRO_DISABLE_CACHE=1, and VULKRO_DETERMINISTIC=1 fixes ordering. These are not special audit settings; they are how Vulkro measures itself. The published benchmark for 0.36.0, run on 2026-10-19 across nine blind applications, was executed with VULKRO_OFFLINE=1, VULKRO_DISABLE_CACHE=1 and VULKRO_DETERMINISTIC=1. It found 181 of 229 planted vulnerabilities and flagged 39 of 152 known-safe controls at Medium or above. Those numbers are reproducible for the same reason your scan is.

Determinism also has a plain engineering benefit. Diffing two scan outputs becomes meaningful: a changed line in the report is a changed finding, not a reordered one.

Mapping the output to the frameworks

Evidence is only evidence if it points at a control. vulkro compliance-pack . --framework iso27001-2022 --offline writes a pack mapped to the 2022 Annex A, including A.8.28. The same command takes nis2 and dora for EU teams. For US reference, NIST SP 800-218, the Secure Software Development Framework v1.1 published 2022-02-03, describes the practices these controls expect, and a reproducible scan with a dated bundle is a direct answer to its verification practices.

Be clear with the auditor about scope. A scan produces evidence for code-level controls: the vulnerability was found, the fix was made, the re-scan shows it gone. It does not prove that your review process exists or that your training happened. Keep those records separately and let the scan carry the part it can carry.

The one caveat

Offline entitlement works on a signed file with an expiry. A lease refreshed online renews itself; an offline installation does not, so it lapses at the signed date unless you install a .lic file for the term. The portal's Connect page issues that file. For CI runners that cannot hold a file, VULKRO_LICENSE_B64 carries the same licence as an environment variable. Plan the renewal into the same calendar entry as the bundle refresh and the problem disappears.

For teams that want the shared console without a cloud tenant, Vulkro Cloud can also run on your own servers.

What to do this week

  • Run one scan with unshare -rn vulkro scan . --offline and keep the command and its exit code with the result.
  • Add --max-bundle-age <hours> --max-bundle-age-fail to the pipeline so a stale bundle fails as an error, not as a clean report.
  • Run the same scan twice with --no-timestamp and diff the outputs. If they differ, you have found a non-deterministic input to fix.
  • Generate the first compliance-pack for the framework your auditor named, and label the controls it does not cover.
  • Put the bundle refresh and the licence expiry on one recurring calendar entry.

The offline-mode docs cover every refused command and the mirror setup. To try it on your own code, start the 14-day trial, then generate an offline licence from the Connect page.