| 1 | #!/bin/bash |
| 2 | # ~ 2017-11-12 |
| 3 | |
| 4 | WANTS_FILE=/usr/local/etc/rpm-wants |
| 5 | |
| 6 | set -e |
| 7 | set -o pipefail |
| 8 | # XXX: $BASH_COMMAND may be wrong with pipefail, but it's better than what we had before. |
| 9 | trap 'echo "Unexpected error in command: $BASH_COMMAND"; if [ -n "$tmpdir" ] && [ -r "$tmpdir/dnf.log" ]; then cat "$tmpdir/dnf.log"; fi ' ERR |
| 10 | |
| 11 | if [ $EUID != 0 ]; then |
| 12 | echo >&2 'Unfortunately, rpm-audit can only run as root because it calls' |
| 13 | echo >&2 '"dnf install --assumeno".' |
| 14 | exit 1 |
| 15 | fi |
| 16 | |
| 17 | tmpdir="$(mktemp -d --tmpdir rpm-audit.XXXXXX)" |
| 18 | trap 'rm -rf "$tmpdir"' EXIT |
| 19 | |
| 20 | uncommented_entries=($(<"$WANTS_FILE" sed -re 's/(^|[^[:space:]])[[:space:]]*#.*$/\1/' | egrep -v '^$')) |
| 21 | wants=() |
| 22 | modules=() |
| 23 | for e in "${uncommented_entries[@]}"; do |
| 24 | case "$e" in |
| 25 | (module:*) modules+=("${e#module:}");; |
| 26 | (*) wants+=("$e");; |
| 27 | esac |
| 28 | done |
| 29 | |
| 30 | function rpmq { |
| 31 | # Set query format to match "dnf repoquery". |
| 32 | # |
| 33 | # Looks like rpm exits with the number of unmatched arguments, so we |
| 34 | # have to swallow any exit code. :( ~ Matt 2018-06-01 |
| 35 | rpm -q --qf '%{NAME}-%|EPOCH?{%{EPOCH}}:{0}|:%{VERSION}-%{RELEASE}.%{ARCH}\n' "$@" || true |
| 36 | } |
| 37 | function strip_zero_epoch { |
| 38 | sed -e 's/-0:/-/' |
| 39 | } |
| 40 | function indent { |
| 41 | sed -e 's/^/ /' |
| 42 | } |
| 43 | function filter_packages { |
| 44 | # Exempt kernel-{core,devel,modules{,-extra}} from audit because of multiple |
| 45 | # installed versions: not worth trying to do something better. |
| 46 | grep -Ev '^(gpg-pubkey|kernel-core|kernel-devel|kernel-modules|kernel-modules-extra)-[^-]+-[^-]+$' |
| 47 | } |
| 48 | |
| 49 | # Check 1: installed want providers vs. userinstalled |
| 50 | |
| 51 | # "dnf repoquery --installed --whatprovides" doesn't seem to accept multiple |
| 52 | # arguments, and running it once per entry would be unacceptably slow. See how |
| 53 | # long we can get away with this before looking for another solution. |
| 54 | # |
| 55 | # "rpm -q --whatprovides glibc.i686" does not work. Take all wants that look |
| 56 | # like they have an architecture and use "rpm -q" instead. This may fail if |
| 57 | # someone writes an actual provide of a package-like name with an architecture. |
| 58 | # ~ Matt 2017-11-16 |
| 59 | pkg_arch_wants=() |
| 60 | normal_wants=() |
| 61 | for w in "${wants[@]}"; do |
| 62 | case "$w" in |
| 63 | (*.i686|*.x86_64) |
| 64 | pkg_arch_wants+=("$w");; |
| 65 | (*) |
| 66 | normal_wants+=("$w");; |
| 67 | esac |
| 68 | done |
| 69 | # Since F29, kernel packages keep getting unmarked as userinstalled. Not |
| 70 | # investigating; just exclude them here. ~ Matt 2019-01-22 |
| 71 | { rpmq --whatprovides "${normal_wants[@]}" && rpmq "${pkg_arch_wants[@]}"; } | strip_zero_epoch | filter_packages | sort | uniq >"$tmpdir/wants-installed" |
| 72 | dnf repoquery --userinstalled | strip_zero_epoch | filter_packages | sort >"$tmpdir/userinstalled" |
| 73 | |
| 74 | if ! cmp -s "$tmpdir/wants-installed" "$tmpdir/userinstalled"; then |
| 75 | echo "Installed wants that are not marked as userinstalled:" |
| 76 | comm -2 -3 "$tmpdir/wants-installed" "$tmpdir/userinstalled" | indent |
| 77 | echo "Userinstalled packages that are not wants:" |
| 78 | comm -1 -3 "$tmpdir/wants-installed" "$tmpdir/userinstalled" | indent |
| 79 | echo "To correct, edit the wants file or use 'dnf mark {install|remove} PACKAGE_NAME'." |
| 80 | exit 1 |
| 81 | fi |
| 82 | |
| 83 | # Check 2: fresh solution of wants vs. installed (should catch different choice of provider, packages needing update, orphans, and problems) |
| 84 | |
| 85 | platform_id="$(sed -nre 's,^PLATFORM_ID="(.*)"$,\1,p' /etc/os-release)" |
| 86 | |
| 87 | SANDBOX_DNF=(dnf |
| 88 | --installroot="$tmpdir/installroot" |
| 89 | --setopt=cachedir=/var/cache/dnf # Share main cache to save time |
| 90 | --disableplugin=qubes-hooks # Qubes plugin takes unwanted actions when using an installroot |
| 91 | --releasever=/ --setopt=module_platform_id=$platform_id |
| 92 | ) |
| 93 | |
| 94 | "${SANDBOX_DNF[@]}" --assumeyes module enable "${modules[@]}" &>"$tmpdir/dnf.log" |
| 95 | |
| 96 | # --verbose for "Package ... will be installed" output. The human-readable list |
| 97 | # of packages to install is harder to scrape because of line wrapping. |
| 98 | # |
| 99 | # "dnf --assumeno" exits 1. I don't see an obvious way to distinguish this from |
| 100 | # real errors. However, real errors are likely to generate a diff anyway. |
| 101 | { "${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" |
| 102 | # Looks like "dnf repoquery --installed" doesn't catch extras. |
| 103 | rpmq -a | filter_packages | strip_zero_epoch | sort >"$tmpdir/installed" |
| 104 | |
| 105 | if ! cmp -s "$tmpdir/solved" "$tmpdir/installed"; then |
| 106 | echo "Packages in fresh solution that are not installed:" |
| 107 | comm -2 -3 "$tmpdir/solved" "$tmpdir/installed" | indent |
| 108 | echo "Installed packages that are not in fresh solution:" |
| 109 | comm -1 -3 "$tmpdir/solved" "$tmpdir/installed" | indent |
| 110 | echo "To correct, install or remove packages to match the canonical solution," |
| 111 | echo "or override the default choice of providers by adding the desired" |
| 112 | echo "providers to the wants file." |
| 113 | exit 1 |
| 114 | fi |
| 115 | |
| 116 | echo "RPM package set audit passed!" |