📎 Unreleased docs. This is the next version (from main), features here may change or not be in a release yet.See the latest release →

Guides

Run Draugr air-gapped

Draugr reaches out from a handful of places. --offline, or DRAUGR_OFFLINE=1, says once that this machine has no network, and every one of them honors it.

draugr scan draugr.saga.yaml --offline
DRAUGR_OFFLINE=1 draugr scan draugr.saga.yaml

Offline never fails quietly. Where a fetch was optional it is skipped, with a line saying so. Where it was the whole point of the command, the command refuses and names what it would have downloaded.

What Draugr fetches, and when#

draugr doctor prints this list on any machine, so you don't have to keep it:

WhenWhat
draugr tools installeach tool's pinned release archive, verified against a recorded SHA-256
draugr feeds updatethe CISA KEV catalog and the FIRST EPSS scores
draugr self-updatethe latest Draugr release
draugr doctorthe latest Draugr release, to compare against yours
a scan, before it startsTrivy's vulnerability database and Nuclei's template set
a scan, per targetthe registry, for an image; the endpoint itself, for a host or DAST target

The last row is the one --offline cannot help with. Scanning a remote image or probing a live endpoint is a network operation. If a target is unreachable, the control reports an error rather than a pass.

Preparing a runner#

Do this once, on a machine that has a network, and copy ~/.draugr across.

draugr tools install            # binaries and their data into ~/.draugr/bin
draugr feeds update             # KEV and EPSS into ~/.draugr/feeds
trivy image --download-db-only  # Trivy's vulnerability database, into its own cache
grype db update                 # Grype's vulnerability database, if you run it
nuclei -update-templates        # Nuclei's template set, if you run dast

These databases and template sets live in their own caches, not in ~/.draugr. Copy those too, ~/.cache/trivy, ~/.cache/grype and ~/.local/nuclei-templates by default, all relocatable with TRIVY_CACHE_DIR, GRYPE_DB_CACHE_DIR and NUCLEI_TEMPLATES_DIR.

Grype refuses a database older than five days, and copying one across takes time the clock keeps counting. GRYPE_DB_UPDATE_URL points it at an internal mirror so a runner refreshes from inside your network instead. Raising GRYPE_DB_MAX_ALLOWED_BUILT_AGE is the other lever and the worse one: it buys quiet by letting the scan run against data that has stopped being updated, and a pass earned that way is the failure this tool exists to prevent.

A scan with --offline and no Trivy database does not silently return "no vulnerabilities". The control reports an error and the run fails:

INFO   offline: not refreshing scanner data, using what is on disk
  sca  ERROR  did not run
       run trivy-fs: … --skip-db-update cannot be specified on the first run

That is the intended behavior: a scanner that could not run has found nothing, and nothing found is not the same as nothing there.

Exploitability feeds#

--kev cache and --epss cache read ~/.draugr/feeds and never touch the network, which is what you want on a runner whether or not it has one. auto fetches when the cache is stale. Offline turns that off, so it reads the cache or says clearly there is nothing to read.

A copy older than config.exploitability.maxAge is used and reported as stale rather than refused; on a deliberately pinned runner, raise maxAge so a reproducible verdict does not come with a warning every run. See config.exploitability.

Descriptors that name remote fragments#

A Saga can assemble itself from fragments, and a fragment held in another repository has to be fetched. Offline, that is refused rather than skipped, a fragment that cannot be read is scope the descriptor claims and the run would not have, and a scan quietly covering less than it says is worse than one that stops.

Resolve it on a connected machine and carry the flattened descriptor across instead:

# connected
draugr validate azure.saga.yaml --resolved > acme.flat.saga.yaml

# air-gapped
draugr scan acme.flat.saga.yaml

The flattened copy contains every component and exclusion the fragments contributed, with each remote one recorded at the commit it resolved to, so it is reproducible as well as portable, and the provenance survives the crossing as comments.

Keeping it that way#

--offline is a promise you can check. Run the scan on a host with no route out and it either works or tells you exactly which fetch it needed, which is a better test than trusting the flag, and the one worth putting in CI for an air-gapped environment.

Two narrower opt-outs remain, for a machine that does have a network:

  • draugr doctor --offline skips only the release check.
  • DRAUGR_NO_UPDATE_CHECK=1 does the same, for someone who does not want to be told about releases but is otherwise online.