Import the remaining utilities.
[utils/utils.git] / rpm-audit
diff --git a/rpm-audit b/rpm-audit
new file mode 100755 (executable)
index 0000000..ca7431a
--- /dev/null
+++ b/rpm-audit
@@ -0,0 +1,116 @@
+#!/bin/bash
+# ~ 2017-11-12
+
+WANTS_FILE=/usr/local/etc/rpm-wants
+
+set -e
+set -o pipefail
+# XXX: $BASH_COMMAND may be wrong with pipefail, but it's better than what we had before.
+trap 'echo "Unexpected error in command: $BASH_COMMAND"; if [ -n "$tmpdir" ] && [ -r "$tmpdir/dnf.log" ]; then cat "$tmpdir/dnf.log"; fi ' ERR
+
+if [ $EUID != 0 ]; then
+       echo >&2 'Unfortunately, rpm-audit can only run as root because it calls'
+       echo >&2 '"dnf install --assumeno".'
+       exit 1
+fi
+
+tmpdir="$(mktemp -d --tmpdir rpm-audit.XXXXXX)"
+trap 'rm -rf "$tmpdir"' EXIT 
+
+uncommented_entries=($(<"$WANTS_FILE" sed -re 's/(^|[^[:space:]])[[:space:]]*#.*$/\1/' | egrep -v '^$'))
+wants=()
+modules=()
+for e in "${uncommented_entries[@]}"; do
+       case "$e" in
+       (module:*) modules+=("${e#module:}");;
+       (*) wants+=("$e");;
+       esac
+done
+
+function rpmq {
+       # Set query format to match "dnf repoquery".
+       #
+       # Looks like rpm exits with the number of unmatched arguments, so we
+       # have to swallow any exit code. :( ~ Matt 2018-06-01
+       rpm -q --qf '%{NAME}-%|EPOCH?{%{EPOCH}}:{0}|:%{VERSION}-%{RELEASE}.%{ARCH}\n' "$@" || true
+}
+function strip_zero_epoch {
+       sed -e 's/-0:/-/'
+}
+function indent {
+       sed -e 's/^/  /'
+}
+function filter_packages {
+       # Exempt kernel-{core,devel,modules{,-extra}} from audit because of multiple
+       # installed versions: not worth trying to do something better.
+       grep -Ev '^(gpg-pubkey|kernel-core|kernel-devel|kernel-modules|kernel-modules-extra)-[^-]+-[^-]+$'
+}
+
+# Check 1: installed want providers vs. userinstalled
+
+# "dnf repoquery --installed --whatprovides" doesn't seem to accept multiple
+# arguments, and running it once per entry would be unacceptably slow.  See how
+# long we can get away with this before looking for another solution.
+#
+# "rpm -q --whatprovides glibc.i686" does not work.  Take all wants that look
+# like they have an architecture and use "rpm -q" instead.  This may fail if
+# someone writes an actual provide of a package-like name with an architecture.
+# ~ Matt 2017-11-16
+pkg_arch_wants=()
+normal_wants=()
+for w in "${wants[@]}"; do
+       case "$w" in
+       (*.i686|*.x86_64)
+               pkg_arch_wants+=("$w");;
+       (*)
+               normal_wants+=("$w");;
+       esac
+done
+# Since F29, kernel packages keep getting unmarked as userinstalled.  Not
+# investigating; just exclude them here. ~ Matt 2019-01-22
+{ rpmq --whatprovides "${normal_wants[@]}" && rpmq "${pkg_arch_wants[@]}"; } | strip_zero_epoch | filter_packages | sort | uniq >"$tmpdir/wants-installed"
+dnf repoquery --userinstalled | strip_zero_epoch | filter_packages | sort >"$tmpdir/userinstalled"
+
+if ! cmp -s "$tmpdir/wants-installed" "$tmpdir/userinstalled"; then
+       echo "Installed wants that are not marked as userinstalled:"
+       comm -2 -3 "$tmpdir/wants-installed" "$tmpdir/userinstalled" | indent
+       echo "Userinstalled packages that are not wants:"
+       comm -1 -3 "$tmpdir/wants-installed" "$tmpdir/userinstalled" | indent
+       echo "To correct, edit the wants file or use 'dnf mark {install|remove} PACKAGE_NAME'."
+       exit 1
+fi
+
+# Check 2: fresh solution of wants vs. installed (should catch different choice of provider, packages needing update, orphans, and problems)
+
+platform_id="$(sed -nre 's,^PLATFORM_ID="(.*)"$,\1,p' /etc/os-release)"
+
+SANDBOX_DNF=(dnf
+       --installroot="$tmpdir/installroot"
+       --setopt=cachedir=/var/cache/dnf  # Share main cache to save time
+       --disableplugin=qubes-hooks  # Qubes plugin takes unwanted actions when using an installroot
+       --releasever=/ --setopt=module_platform_id=$platform_id
+)
+
+"${SANDBOX_DNF[@]}" --assumeyes module enable "${modules[@]}" &>"$tmpdir/dnf.log"
+
+# --verbose for "Package ... will be installed" output.  The human-readable list
+# of packages to install is harder to scrape because of line wrapping.
+#
+# "dnf --assumeno" exits 1.  I don't see an obvious way to distinguish this from
+# real errors.  However, real errors are likely to generate a diff anyway.
+{ "${SANDBOX_DNF[@]}" --verbose --assumeno install --best "${wants[@]}" 2>&1 || [ $? == 1 ]; } | tee -a "$tmpdir/dnf.log" | sed -nre 's/^---> Package ([^ ]+)\.([^ .]+) ([^ ]+) will be installed$/\1-\3.\2/p' | filter_packages | sort >"$tmpdir/solved"
+# Looks like "dnf repoquery --installed" doesn't catch extras.
+rpmq -a | filter_packages | strip_zero_epoch | sort >"$tmpdir/installed"
+
+if ! cmp -s "$tmpdir/solved" "$tmpdir/installed"; then
+       echo "Packages in fresh solution that are not installed:"
+       comm -2 -3 "$tmpdir/solved" "$tmpdir/installed" | indent
+       echo "Installed packages that are not in fresh solution:"
+       comm -1 -3 "$tmpdir/solved" "$tmpdir/installed" | indent
+       echo "To correct, install or remove packages to match the canonical solution,"
+       echo "or override the default choice of providers by adding the desired"
+       echo "providers to the wants file."
+       exit 1
+fi
+
+echo "RPM package set audit passed!"