diff --git a/enroll-diff.md b/enroll-diff.md new file mode 100644 index 0000000..867f298 --- /dev/null +++ b/enroll-diff.md @@ -0,0 +1,260 @@ +The Enroll `diff` subcommand will compare two 'harvests' and see if there are any differences. + +# What does diff do? + +These differences include: + + * Packages added/removed + * Files added/removed/changed + * Users added/removed/changed + * Services added/removed/changed + +The `diff` command can also understand SOPS-encrypted harvests, assume the user executing the command is able to decrypt the SOPS files. + +# How do I run it? + +Assuming you have two harvests on your filesystem: + +``` +enroll diff \ + --old harvest.v1 \ + --new harvest.v2 +``` + +# Output formats + +The default output is just a plain text format, which is the same as passing `--format text`. + +You can pass `--format json` or `--format markdown` to get structured formats instead. + +# --exit-code mode + +You can pass `--exit-code` as an argument, which will return a return code of 2 if there was any 'diff'. + +You can use this to script any further action your cron service might take, such as sending e-mail output, or rely on it to trigger failures in CI builds. + +# Webhooks + +You can also send the output to a webhook under your control. Here is an example: + +``` +enroll diff \ + --old harvest.v1 \ + --new harvest.v2 \ + --webhook https://example.com/some/endpoint \ + --webhook-format json \ + --webhook-header 'X-Enroll-Secret: Some-Secret-Here' +``` + +You can pass `--webhook-format markdown` if you wanted the Markdown format in the payload, but most people probably want JSON structured data for consumption at the other end. + +Use the `--webhook-header` to send some sort of authentication/authorization attribute (e.g an OAuth2.0 bearer token, static secret, etc) to help lock down your webhook from unauthorised requests. You can send multiple headers. + +# Email + +You can send the output via email using email and smtp arguments. See 'Full usage info' below for more information. + +# Example: Running the diff on a systemd timer + +In the example below, we set up a systemd timer to run on a periodic basis and trigger the webhook. + +It will set up a 'golden' harvest if it didn't already exist, then periodically run a second harvest to compare against it. + +You can optionally promote the new harvest to become the new 'golden' harvest for future invocations if you want a continuous, rolling 'diff' scenario. + +## Create script + +Store in `/usr/local/bin/enroll-harvest-diff.sh` and make it `chmod 0700` + +``` +#!/usr/bin/env bash +set -euo pipefail + +# Required env +: "${WEBHOOK_URL:?Set WEBHOOK_URL in /etc/enroll/enroll-harvest-diff.env}" +: "${ENROLL_SECRET:?Set ENROLL_SECRET in /etc/enroll/enroll-harvest-diff.env}" + +# Optional env +STATE_DIR="${ENROLL_STATE_DIR:-/var/lib/enroll}" +GOLDEN_DIR="${STATE_DIR}/golden" +PROMOTE_NEW="${PROMOTE_NEW:-1}" # 1=promote new->golden; 0=keep golden fixed +KEEP_BACKUPS="${KEEP_BACKUPS:-7}" # only used if PROMOTE_NEW=1 +LOCKFILE="${STATE_DIR}/.enroll-harvest-diff.lock" + +mkdir -p "${STATE_DIR}" +chmod 700 "${STATE_DIR}" || true + +# single-instance lock (avoid overlapping timer runs) +exec 9>"${LOCKFILE}" +flock -n 9 || exit 0 + +tmp_new="" +cleanup() { + if [[ -n "${tmp_new}" && -d "${tmp_new}" ]]; then + rm -rf "${tmp_new}" + fi +} +trap cleanup EXIT + +make_tmp_dir() { + mktemp -d "${STATE_DIR}/.harvest.XXXXXX" +} + +run_harvest() { + local out_dir="$1" + rm -rf "${out_dir}" + mkdir -p "${out_dir}" + chmod 700 "${out_dir}" || true + enroll harvest --out "${out_dir}" >/dev/null +} + +# A) create golden if missing +if [[ ! -f "${GOLDEN_DIR}/state.json" ]]; then + tmp="$(make_tmp_dir)" + run_harvest "${tmp}" + rm -rf "${GOLDEN_DIR}" + mv "${tmp}" "${GOLDEN_DIR}" + echo "Golden harvest created at ${GOLDEN_DIR}" + exit 0 +fi + +# B) create new harvest +tmp_new="$(make_tmp_dir)" +run_harvest "${tmp_new}" + +# C) diff + webhook notify +enroll diff \ + --old "${GOLDEN_DIR}" \ + --new "${tmp_new}" \ + --webhook "${WEBHOOK_URL}" \ + --webhook-format json \ + --webhook-header "X-Enroll-Secret: ${ENROLL_SECRET}" + +# Promote or discard new harvest +if [[ "${PROMOTE_NEW}" == "1" || "${PROMOTE_NEW,,}" == "true" || "${PROMOTE_NEW}" == "yes" ]]; then + ts="$(date -u +%Y%m%d-%H%M%S)" + backup="${STATE_DIR}/golden.prev.${ts}" + mv "${GOLDEN_DIR}" "${backup}" + mv "${tmp_new}" "${GOLDEN_DIR}" + tmp_new="" # don't delete it in trap + + # Keep only latest N backups + if [[ "${KEEP_BACKUPS}" =~ ^[0-9]+$ ]] && (( KEEP_BACKUPS > 0 )); then + ls -1dt "${STATE_DIR}"/golden.prev.* 2>/dev/null | tail -n +"$((KEEP_BACKUPS+1))" | xargs -r rm -rf + fi + + echo "Diff complete; baseline updated." +else + # tmp_new will be deleted by trap + echo "Diff complete; baseline unchanged (PROMOTE_NEW=${PROMOTE_NEW})." +fi +``` + +## Environment file + +Save as: `/etc/enroll/enroll-harvest-diff` and `chmod 0600` it. + +``` +# Where to store golden + temp harvests +ENROLL_STATE_DIR=/var/lib/enroll + +# 1 = each run becomes new baseline ("since last harvest") +# 0 = compare against a fixed baseline ("since golden") +PROMOTE_NEW=1 + +# If PROMOTE_NEW=1, keep this many old baselines +KEEP_BACKUPS=7 + +WEBHOOK_URL=https://example.com/webhook/xxxxxxxx +ENROLL_SECRET=xxxxxxxxxxxxxxxxxxxx +``` + + +## Systemd service + +`/etc/systemd/system/enroll-harvest-diff.service`: + +``` +[Unit] +Description=Enroll harvest + diff + webhook notify +Wants=network-online.target +After=network-online.target +ConditionPathExists=/etc/enroll/enroll-harvest-diff + +[Service] +Type=oneshot +EnvironmentFile=/etc/enroll/enroll-harvest-diff +Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +UMask=0077 + +# Create /var/lib/enroll automatically +StateDirectory=enroll + +ExecStart=/usr/local/bin/enroll-harvest-diff.sh +``` + +## Systemd timer + +`/etc/systemd/system/enroll-harvest-diff.timer` + +``` +[Unit] +Description=Run Enroll harvest diff hourly + +[Timer] +OnCalendar=hourly +RandomizedDelaySec=10m +Persistent=true + +[Install] +WantedBy=timers.target +``` + +## Enable + test + +``` +sudo systemctl daemon-reload +sudo systemctl enable --now enroll-harvest-diff.timer + +# run once now +sudo systemctl start enroll-harvest-diff.service +sudo journalctl -u enroll-harvest-diff.service -n 200 --no-pager +``` + +If you want the “fixed baseline” behavior (always diff against the original golden snapshot), set `PROMOTE_NEW=0` and you're done. + + +# Full usage info + +``` +usage: enroll diff [-h] --old OLD --new NEW [--sops] [--format {text,markdown,json}] [--out OUT] [--exit-code] [--notify-always] [--webhook WEBHOOK] + [--webhook-format {json,text,markdown}] [--webhook-header K:V] [--email-to EMAIL_TO] [--email-from EMAIL_FROM] [--email-subject EMAIL_SUBJECT] [--smtp SMTP] + [--smtp-user SMTP_USER] [--smtp-password-env SMTP_PASSWORD_ENV] + +options: + -h, --help show this help message and exit + --old OLD Old/baseline harvest (directory, a path to state.json, a tarball, or a SOPS-encrypted bundle). + --new NEW New/current harvest (directory, a path to state.json, a tarball, or a SOPS-encrypted bundle). + --sops Allow SOPS-encrypted harvest bundle inputs (requires `sops` on PATH). + --format {text,markdown,json} + Report output format (default: text). + --out OUT Write the report to this file instead of stdout. + --exit-code Exit with status 2 if differences are detected. + --notify-always Send webhook/email even when there are no differences. + --webhook WEBHOOK POST the report to this URL (only when differences are detected, unless --notify-always). + --webhook-format {json,text,markdown} + Payload format for --webhook (default: json). + --webhook-header K:V Extra HTTP header for --webhook (repeatable), e.g. 'Authorization: Bearer ...'. + --email-to EMAIL_TO Email the report to this address (repeatable; only when differences are detected unless --notify-always). + --email-from EMAIL_FROM + From address for --email-to (default: enroll@). + --email-subject EMAIL_SUBJECT + Subject for --email-to (default: 'enroll diff report'). + --smtp SMTP SMTP server host[:port] for --email-to. If omitted, uses local sendmail. + --smtp-user SMTP_USER + SMTP username (optional). + --smtp-password-env SMTP_PASSWORD_ENV + Environment variable containing SMTP password (optional). +``` + +The main purpose of the `diff` command \ No newline at end of file