diff --git a/README.md b/README.md index c7f3419..35c8533 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ Lists listener protocol, port, and bind scope (`loopback`, `specific-address`, o ### `update-posture.sh` -Counts updates visible in existing APT or DNF metadata and checks the reboot-required marker. It never refreshes metadata, lists package names, installs updates, or reboots. +Counts updates visible in existing APT or DNF metadata, reports deferred/kept-back APT candidates as a privacy-safe aggregate, and checks the reboot-required marker. It never refreshes metadata, lists package names, installs updates, or reboots. ```bash ./scripts/update-posture.sh diff --git a/SHA256SUMS b/SHA256SUMS index 0ed7a97..4df3b87 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -8,7 +8,7 @@ cf4606e3cd496dd9d8ca9db1281488507e3abeb2f45f460f1af070297a3fec22 scripts/exim-q 859fcefc6b631f9b63e28db8c2f58372e55adfe567e70c3d2317e609b5543d59 scripts/memory-pressure.sh c35c04f1f5f54eb945b46cbd6ebecb05146f02baf8f7de5bbe967c494ec5bb56 scripts/service-health.sh a12029e59604066a57edf0bd12668a34031779d59838957d7991973580ea8c2f scripts/listener-exposure.sh -7e04e99afcaad2d704e69f5efc5e4b1866804f451cf90e730f83419b42ff1da9 scripts/update-posture.sh +2bcf1ab868d6055c61b507407b424cd753894bc8d0b190e06c9a156281dbeb59 scripts/update-posture.sh fe3e47781e5694ebcc2c260df97bb3421fa5e13fa353c0e25e00c9ecffaf2a8b scripts/backup-freshness.sh 46d3961f9364b9ccf7cda154dcef52810855aaf6040ac08a416d641b014e84fb scripts/endpoint-health.sh ea624496235f3d78cd38be3173f6bb18d898d7cc25b71fe36d678459691b61f0 scripts/tls-expiry.sh diff --git a/scripts/update-posture.sh b/scripts/update-posture.sh index 21eaba6..9055186 100755 --- a/scripts/update-posture.sh +++ b/scripts/update-posture.sh @@ -33,6 +33,7 @@ fi manager='none' updates_total=0 updates_security=0 +updates_deferred=0 APT_GET_BIN="${APT_GET_BIN:-$(command -v apt-get 2>/dev/null || true)}" DNF_BIN="${DNF_BIN:-$(command -v dnf 2>/dev/null || true)}" if [[ -n "$APT_GET_BIN" && -x "$APT_GET_BIN" ]]; then @@ -41,7 +42,20 @@ if [[ -n "$APT_GET_BIN" && -x "$APT_GET_BIN" ]]; then printf 'ERROR: apt update simulation failed\n' >&2 exit 1 fi - updates_total="$(awk '/^Inst /{count++} END{print count+0}' <<<"$raw")" + installable_count="$(awk '/^Inst /{count++} END{print count+0}' <<<"$raw")" + updates_deferred="$(awk ' + { + for (i = 2; i < NF; i++) { + if ($i == "not" && $(i + 1) ~ /^upgraded[.]?$/ && $(i - 1) ~ /^[0-9]+$/) { + print $(i - 1) + found = 1 + exit + } + } + } + END { if (!found) print 0 } + ' <<<"$raw")" + updates_total=$((installable_count + updates_deferred)) updates_security="$(awk 'BEGIN{IGNORECASE=1} /^Inst / && /security/{count++} END{print count+0}' <<<"$raw")" elif [[ -n "$DNF_BIN" && -x "$DNF_BIN" ]]; then manager='dnf' @@ -73,5 +87,5 @@ if ((updates_total > 0 || updates_security > 0)) || [[ "$reboot_required" == 'ye status='warning' fi -printf 'status=%s\nmanager=%s\nupdates_total=%d\nupdates_security=%d\nreboot_required=%s\n' \ - "$status" "$manager" "$updates_total" "$updates_security" "$reboot_required" +printf 'status=%s\nmanager=%s\nupdates_total=%d\nupdates_security=%d\nupdates_deferred=%d\nreboot_required=%s\n' \ + "$status" "$manager" "$updates_total" "$updates_security" "$updates_deferred" "$reboot_required" diff --git a/tests/test-linux-modules.sh b/tests/test-linux-modules.sh index 3f23cd0..ce6a0a4 100755 --- a/tests/test-linux-modules.sh +++ b/tests/test-linux-modules.sh @@ -235,6 +235,26 @@ assert_contains "update posture reports reboot marker" "$update_output" 'reboot_ assert_contains "update posture reports warning state" "$update_output" 'status=warning' assert_not_contains "update posture omits package names" "$update_output" 'openssl' +cat >"$TMP/update bin/apt-get-deferred" <<'EOF' +#!/usr/bin/env bash +[[ "$*" == '-s upgrade' ]] || exit 2 +printf '%s\n' \ + 'The following packages have been kept back:' \ + ' linux-generic openssl' \ + '0 upgraded, 0 newly installed, 0 to remove and 2 not upgraded.' +EOF +chmod 700 "$TMP/update bin/apt-get-deferred" +if deferred_update_output="$(APT_GET_BIN="$TMP/update bin/apt-get-deferred" REBOOT_REQUIRED_FILE="$TMP/no-reboot" "$ROOT/scripts/update-posture.sh" 2>&1)"; then + deferred_update_status=0 +else + deferred_update_status=$? +fi +assert_eq "update posture collects deferred APT updates" "0" "$deferred_update_status" +assert_contains "update posture counts deferred APT updates in total" "$deferred_update_output" 'updates_total=2' +assert_contains "update posture reports deferred APT count" "$deferred_update_output" 'updates_deferred=2' +assert_contains "update posture warns for deferred APT updates" "$deferred_update_output" 'status=warning' +assert_not_contains "update posture omits deferred APT package names" "$deferred_update_output" 'linux-generic' + cat >"$TMP/update bin/dnf" <<'EOF' #!/usr/bin/env bash case "$*" in