Import the remaining utilities.
[utils/utils.git] / rpm-audit
... / ...
CommitLineData
1#!/bin/bash
2# ~ 2017-11-12
3
4WANTS_FILE=/usr/local/etc/rpm-wants
5
6set -e
7set -o pipefail
8# XXX: $BASH_COMMAND may be wrong with pipefail, but it's better than what we had before.
9trap 'echo "Unexpected error in command: $BASH_COMMAND"; if [ -n "$tmpdir" ] && [ -r "$tmpdir/dnf.log" ]; then cat "$tmpdir/dnf.log"; fi ' ERR
10
11if [ $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
15fi
16
17tmpdir="$(mktemp -d --tmpdir rpm-audit.XXXXXX)"
18trap 'rm -rf "$tmpdir"' EXIT
19
20uncommented_entries=($(<"$WANTS_FILE" sed -re 's/(^|[^[:space:]])[[:space:]]*#.*$/\1/' | egrep -v '^$'))
21wants=()
22modules=()
23for e in "${uncommented_entries[@]}"; do
24 case "$e" in
25 (module:*) modules+=("${e#module:}");;
26 (*) wants+=("$e");;
27 esac
28done
29
30function 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}
37function strip_zero_epoch {
38 sed -e 's/-0:/-/'
39}
40function indent {
41 sed -e 's/^/ /'
42}
43function 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
59pkg_arch_wants=()
60normal_wants=()
61for w in "${wants[@]}"; do
62 case "$w" in
63 (*.i686|*.x86_64)
64 pkg_arch_wants+=("$w");;
65 (*)
66 normal_wants+=("$w");;
67 esac
68done
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"
72dnf repoquery --userinstalled | strip_zero_epoch | filter_packages | sort >"$tmpdir/userinstalled"
73
74if ! 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
81fi
82
83# Check 2: fresh solution of wants vs. installed (should catch different choice of provider, packages needing update, orphans, and problems)
84
85platform_id="$(sed -nre 's,^PLATFORM_ID="(.*)"$,\1,p' /etc/os-release)"
86
87SANDBOX_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.
103rpmq -a | filter_packages | strip_zero_epoch | sort >"$tmpdir/installed"
104
105if ! 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
114fi
115
116echo "RPM package set audit passed!"