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:
| When | What |
|---|---|
draugr tools install | each tool's pinned release archive, verified against a recorded SHA-256 |
draugr feeds update | the CISA KEV catalog and the FIRST EPSS scores |
draugr self-update | the latest Draugr release |
draugr doctor | the latest Draugr release, to compare against yours |
| a scan, before it starts | Trivy's vulnerability database and Nuclei's template set |
| a scan, per target | the 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 --offlineskips only the release check.DRAUGR_NO_UPDATE_CHECK=1does the same, for someone who does not want to be told about releases but is otherwise online.
Related#
- Caching and performance — what a scan reuses between runs.
- Prioritization — KEV and EPSS, and what a stale feed costs you.
- CLI reference — every flag, including
draugr feeds.