├── sysclean.ignore ├── Makefile ├── README.md ├── sysclean.8 └── sysclean.pl /sysclean.ignore: -------------------------------------------------------------------------------- 1 | # sysclean.ignore example file 2 | # 3 | # each line is a pattern which may use any of the special characters documented 4 | # in File::Glob(3p). 5 | # 6 | /data 7 | /etc/*.local 8 | /upgrade.site 9 | 10 | # ignore files listed in changelist(5) 11 | @include "/etc/changelist" 12 | 13 | # ignore local system user/group 14 | @user _service:999:_service::/var/empty:/sbin/nologin 15 | @group _service:999 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # $OpenBSD$ 2 | 3 | MAN= sysclean.8 4 | 5 | SCRIPT= sysclean.pl 6 | 7 | BINDIR?= /usr/local/sbin 8 | MANDIR?= /usr/local/man/man 9 | 10 | realinstall: 11 | ${INSTALL} ${INSTALL_COPY} -o ${BINOWN} -g ${BINGRP} -m ${BINMODE} \ 12 | ${.CURDIR}/${SCRIPT} ${DESTDIR}${BINDIR}/sysclean 13 | 14 | README.md: sysclean.8 15 | mandoc -T markdown sysclean.8 \ 16 | | sed -e 's/ / /g' \ 17 | -e 's/<//g' \ 19 | -e 's/\\\[/[/g' \ 20 | >$@ 21 | 22 | regress: run-regress-perl-syntax \ 23 | run-regress-man-lint \ 24 | run-regress-man-readme \ 25 | run-regress-man-date 26 | 27 | # check perl syntax 28 | run-regress-perl-syntax: 29 | @echo TEST: perl syntax 30 | @perl -c sysclean.pl 31 | 32 | # check man page 33 | run-regress-man-lint: 34 | @echo TEST: man page lint 35 | @mandoc -T lint -W style sysclean.8 36 | 37 | # ensure README.md and man page are in sync 38 | run-regress-man-readme: 39 | @echo TEST: README.md sync 40 | @mv README.md README.md.orig 41 | @${MAKE} README.md 42 | @mv README.md README.md.new 43 | @mv README.md.orig README.md 44 | diff -q README.md README.md.new 45 | @rm README.md.new 46 | 47 | # ensure .Dd date inside man page is the right date 48 | run-regress-man-date: 49 | @echo TEST: man page date 50 | @if [ -d .git ]; then \ 51 | grep -qF -- \ 52 | "$$(date -r $$(git log -1 --format=%ct sysclean.8) \ 53 | +'.Dd %B %d, %Y')" \ 54 | sysclean.8 ; \ 55 | elif [ -d .got ]; then \ 56 | grep -qF -- \ 57 | "$$(got log -l 1 sysclean.8 \ 58 | | sed -ne 's/^date: //p' \ 59 | | xargs -0 date -j -f '%a %b %d %T %Y %Z' \ 60 | +'.Dd %B %d, %Y')" \ 61 | sysclean.8 ; \ 62 | else \ 63 | echo "unchecked" ; \ 64 | fi 65 | 66 | .if !defined(VERSION) 67 | release: 68 | @echo "error: please define VERSION"; false 69 | 70 | .else 71 | release: sysclean-${VERSION}.tar.gz 72 | 73 | DISTRIBUTED_FILES = \ 74 | Makefile \ 75 | README.md \ 76 | sysclean.ignore \ 77 | ${MAN} \ 78 | ${SCRIPT} 79 | 80 | sysclean-${VERSION}.tar.gz: ${DISTRIBUTED_FILES} 81 | chmod a+rX ${DISTRIBUTED_FILES} 82 | pax -w -zf "$@" -s ',^,sysclean-${VERSION}/,' ${DISTRIBUTED_FILES} 83 | 84 | .endif 85 | 86 | .include 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SYSCLEAN(8) - System Manager's Manual 2 | 3 | # NAME 4 | 5 | **sysclean** - list obsolete elements between OpenBSD upgrades 6 | 7 | # SYNOPSIS 8 | 9 | **sysclean** 10 | [**-a** | **-p**] 11 | [**-i**] 12 | 13 | # DESCRIPTION 14 | 15 | **sysclean** 16 | is a 17 | perl(1) 18 | script designed to help remove obsolete files, users and groups, between 19 | OpenBSD 20 | upgrades. 21 | 22 | **sysclean** 23 | compares a reference installation against the currently installed elements, 24 | taking files from both the base system and packages into account. 25 | 26 | **sysclean** 27 | is a read-only tool. 28 | It does not remove anything on the system. 29 | 30 | By default, 31 | **sysclean** 32 | lists obsolete filenames, users and groups on the system. 33 | It excludes any used dynamic libraries. 34 | It will report base libraries with versions newer than what's expected. 35 | 36 | The options are as follows: 37 | 38 | **-a** 39 | 40 | > All files mode. 41 | > **sysclean** 42 | > will not exclude filenames used by installed packages from output. 43 | 44 | **-i** 45 | 46 | > With ignored. 47 | > **sysclean** 48 | > will include filenames that are ignored by default, using 49 | > */etc/sysclean.ignore*. 50 | 51 | **-p** 52 | 53 | > Package mode. 54 | > **sysclean** 55 | > will output package names that are using obsolete files. 56 | 57 | # ENVIRONMENT 58 | 59 | `PKG_DBDIR` 60 | 61 | > The standard package database directory, 62 | > */var/db/pkg*, 63 | > can be overridden by specifying an alternative directory in the 64 | > `PKG_DBDIR` 65 | > environment variable. 66 | 67 | # FILES 68 | 69 | */etc/sysclean.ignore* 70 | 71 | > Files to ignore, one per line, with absolute pathnames. 72 | 73 | > A line starting with 74 | > '@user' 75 | > or 76 | > '@group' 77 | > will be interpreted as user or group to ignore. 78 | 79 | > Shell globbing is supported in pathnames; see 80 | > File::Glob(3p). 81 | > If the pattern matches a directory, 82 | > **sysclean** 83 | > will not inspect it or any files contained within. 84 | > For compatibility with the 85 | > changelist(5) 86 | > file format, the character 87 | > '+' 88 | > is skipped at the beginning of a line. 89 | 90 | > */etc/changelist* 91 | > is implictly included. 92 | 93 | # EXAMPLES 94 | 95 | Obtain a list of outdated files (without libraries used by packages): 96 | 97 | # sysclean 98 | /usr/lib/libc.so.83.0 99 | 100 | Obtain a list of old libraries and the package using them: 101 | 102 | # sysclean -p 103 | /usr/lib/libc.so.84.1 git-2.7.0 104 | /usr/lib/libc.so.84.1 gmake-4.1p0 105 | 106 | Obtain a list of all outdated files (including used libraries): 107 | 108 | # sysclean -a 109 | /usr/lib/libc.so.83.0 110 | /usr/lib/libc.so.84.1 111 | 112 | Obtain a list of users and groups that can safely be removed 113 | (from ports or base): 114 | 115 | # sysclean 116 | @user _mpd:560:_mpd::/var/spool/mpd:/sbin/nologin 117 | @group _mpd:560 118 | 119 | # SEE ALSO 120 | 121 | pkg\_info(1), 122 | sysmerge(8) 123 | 124 | # HISTORY 125 | 126 | The first version of 127 | **sysclean** 128 | was written as 129 | ksh(1) 130 | script in 2015, and rewritten using 131 | perl(1) 132 | in 2016. 133 | 134 | # AUTHORS 135 | 136 | **sysclean** 137 | was written by 138 | Sebastien Marie <[semarie@kapouay.eu.org](mailto:semarie@kapouay.eu.org)>. 139 | 140 | OpenBSD 7.8 - March 10, 2024 - SYSCLEAN(8) 141 | -------------------------------------------------------------------------------- /sysclean.8: -------------------------------------------------------------------------------- 1 | .\" $OpenBSD$ 2 | .\" 3 | .\" Copyright (c) 2016-2023 Sebastien Marie 4 | .\" 5 | .\" Permission to use, copy, modify, and distribute this software for any 6 | .\" purpose with or without fee is hereby granted, provided that the above 7 | .\" copyright notice and this permission notice appear in all copies. 8 | .\" 9 | .\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | .\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | .\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | .\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | .\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | .\" 17 | .Dd March 10, 2024 18 | .Dt SYSCLEAN 8 19 | .Os 20 | .Sh NAME 21 | .Nm sysclean 22 | .Nd list obsolete elements between OpenBSD upgrades 23 | .Sh SYNOPSIS 24 | .Nm 25 | .Op Fl a | p 26 | .Op Fl i 27 | .Sh DESCRIPTION 28 | .Nm 29 | is a 30 | .Xr perl 1 31 | script designed to help remove obsolete files, users and groups, between 32 | .Ox 33 | upgrades. 34 | .Pp 35 | .Nm 36 | compares a reference installation against the currently installed elements, 37 | taking files from both the base system and packages into account. 38 | .Pp 39 | .Nm 40 | is a read-only tool. 41 | It does not remove anything on the system. 42 | .Pp 43 | By default, 44 | .Nm 45 | lists obsolete filenames, users and groups on the system. 46 | It excludes any used dynamic libraries. 47 | It will report base libraries with versions newer than what's expected. 48 | .Pp 49 | The options are as follows: 50 | .Bl -tag -width Ds 51 | .It Fl a 52 | All files mode. 53 | .Nm 54 | will not exclude filenames used by installed packages from output. 55 | .It Fl i 56 | With ignored. 57 | .Nm 58 | will include filenames that are ignored by default, using 59 | .Pa /etc/sysclean.ignore . 60 | .It Fl p 61 | Package mode. 62 | .Nm 63 | will output package names that are using obsolete files. 64 | .El 65 | .Sh ENVIRONMENT 66 | .Bl -tag -width "PKG_DBDIR" 67 | .It Ev PKG_DBDIR 68 | The standard package database directory, 69 | .Pa /var/db/pkg , 70 | can be overridden by specifying an alternative directory in the 71 | .Ev PKG_DBDIR 72 | environment variable. 73 | .El 74 | .Sh FILES 75 | .Bl -tag -width "/etc/sysclean.ignore" 76 | .It Pa /etc/sysclean.ignore 77 | Files to ignore, one per line, with absolute pathnames. 78 | .Pp 79 | A line starting with 80 | .Sq @user 81 | or 82 | .Sq @group 83 | will be interpreted as user or group to ignore. 84 | .Pp 85 | Shell globbing is supported in pathnames; see 86 | .Xr File::Glob 3p . 87 | If the pattern matches a directory, 88 | .Nm 89 | will not inspect it or any files contained within. 90 | For compatibility with the 91 | .Xr changelist 5 92 | file format, the character 93 | .Sq + 94 | is skipped at the beginning of a line. 95 | .Pp 96 | .Pa /etc/changelist 97 | is implictly included. 98 | .El 99 | .Sh EXAMPLES 100 | Obtain a list of outdated files (without libraries used by packages): 101 | .Bd -literal -offset indent 102 | # sysclean 103 | /usr/lib/libc.so.83.0 104 | .Ed 105 | .Pp 106 | Obtain a list of old libraries and the package using them: 107 | .Bd -literal -offset indent 108 | # sysclean -p 109 | /usr/lib/libc.so.84.1 git-2.7.0 110 | /usr/lib/libc.so.84.1 gmake-4.1p0 111 | .Ed 112 | .Pp 113 | Obtain a list of all outdated files (including used libraries): 114 | .Bd -literal -offset indent 115 | # sysclean -a 116 | /usr/lib/libc.so.83.0 117 | /usr/lib/libc.so.84.1 118 | .Ed 119 | .Pp 120 | Obtain a list of users and groups that can safely be removed 121 | (from ports or base): 122 | .Bd -literal -offset indent 123 | # sysclean 124 | @user _mpd:560:_mpd::/var/spool/mpd:/sbin/nologin 125 | @group _mpd:560 126 | .Ed 127 | .Sh SEE ALSO 128 | .Xr pkg_info 1 , 129 | .Xr sysmerge 8 130 | .Sh HISTORY 131 | The first version of 132 | .Nm 133 | was written as 134 | .Xr ksh 1 135 | script in 2015, and rewritten using 136 | .Xr perl 1 137 | in 2016. 138 | .Sh AUTHORS 139 | .An -nosplit 140 | .Nm 141 | was written by 142 | .An Sebastien Marie Aq Mt semarie@kapouay.eu.org . 143 | -------------------------------------------------------------------------------- /sysclean.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # 3 | # $OpenBSD$ 4 | # 5 | # Copyright (c) 2016-2025 Sebastien Marie 6 | # 7 | # Permission to use, copy, modify, and distribute this software for any 8 | # purpose with or without fee is hereby granted, provided that the above 9 | # copyright notice and this permission notice appear in all copies. 10 | # 11 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 14 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 16 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 17 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | # 19 | 20 | use v5.36; 21 | 22 | package sysclean; 23 | 24 | # return subclass according to options 25 | sub subclass($self, $options) 26 | { 27 | return 'sysclean::allfiles' if (defined $$options{a}); 28 | return 'sysclean::packages' if (defined $$options{p}); 29 | return 'sysclean::files'; 30 | } 31 | 32 | # choose class for mode, depending on %options 33 | sub create($base, $options) 34 | { 35 | my $with_ignored = !defined $$options{i}; 36 | my $mode_count = 0; 37 | 38 | $mode_count++ if (defined $$options{a}); 39 | $mode_count++ if (defined $$options{p}); 40 | sysclean->usage if ($mode_count > 1); 41 | 42 | return $base->subclass($options)->new($with_ignored); 43 | } 44 | 45 | # constructor 46 | sub new($class, $with_ignored) 47 | { 48 | my $self = bless {}, $class; 49 | 50 | $self->init_ignored; 51 | $self->init; 52 | if ($with_ignored) { 53 | $self->add_user_ignored("/etc/changelist"); 54 | $self->add_user_ignored("/etc/sysclean.ignore"); 55 | $self->{expected}{'/etc/sysclean.ignore'} = 1; 56 | } 57 | 58 | return $self; 59 | } 60 | 61 | # print usage and exit 62 | sub usage($self) 63 | { 64 | print "usage: $0 [ -a | -p ] [-i]\n"; 65 | exit 1 66 | } 67 | 68 | # print error and exit 69 | sub err($self, $exitcode, @rest) 70 | { 71 | print STDERR "$0: error: @rest\n"; 72 | 73 | exit $exitcode; 74 | } 75 | 76 | # print warning 77 | sub warn($self, @rest) 78 | { 79 | print STDERR "$0: warn: @rest\n"; 80 | } 81 | 82 | # initial list of ignored files and directories 83 | sub init_ignored($self) 84 | { 85 | $self->{ignored} = { 86 | '/home' => 1, 87 | '/root' => 1, 88 | '/tmp' => 1, 89 | '/usr/local' => 1, # remove ? 90 | '/usr/obj' => 1, 91 | '/usr/ports' => 1, 92 | '/usr/share/relink/kernel' => 1, 93 | '/usr/src' => 1, 94 | '/usr/xenocara' => 1, 95 | '/usr/xobj' => 1, 96 | '/var/backups' => 1, 97 | '/var/cache' => 1, 98 | '/var/cron' => 1, 99 | '/var/db' => 1, 100 | '/var/log' => 1, 101 | '/var/mail' => 1, 102 | '/var/run' => 1, 103 | '/var/spool/smtpd' => 1, 104 | '/var/sysmerge' => 1, 105 | '/var/syspatch' => 1, 106 | '/var/www/htdocs' => 1, 107 | '/var/www/logs' => 1, 108 | '/var/www/run' => 1, 109 | '/var/www/tmp' => 1, 110 | }; 111 | 112 | # additionnal ignored files, using pattern 113 | foreach my $filename () { 114 | $self->{ignored}{$filename} = 1; 115 | } 116 | } 117 | 118 | sub init($self) 119 | { 120 | use OpenBSD::PackageInfo; 121 | use OpenBSD::Pledge; 122 | use OpenBSD::Unveil; 123 | 124 | lock_db(1); 125 | 126 | unveil('/', 'r'); 127 | unveil('/dev/MAKEDEV', 'rx'); 128 | unveil('/usr/bin/locate', 'rx'); 129 | unveil('/usr/sbin/rcctl', 'rx'); 130 | 131 | pledge('rpath getpw proc exec') || $self->err(1, "pledge"); 132 | $self->add_expected_base; 133 | $self->add_expected_dev; 134 | $self->add_expected_rcctl; 135 | 136 | pledge('rpath getpw') || $self->err(1, "pledge"); 137 | $self->add_expected_users; 138 | $self->add_expected_ports_info; 139 | } 140 | 141 | # add expected files from base. call `add_expected_base_one' overriden method. 142 | # WARNING: `expected' attribute is overrided 143 | sub add_expected_base($self) 144 | { 145 | # simple files expected (and not in locate databases) 146 | $self->{expected} = { 147 | '/' => 1, 148 | '/boot' => 1, 149 | '/ofwboot' => 1, 150 | '/bsd' => 1, 151 | '/bsd.booted' => 1, 152 | '/bsd.mp' => 1, 153 | '/bsd.rd' => 1, 154 | '/bsd.sp' => 1, 155 | '/obsd' => 1, 156 | '/dev/rootdisk' => 1, 157 | '/dev/rrootdisk' => 1, 158 | '/etc/acme/letsencrypt-privkey.pem' => 1, 159 | '/etc/acme/letsencrypt-staging-privkey.pem' => 1, 160 | '/etc/fstab' => 1, 161 | '/etc/hosts' => 1, 162 | '/etc/installurl' => 1, 163 | '/etc/iked/local.pub' => 1, 164 | '/etc/iked/private/local.key' => 1, 165 | '/etc/isakmpd/local.pub' => 1, 166 | '/etc/isakmpd/private/local.key' => 1, 167 | '/etc/kbdtype' => 1, 168 | '/etc/ssh/ssh_host_rsa_key' => 1, 169 | '/etc/ssh/ssh_host_rsa_key.pub' => 1, 170 | '/etc/ssh/ssh_host_ecdsa_key' => 1, 171 | '/etc/ssh/ssh_host_ecdsa_key.pub' => 1, 172 | '/etc/ssh/ssh_host_ed25519_key' => 1, 173 | '/etc/ssh/ssh_host_ed25519_key.pub' => 1, 174 | '/etc/myname' => 1, 175 | '/etc/random.seed' => 1, 176 | '/usr/libexec/ld.so.save' => 1, 177 | '/var/account/acct' => 1, 178 | '/var/account/acct.0' => 1, 179 | '/var/account/acct.1' => 1, 180 | '/var/account/acct.2' => 1, 181 | '/var/account/acct.3' => 1, 182 | '/var/account/savacct' => 1, 183 | '/var/account/usracct' => 1, 184 | }; 185 | 186 | # additionnal expected files, using pattern 187 | foreach my $filename () { 188 | $self->{expected}{$filename} = 1; 189 | } 190 | 191 | # expected files, from locate databases 192 | use OpenBSD::Paths; 193 | 194 | open(my $cmd, '-|', 'locate', 195 | '-d', OpenBSD::Paths->srclocatedb, 196 | '-d', OpenBSD::Paths->xlocatedb, 197 | '*') || $self->err(1, "can't read base locatedb"); 198 | while (<$cmd>) { 199 | chomp; 200 | my ($set, $filename) = split(':', $_, 2); 201 | $self->add_expected_base_one($filename); 202 | } 203 | close($cmd); 204 | } 205 | 206 | # default method for manipulated one expected filename of base. 207 | sub add_expected_base_one($self, $filename) 208 | { 209 | $self->{expected}{$filename} = 1; 210 | } 211 | 212 | # add expected files from /dev 213 | sub add_expected_dev($self) 214 | { 215 | # set 'eo=echo' in env, to run MAKEDEV(8) in echo mode. 216 | $ENV{'eo'} = 'echo'; 217 | 218 | chdir('/dev') || 219 | $self->err(1, "can't chdir to /dev"); 220 | open(my $dev, '-|', './MAKEDEV', 'all') || 221 | $self->err(1, "can't execute /dev/MAKEDEV"); 222 | 223 | while (<$dev>) { 224 | chomp; 225 | 226 | # simplify command separator 227 | s/\s*(;|&&|\|\|)\s*/;/g; 228 | 229 | # iterate on commands 230 | foreach my $commandline (split(/;/)) { 231 | # split $commandline in words 232 | my @args = split(/\s/, $commandline); 233 | my $cmd = shift(@args); 234 | 235 | # skip commands 236 | next if ($cmd eq 'rm'); 237 | next if ($cmd eq 'chown'); 238 | next if ($cmd eq 'chgrp'); 239 | next if ($cmd eq 'chmod'); 240 | next if ($cmd eq '['); 241 | 242 | if ($cmd eq 'mkdir') { 243 | shift(@args); # skip -p 244 | 245 | foreach my $dir (@args) { 246 | $self->{expected}{"/dev/$dir"} = 1; 247 | } 248 | 249 | } elsif ($cmd eq 'ln') { 250 | my ($option, $src, $dest) = @args; 251 | $self->{expected}{"/dev/$dest"} = 1; 252 | 253 | } elsif ($cmd eq 'mknod') { 254 | foreach my $arg (@args) { 255 | if ($arg eq '-m') { 256 | shift(@args); # mode 257 | next; 258 | } 259 | 260 | $self->{expected}{"/dev/$arg"} = 1; 261 | shift(@args); # b|c 262 | shift(@args); # major 263 | shift(@args); # minor 264 | } 265 | 266 | } else { 267 | $self->err(1, "unexpected command '$cmd' in MAKEDEV output"); 268 | } 269 | } 270 | } 271 | close($dev); 272 | } 273 | 274 | # add expected files from enabled daemons and services. 275 | sub add_expected_rcctl($self) 276 | { 277 | open(my $cmd, '-|', 'rcctl', 'ls', 'on') || 278 | $self->err(1, "can't read enabled daemons and services"); 279 | while (<$cmd>) { 280 | chomp; 281 | if ('apmd' eq $_) { 282 | $self->{expected}{'/etc/apm'} = 1; 283 | $self->{expected}{'/etc/apm/suspend'} = 1; 284 | $self->{expected}{'/etc/apm/hibernate'} = 1; 285 | $self->{expected}{'/etc/apm/standby'} = 1; 286 | $self->{expected}{'/etc/apm/resume'} = 1; 287 | $self->{expected}{'/etc/apm/powerup'} = 1; 288 | $self->{expected}{'/etc/apm/powerdown'} = 1; 289 | 290 | } elsif ('dhcp6leased' eq $_) { 291 | $self->{expected}{'/dev/dhcp6leased.lock'} = 1; 292 | $self->{expected}{'/dev/dhcp6leased.sock'} = 1; 293 | 294 | } elsif ('dhcpleased' eq $_) { 295 | $self->{expected}{'/dev/dhcpleased.lock'} = 1; 296 | $self->{expected}{'/dev/dhcpleased.sock'} = 1; 297 | 298 | } elsif ('hotplugd' eq $_) { 299 | $self->{expected}{'/etc/hotplug/attach'} = 1; 300 | $self->{expected}{'/etc/hotplug/detach'} = 1; 301 | 302 | } elsif ('iked' eq $_) { 303 | $self->{ignored}{'/etc/iked/pubkeys'} = 1; 304 | 305 | } elsif ('isakmpd' eq $_) { 306 | $self->{ignored}{'/etc/isakmpd/pubkeys'} = 1; 307 | 308 | } elsif ('lpd' eq $_) { 309 | $self->{expected}{'/etc/printcap'} = 1; 310 | $self->{ignored}{'/var/spool/output/lpd'} = 1; 311 | 312 | } elsif ('nsd' eq $_) { 313 | $self->{ignored}{'/var/nsd/run'} = 1; 314 | $self->{ignored}{'/var/nsd/zones'} = 1; 315 | 316 | } elsif ('resolvd' eq $_) { 317 | $self->{expected}{'/dev/resolvd.lock'} = 1; 318 | 319 | } elsif ('slaacd' eq $_) { 320 | $self->{expected}{'/dev/slaacd.lock'} = 1; 321 | $self->{expected}{'/dev/slaacd.sock'} = 1; 322 | 323 | } elsif ('syslogd' eq $_) { 324 | $self->{expected}{'/dev/log'} = 1; 325 | 326 | } elsif ('smtpd' eq $_) { 327 | $self->{expected}{'/etc/mail/aliases.db'} = 1; 328 | 329 | } elsif ('unbound' eq $_) { 330 | $self->{expected}{'/var/unbound/db/root.key'} = 1; 331 | 332 | } elsif ('unwind' eq $_) { 333 | $self->{expected}{'/dev/unwind.sock'} = 1; 334 | $self->{expected}{'/etc/unwind/trustanchor/root.key'} = 1; 335 | 336 | } elsif ('xenodm' eq $_) { 337 | $self->{ignored}{'/etc/X11/xenodm/authdir'} = 1; 338 | } 339 | } 340 | close($cmd); 341 | } 342 | 343 | # add expected information for users/groups. 344 | sub add_expected_users($self) 345 | { 346 | use Archive::Tar; 347 | 348 | my $tar = Archive::Tar->new(); 349 | $tar->read( 350 | '/var/sysmerge/etc.tgz', { 351 | limit => 2, 352 | filter => './etc/{master.passwd,group}', 353 | }) || $self->err(1, "can't read /var/sysmerge/etc.tgz"); 354 | 355 | # add groups (and keep track of gid -> gname association) 356 | my %groups = (); 357 | my $group = $tar->get_content('./etc/group'); 358 | foreach my $entry (split(/\n/, $group)) { 359 | my ($name, $passwd, $gid, $members) = split(/:/, $entry); 360 | my $group = join(':', ($name, $gid)); 361 | 362 | $groups{$gid} = $name; 363 | $self->{groups}{$group} = 1; 364 | } 365 | 366 | # add users 367 | my $passwd = $tar->get_content('./etc/master.passwd'); 368 | foreach my $entry (split(/\n/, $passwd)) { 369 | my ($name, $passwd, $uid, $gid, $class, $change, $expire, 370 | $gecos, $home, $shell) = split(/:/, $entry); 371 | my $gname = $groups{$gid} || 372 | $self->err(1, "unknown gid $gid in passwd '$name' entry"); 373 | 374 | my $user = join(':', ($name, $uid, $gname, $class, $home, $shell)); 375 | $self->{users}{$user} = 1; 376 | 377 | $self->{user_fields}{$name} = { 378 | gid => $gid, 379 | group => $gname, 380 | class => $class, 381 | home => $home, 382 | shell => $shell, 383 | }; 384 | 385 | my $short = join(':', ($name, $uid)); 386 | $self->{users}{$short} = 1; 387 | } 388 | 389 | } 390 | 391 | # add expected information from ports. the method will call `plist_reader' 392 | # overriden method. 393 | sub add_expected_ports_info($self) 394 | { 395 | use OpenBSD::PackageInfo; 396 | use OpenBSD::PackingList; 397 | 398 | for my $pkgname (installed_packages()) { 399 | my $plist = OpenBSD::PackingList->from_installation($pkgname, 400 | $self->plist_reader); 401 | $plist->walk_sysclean($pkgname, $self); 402 | } 403 | } 404 | 405 | # default plist_reader sub. could be overrided. 406 | sub plist_reader($self) 407 | { 408 | return sub ($fh, $cont) { 409 | while (<$fh>) { 410 | next unless m/^\@(?:cwd|name|info|fontdir|man|mandir|file|lib|shell|so|static-lib|extra|sample|bin|rcscript|wantlib|newuser|newgroup)\b/o || !m/^\@/o; 411 | &$cont($_); 412 | }; 413 | } 414 | } 415 | 416 | # add user-defined `ignored' elements 417 | sub add_user_ignored($self, $conffile) 418 | { 419 | open(my $fh, "<", $conffile) || return 0; 420 | while (<$fh>) { 421 | chomp; 422 | 423 | # strip starting '+' (compat with changelist(5) format) 424 | s/^\+//; 425 | 426 | # strip comments 427 | s/\s*#.*$//; 428 | next if (m/^$/o); 429 | 430 | if (m/^\@include\s+"(.*)"\s*$/) { 431 | # include another conffile 432 | $self->add_user_ignored($1) || 433 | $self->warn("open \"$1\": $!"); 434 | 435 | } elsif (m|^/|) { 436 | # absolute filename 437 | foreach my $filename (glob qq("$_")) { 438 | $self->{ignored}{$filename} = 1; 439 | } 440 | 441 | } elsif (s/^\@user\s+//) { 442 | # user entry 443 | $self->{ignored_users}{$_} = 1; 444 | 445 | } elsif (s/^\@group\s+//) { 446 | # group entry 447 | $self->{ignored_groups}{$_} = 1; 448 | 449 | } else { 450 | $self->err(1, "$conffile: invalid entry: $_"); 451 | } 452 | } 453 | close($fh); 454 | return 1; 455 | } 456 | 457 | # walk the filesystem. the method will call `find_sub' overriden method. 458 | sub walk_filesystem($self) 459 | { 460 | use File::Find; 461 | 462 | find({ wanted => 463 | sub { 464 | if (exists($self->{ignored}{$_})) { 465 | # skip ignored files 466 | $File::Find::prune = 1; 467 | 468 | } elsif (! exists($self->{expected}{$_})) { 469 | # not expected file 470 | 471 | if ( -d $_ ) { 472 | # don't descend in unknown directory 473 | $File::Find::prune = 1; 474 | } 475 | 476 | # find_sub is defined per mode 477 | $self->find_sub($_); 478 | } 479 | }, follow => 0, no_chdir => 1, }, '/'); 480 | } 481 | 482 | # walk all users in the system. 483 | sub walk_users($self) 484 | { 485 | # walk users 486 | while (my ($name, $passwd, $uid, $gid, $quota, $class, $gecos, $home, 487 | $shell, $expire) = getpwent()) { 488 | 489 | # only system users 490 | next if ($uid >= 1000); 491 | 492 | my $gname = getgrgid($gid) || $gid; 493 | my $user = join(':', ($name, $uid, $gname, $class, $home, $shell)); 494 | my $user_gid = join(':', ($name, $uid, $gid, $class, $home, $shell)); 495 | 496 | # check both $user and $user_gid, 497 | # as @newuser in ports could be both. 498 | if (!exists($self->{ignored_users}{$user}) && 499 | !(exists($self->{users}{$user}) || 500 | exists($self->{users}{$user_gid})) ) { 501 | 502 | my $short = join(':', ($name, $uid)); 503 | 504 | if (exists($self->{users}{$short})) { 505 | # user exists, but it seems modified, compare fields 506 | my @changed = (); 507 | my $uf = $self->{user_fields}{$name}; 508 | push @changed, "gid is $gid, should be $uf->{gid}" 509 | if (defined $uf->{gid} and $gid != $uf->{gid}); 510 | push @changed, "group is $gname, should be $uf->{group}" 511 | if (defined $uf->{group} and $gname ne $uf->{group}); 512 | push @changed, "class is '$class', should be '$uf->{class}'" 513 | if ($class ne $uf->{class}); 514 | push @changed, "homedir is $home, should be $uf->{home}" 515 | if ($home ne $uf->{home}); 516 | push @changed, "shell is $shell, should be $uf->{shell}" 517 | if ($shell ne $uf->{shell}); 518 | print('@user ', $user, " => ", join(' / ', @changed), "\n"); 519 | } else { 520 | # not expected user 521 | print('@user ', $user, "\n"); 522 | } 523 | } 524 | } 525 | endpwent(); 526 | } 527 | 528 | # walk all groups in the system. 529 | sub walk_groups($self) 530 | { 531 | # walk groups 532 | while (my ($name, $passwd, $gid, $members) = getgrent()) { 533 | # only system groups 534 | next if ($gid >= 1000); 535 | 536 | my $group = join(':', ($name, $gid)); 537 | 538 | if (!exists($self->{ignored_groups}{$group}) && 539 | !exists($self->{groups}{$group})) { 540 | 541 | # not expected group 542 | print('@group ', $group, "\n"); 543 | } 544 | } 545 | endgrent(); 546 | } 547 | 548 | sub walk($self) 549 | { 550 | $self->walk_users; 551 | $self->walk_groups; 552 | $self->walk_filesystem; 553 | } 554 | 555 | 556 | # 557 | # specialized versions 558 | # 559 | 560 | package sysclean::allfiles; 561 | use parent -norequire, qw(sysclean); 562 | 563 | sub find_sub($self, $filename) 564 | { 565 | print($filename, "\n"); 566 | } 567 | 568 | package sysclean::files; 569 | use parent -norequire, qw(sysclean); 570 | 571 | use OpenBSD::LibSpec; 572 | 573 | sub add_expected_base_one($self, $filename) 574 | { 575 | $self->SUPER::add_expected_base_one($filename); 576 | 577 | # track libraries (should not contains duplicate) 578 | if ($filename =~ m|/lib([^/]+)\.so\.\d+\.\d+$|o) { 579 | $self->{libs}{$1} = OpenBSD::Library->from_string($filename); 580 | } 581 | } 582 | 583 | sub find_sub($self, $filename) 584 | { 585 | if ($filename =~ m|/lib([^/]*)\.so(\.\d+\.\d+)$|o) { 586 | 587 | if (exists($self->{used_libs}{"$1$2"})) { 588 | # skip used-libs (from ports) 589 | return; 590 | } 591 | 592 | if (exists($self->{libs}{$1})) { 593 | # skip if file from expected is not better than current 594 | my $expectedlib = $self->{libs}{$1}; 595 | my $currentlib = OpenBSD::Library->from_string($filename); 596 | 597 | if ($currentlib->is_better($expectedlib)) { 598 | $self->warn("discard better version: $filename"); 599 | return; 600 | } 601 | } 602 | } 603 | 604 | print($filename, "\n"); 605 | } 606 | 607 | package sysclean::packages; 608 | use parent -norequire, qw(sysclean); 609 | 610 | sub add_expected_rcctl($self) 611 | { 612 | # skip add_expected_rcctl: it shouldn't contain libraries 613 | } 614 | 615 | sub add_expected_users($self) 616 | { 617 | # skip add_expected_users 618 | } 619 | 620 | sub plist_reader($self) 621 | { 622 | return \&OpenBSD::PackingList::DependOnly; 623 | } 624 | 625 | sub walk($self) 626 | { 627 | $self->walk_filesystem; 628 | } 629 | 630 | sub find_sub($self, $filename) 631 | { 632 | if ($filename =~ m|/lib([^/]*)\.so(\.\d+\.\d+)$|o) { 633 | my $wantlib = "$1$2"; 634 | 635 | for my $pkgname (@{$self->{used_libs}{$wantlib}}) { 636 | print($filename, "\t", $pkgname, "\n") 637 | } 638 | } 639 | } 640 | 641 | 642 | # 643 | # extent OpenBSD::PackingElement for walking 644 | # 645 | 646 | package OpenBSD::PackingElement; 647 | sub walk_sysclean($item, $pkgname, $sc) 648 | { 649 | } 650 | 651 | package OpenBSD::PackingElement::Cwd; 652 | sub walk_sysclean($item, $pkgname, $sc) 653 | { 654 | use File::Basename; 655 | 656 | my $path = $item->name; 657 | 658 | do { 659 | $sc->{expected}{$path} = 1; 660 | $path = dirname($path); 661 | } while ($path ne "/"); 662 | } 663 | 664 | package OpenBSD::PackingElement::FileObject; 665 | sub walk_sysclean($item, $pkgname, $sc) 666 | { 667 | my $filename = $item->fullname; 668 | 669 | # link: /usr/local/lib/X11/app-defaults/ -> /etc/X11/app-defaults/ 670 | $filename =~ s|^/usr/local/lib/X11/app-defaults/|/etc/X11/app-defaults/|o; 671 | 672 | $sc->{expected}{$filename} = 1; 673 | } 674 | 675 | package OpenBSD::PackingElement::NewGroup; 676 | sub walk_sysclean($item, $pkgname, $sc) 677 | { 678 | my $group = join(':', map { $item->{$_} } 679 | qw(name gid)); 680 | 681 | $sc->{groups}{$group} = 1; 682 | } 683 | 684 | package OpenBSD::PackingElement::NewUser; 685 | sub walk_sysclean($item, $pkgname, $sc) 686 | { 687 | my $user = join(':', map { $item->{$_} } 688 | qw(name uid group class home shell)); 689 | my $short = join(':', map { $item->{$_} } 690 | qw(name uid)); 691 | 692 | $sc->{users}{$user} = 1; 693 | $sc->{users}{$short} = 1; 694 | $sc->{user_fields}{$item->{name}} = { 695 | class => $item->{class}, 696 | home => $item->{home}, 697 | shell => $item->{shell}, 698 | }; 699 | # sometimes, the group field in the PLIST is a gid, sometimes a groupname... 700 | if ($item->{group} =~ m|^\d+$|) { 701 | $sc->{user_fields}{$item->{name}}{gid} = $item->{group}; 702 | } else { 703 | $sc->{user_fields}{$item->{name}}{group} = $item->{group}; 704 | } 705 | } 706 | 707 | package OpenBSD::PackingElement::Sampledir; 708 | sub walk_sysclean($item, $pkgname, $sc) 709 | { 710 | $sc->{ignored}{$item->fullname} = 1; 711 | } 712 | 713 | package OpenBSD::PackingElement::Wantlib; 714 | sub walk_sysclean($item, $pkgname, $sc) 715 | { 716 | push(@{$sc->{used_libs}{$item->name}}, $pkgname); 717 | } 718 | 719 | 720 | # 721 | # main program 722 | # 723 | package main; 724 | 725 | use Getopt::Std; 726 | 727 | my %options = (); # program flags 728 | 729 | getopts("apih", \%options) || sysclean->usage; 730 | sysclean->usage if (defined $options{h} || scalar(@ARGV) != 0); 731 | 732 | sysclean->err(1, "need root privileges") if ($> != 0); 733 | 734 | sysclean->create(\%options)->walk; 735 | --------------------------------------------------------------------------------