Ga naar inhoud

Feed Internet.nl results from CI

Wanderer's standards dimension (DNSSEC, SPF/DKIM/DMARC, STARTTLS/DANE, IPv6, RPKI, TLS configuration) never talks to Internet.nl itself — it only reads a file. The measurement is netnl's job (internetnl-cli, a separate tool), already running in GitHub Actions for most operators. This page is the three-command route from "a domain measured in CI" to "a scored standards dimension in Wanderer", and the one exit-code detail a CI step must check.

See assessor.md for how the six rules score, and Permanent non-goals for why Wanderer does not measure any of this itself.

The three commands

Run on the host (or CI job) with netnl installed, then on the host (or CI job) with wanderer installed — they do not need to be the same machine, since the coupling point is the exported file, not a network call between the two tools.

# 1. Ask netnl's configured Internet.nl (batch API v2) instance to
#    measure a target. --no-poll returns immediately with a request ID
#    instead of blocking until the batch finishes.
internetnl submit example.nl --type web --no-poll

# 2. Once the batch has finished (poll internetnl's own status command,
#    or re-run this on a later CI tick), export it in the shape
#    Wanderer's importer understands.
internetnl results <request-id> --format findings \
  --findings-out findings.json

# 3. Hand the file to Wanderer. The target domain must already exist
#    in Wanderer (`wanderer org add` / an existing scanned target) —
#    an unknown domain is skipped with a WARN, not an error, so the
#    rest of a multi-domain batch still imports.
wanderer import internetnl --db wanderer.db findings.json

Step 3 also works over HTTP, against a running wanderer serve instance, instead of the CLI's own SQLite file — useful when the server is the only process allowed to touch the database (the Wanderer Deployment is a single RWO volume; a CronJob opening the file directly would be a second writer). Set WANDERER_IMPORT_TOKEN on the server, then post the same file as the request body:

curl -sf -X POST https://wanderer.example/imports/internetnl \
  -H "Authorization: Bearer $WANDERER_IMPORT_TOKEN" \
  --data-binary @findings.json

The response reports the same three counts the CLI prints: imported, skipped_unknown, skipped_already. Idempotency works the same way across both doors — the route hashes the request body exactly as the CLI hashes the file, so posting the same bytes twice (by CLI, by HTTP, or one of each) imports once. Without WANDERER_IMPORT_TOKEN set, the route stays registered but refuses every request with 401 — the server logs import.internetnl.inactive once at startup so that is visible without waiting for a failed request. The REST API is otherwise reachable on the tailnet without authentication; this route is the exception because an import writes verdicts that appear in reports as evidence.

Repeat step 1–3 per measurement type (--type web, --type mail) — they are two independent batches with two independent request IDs. Wanderer correlates a target's latest web and latest mail import automatically (Store.FindingsForAssessment); neither import needs to wait for the other.

After both imports, score the target the same way any other scan is scored — the documented route this file contract is built around:

wanderer assess --db wanderer.db <scan-id>

Any scan ID for the target works, including one from a plain perimeter scan that never touched Internet.nl — the CLI's assess command correlates the latest imported standards evidence onto whichever scan you point it at, so you do not need to assess the import-kind scan itself.

The exit code a CI step must trust

internetnl results <id> --json (netnl's original, general-purpose export) intentionally exits 0 even when the batch is still running — it writes "domains": null and request.status: "running", because polling an in-progress batch and getting a partial answer is normal, expected behaviour for that command.

--format findings does not inherit that. Export of an incomplete or failed batch through --format findings exits non-zero and writes nothing — no findings.json is created at all. This is the one thing a CI step feeding Wanderer must actually check: gate the next pipeline step (wanderer import internetnl ...) on this command's exit code, not merely on the file's existence.

if ! internetnl results "$REQUEST_ID" --format findings \
    --findings-out findings.json; then
  echo "internetnl: batch $REQUEST_ID not ready yet — retry next run" >&2
  exit 1
fi

Trusting a 0 exit from the wrong command here is exactly the failure this guards against: a CI step that calls --json (or ignores exit codes altogether) can archive an empty-looking artifact, report success, and leave Wanderer importing a file with zero domains — which renders as "not measured" for every rule, silently, with no error anywhere in the chain.

What "not measured" means downstream

A target that has never been imported (or whose import predates standards.max_age, default 30 days) scores onbekend on every wand.standards.* rule, with the reason not_measured or measurement_stale. That is a measurement gap, not a failing score — the fleet screen and the per-target worst-score pill both already exclude onbekend dimensions from dragging a target down (see assessor.md). Re-running import internetnl with a fresh export is enough to pick a target back up; nothing needs to be re-scanned on Wanderer's side.