Automation & JSON output

Greylag Goose is deliberately a one-shot CLI: it composes with the scheduler you already run instead of shipping its own daemon. Cron, a systemd timer, or a CI job plus the exit codes and --json cover the "audit me every night" case.

Exit codes

0

all green/yellow — nothing restore-threatening

1

at least one RED finding, or a provider error (a host couldn’t be fully audited, so the result is partial — treat it as a failure, since unaudited is unprotected), or the report couldn’t be delivered to a configured destination

2

config or preflight problem (bad config file, unreachable prerequisite)

Machine-readable report: --json

restoreguard audit --json > report.json

Prints a single JSON document on stdout. The field names are a contract — changes are additive only, so scripts written today keep working:

{
  "schemaVersion": 1,
  "generatedAt": "2026-07-05T03:00:12+00:00",
  "overall": "red",
  "partial": false,
  "target": "offsite-minio",
  "hash": "9f2c1e…",
  "counts": {
    "services": 42,
    "backupArtifacts": 118,
    "storageTargets": 9,
    "red": 1,
    "yellow": 3
  },
  "findings": [
    {
      "ruleId": "db-backup/stale",
      "severity": "red",
      "service": "myapp-prod-postgres-1",
      "host": "mydockerhost",
      "evidence": "newest dump myapp-prod-postgres-1_20260630.sql.gz is 4.2 days old (limit 26h)",
      "suggestedAction": "check the dump job's cron/logs on mydockerhost"
    }
  ],
  "suppressedFindings": [ /* same shape */ ],
  "activeSuppressions": [ /* your suppressions.json entries */ ],
  "providerErrors": []
}
  • schemaVersion is the version of this contract — currently 1, and deliberately decoupled from RestoreGuard’s release version. It bumps only on a breaking shape change (a field renamed, removed, retyped, or given new meaning); additive growth leaves it unchanged. The canonical JSON Schema for each version ships in the repo at contracts/restoreguard-report.v{N}.schema.json — validate against it, and on an unknown (newer) schemaVersion treat the report as one you can’t yet parse rather than guessing its shape.

  • overall is the worst severity present; partial: true means at least one provider failed and providerErrors says which.

  • target is the connection id (from reporting.json) this copy was saved to — distinct per destination, null on stdout. hash is the SHA-256 of the report core (all fields except target and hash), so it is identical across every copy of one audit — a reader dedups by hash and reads when from generatedAt (UTC). Both additive since schemaVersion 1.

  • suppressedFindings and activeSuppressions are always included — the JSON shows what was silenced, same as the text report.

Report destinations: read the shared reporting.json, not stdout

Redirecting stdout works, but the audit already persists every report by itself: to a folder (with a stable latest.json), an S3-compatible bucket, and/or a MongoDB collection. Those destinations live in a standalone reporting.json (the main config just points at it via reportingFile) — see the reportingFile section. That file is self-contained on purpose: a consumer reads the same reporting.json, connects to the same destination, and pulls the reports RestoreGuard wrote — which is exactly how HCC consumes them. Read the destination from that file rather than hard-coding paths, so reconfiguring RestoreGuard never breaks the consumer:

REPORTING=/etc/restoreguard/reporting.json
REPORT_DIR=$(jq -r '.folder.path // empty' "$REPORTING")
REPORT="${REPORT_DIR:-$HOME/.local/share/restoreguard/reports}/latest.json"

(For the S3 or MongoDB destinations, reporting.json likewise carries the endpoint/bucket or connection string the consumer needs — the same fields RestoreGuard wrote with.) latest.json is replaced atomically (temp file
rename), so a reader or file watcher never sees a half-written report. A destination that can’t be written turns the run into exit 1 — delivery failures are as loud as findings.

Nightly audit with cron

# /etc/cron.d/restoreguard — 07:00, after the night's backup jobs finished
0 7 * * * auditor cd /home/auditor/goose && \
  /home/auditor/.local/bin/restoreguard audit --json > /dev/null \
  || echo "backup audit RED or failed - check the report folder" | mail -s "goose alarm" [email protected]

Because a partial audit exits 1 too, "the audit itself broke" and "a backup is bad" both raise the alarm — there is no state where failure looks like success.

The same pattern works with a systemd timer, Gotify/ntfy hooks, or any monitoring system that can run a command and read an exit code; feed the persisted report to jq for dashboards:

jq -r '.findings[] | "\(.severity)\t\(.ruleId)\t\(.service)"' "$REPORT"

Doctor in automation

restoreguard doctor uses the same exit-code convention, so you can run it after infrastructure changes (or weekly) to catch a broken prerequisite — an expired SSH key, a moved dump directory — before it turns the nightly audit into a partial one.