the-bastion/bin/admin/check-consistency.pl

726 lines
27 KiB
Perl
Executable file

#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use Data::Dumper;
use Term::ANSIColor;
use Digest::MD5 ();
use File::Basename;
my $BASEDIR = dirname(__FILE__) . '/../..';
my $MIN_KEYGROUP_GID = 2000;
my $MAX_KEYGROUP_GID = 99999;
my @KEY_GROUPS_IGNORE = qw{ keeper reader };
my $HOME_SUBDIRS_IGNORE_RE = qr{^^};
my $bad;
# generate a uniq prefix based on caller's lineno and caller's caller's lineno, useful to grep or grep -v
sub _prefix { return uc(unpack('H*', pack('S', (caller(1))[2])) . unpack('H*', pack('S', (caller(2))[2]))) . ": "; }
sub info { print $_[0] . "\n"; return 1; } ## no critic (RequireArgUnpacking)
sub _wrn { $bad++; print colored(_prefix() . $_[0], "blue") . "\n"; return 1; } ## no critic (RequireArgUnpacking,ProhibitUnusedPrivateSubroutine)
sub _err { $bad++; print colored(_prefix() . $_[0], "red") . "\n"; return 1; } ## no critic (RequireArgUnpacking)
sub _crit { $bad++; print colored(_prefix() . $_[0], "bold red") . "\n"; return 1; } ## no critic (RequireArgUnpacking)
# Linux and BSD don't always have the same account names for UID/GID 0
my ($UID0) = (qx{getent passwd 0})[0] =~ /^([^:]+)/; ## no critic (ProhibitBacktickOperators)
my ($GID0) = (qx{getent group 0})[0] =~ /^([^:]+)/; ## no critic (ProhibitBacktickOperators)
my $islinux = (($^O =~ /linux/i) ? 1 : 0);
my $hasacls = (($^O =~ /linux|freebsd/i) ? 1 : 0);
# get all the key* groups
my %keygroupsbyname = ();
my %aclkgroupsbyname = ();
my %gkgroupsbyname = ();
my %owgroupsbyname = ();
my %keygroupsbyid = ();
my %aclkgroupsbyid = ();
my %gkgroupsbyid = ();
my %owgroupsbyid = ();
my $sudoers_dir = '/etc/sudoers.d';
if (!-d $sudoers_dir && -d '/usr/pkg/etc/sudoers.d') {
$sudoers_dir = '/usr/pkg/etc/sudoers.d';
}
elsif (!-d $sudoers_dir && !$islinux) {
$sudoers_dir = '/usr/local/etc/sudoers.d';
}
_err "/nonexistent exists" if -e "/nonexistent";
open(my $fh_group, '<', '/etc/group') or die $!;
while (<$fh_group>) {
/^key([^:]+):[^:]+:(\d+)/ or next;
my $name = $1;
my $id = $2;
if (exists $keygroupsbyname{$name} or exists $gkgroupsbyname{$name} or exists $owgroupsbyname{$name} or exists $aclkgroupsbyname{$name}) {
_err "group $name already seen!";
}
if ($name =~ /-gatekeeper$/) {
$gkgroupsbyname{$name} = {name => $name, id => $id};
}
elsif ($name =~ /-aclkeeper$/) {
$aclkgroupsbyname{$name} = {name => $name, id => $id};
}
elsif ($name =~ /-owner$/) {
$owgroupsbyname{$name} = {name => $name, id => $id};
}
else {
$keygroupsbyname{$name} = {name => $name, id => $id};
}
if (exists $keygroupsbyid{$id} or exists $gkgroupsbyid{$id} or exists $owgroupsbyid{$id} or exists $aclkgroupsbyname{$id}) {
_crit "group $name 's ID already seen!";
}
if ($name =~ /-gatekeeper$/) {
$gkgroupsbyid{$id} = {name => $name, id => $id};
}
elsif ($name =~ /-aclkeeper$/) {
$aclkgroupsbyid{$id} = {name => $name, id => $id};
}
elsif ($name =~ /-owner$/) {
$owgroupsbyid{$id} = {name => $name, id => $id};
}
else {
$keygroupsbyid{$id} = {name => $name, id => $id};
}
if (grep { $name eq $_ } @KEY_GROUPS_IGNORE) {
delete $keygroupsbyname{$name};
delete $keygroupsbyid{$id};
next;
}
if ($id > $MAX_KEYGROUP_GID) { _err "group $name id $id is too high"; }
if ($id < $MIN_KEYGROUP_GID) { _err "group $name id $id is too low"; }
}
close($fh_group);
info "found " . (scalar keys %keygroupsbyname) . " key groups";
# checking if allowkeeper is a member of all keygroups
my @allowkeeper_groups = split(/ /, qx/groups allowkeeper/); ## no critic (ProhibitBacktickOperators)
chomp @allowkeeper_groups;
# some outputs of `groups` include "$username :" as a prefix, strip that
if ($allowkeeper_groups[0] eq 'allowkeeper' && $allowkeeper_groups[1] eq ':') {
@allowkeeper_groups = splice @allowkeeper_groups, 2;
}
foreach my $group (keys %keygroupsbyname) {
_err "allowkeeper user is not a member of group key$group" if (not grep { $_ eq "key$group" } @allowkeeper_groups);
}
# now check if each key group has a gk
# and vice versa
foreach my $group (keys %keygroupsbyname) {
next if exists $gkgroupsbyname{$group . "-gatekeeper"};
_err "key group $group is missing a gatekeeper group";
}
foreach my $groupori (keys %gkgroupsbyname) {
my $group = $groupori;
$group =~ s/-gatekeeper$//;
next if exists $keygroupsbyname{$group};
_err "gatekeeper group $group is missing a key group";
}
foreach my $group (keys %keygroupsbyname) {
next if exists $owgroupsbyname{$group . "-owner"};
_err "key group $group is missing an owner group";
}
foreach my $groupori (keys %owgroupsbyname) {
my $group = $groupori;
$group =~ s/-owner$//;
next if exists $keygroupsbyname{$group};
_err "owner group $group is missing a key group";
}
# now check if each key group has a /home/key* $HOME
# and vice versa
my @keyhomesfound;
opendir(my $dh, "/home/") or die $!;
while (my $file = readdir($dh)) {
next unless -d "/home/$file";
next if $file eq '.';
next if $file eq '..';
if ($file !~ /[a-zA-Z0-9_-]+$/) {
_err "bad chars in /home/$file";
next;
}
push @keyhomesfound, $file if $file =~ /^key/;
}
foreach my $file (@keyhomesfound) {
my $file2 = $file;
$file2 =~ s/^key//;
next if exists $keygroupsbyname{$file2};
next if (grep { $file2 eq $_ } @KEY_GROUPS_IGNORE);
_err "directory /home/key$file2 exists but no key group $file2";
}
foreach my $group (keys %keygroupsbyname) {
next if -d "/home/key$group";
_err "key group $group is missing /home/key$group";
}
my %ALL_FILES;
foreach (qx{find /home/key* /home/keykeeper /home/allowkeeper -print}) { ## no critic (ProhibitBacktickOperators)
chomp;
/$HOME_SUBDIRS_IGNORE_RE/ and next;
$ALL_FILES{$_} = 1;
}
while (my $homedir = glob '/home/*') {
-d $homedir or next;
-d "$homedir/ttyrec" or next;
next if $homedir eq '/home/proxyhttp';
next if $homedir eq '/home/healthcheck';
#$ALL_FILES{$_} = 1;
#$ALL_FILES{$_.'/ttyrec'} = 1;
#$ALL_FILES{$_.'/.ssh'} = 1;
#$ALL_FILES{$_.'/osh.log'} = 1;
my ($user) = $homedir =~ m{/([^/]+)$};
my $usertty = "$user-tty";
if (not getgrnam($usertty)) {
$usertty = substr($user, 0, 5) . '-tty';
}
check_file_rights("$homedir",
["# file: $homedir", "# owner: $user", "# group: $user", "user::rwx", "group::r-x", "group:$usertty:--x", "group:osh-auditor:--x", "mask::r-x", "other::---",],
"drwxr-x--x", $user, $user);
check_file_rights(
"$homedir/ttyrec",
[
"# file: $homedir/ttyrec", "# owner: $user", "# group: $user", "user::rwx", "group::---", "group:$usertty:r-x",
"mask::r-x", "other::---", "default:user::rwx", "default:group::---", "default:group:$usertty:r-x", "default:mask::r-x",
"default:other::---",
],
"drwxrwxr-x",
$user, $user
);
check_file_rights("$homedir/.ssh",
["# file: $homedir/.ssh", "# owner: $user", "# group: $user", "user::rwx", "group::r-x", "group:osh-auditor:--x", "mask::r-x", "other::---",],
"drwxr-x---", $user, $user);
if (-e "$homedir/osh.log") # doesn't exist? nevermind
{
check_file_rights("$homedir/osh.log", ["# file: $homedir/osh.log", "# owner: $user", "# group: $user", "user::rw-", "group::r--", "other::---",],
"-rw-r-----", $user, $user);
}
# now check all keys in ~/.ssh
opendir(my $dh, "$homedir/.ssh") or die "$homedir/.ssh: $!";
while (my $keyfile = readdir($dh)) {
next unless $keyfile =~ /^id_|private/;
my $ret = check_file_rights(
"$homedir/.ssh/$keyfile",
[
"# file: $homedir/.ssh/$keyfile",
"# owner: $user",
"# group: $user",
"user::r--",
$keyfile =~ /\.pub$/ ? "group::r--" : "group::---",
$keyfile =~ /\.pub$/ ? "other::r--" : "other::---",
],
$keyfile =~ /\.pub$/ ? "-r--r--r--" : "-r--------",
$user, $user
);
if ($keyfile !~ /\.pub$/) {
if (not $ret) {
# wow ! private key readable ?
_crit "due to above error, private key $homedir/.ssh/$keyfile might be readable !!";
}
}
else {
# check for spurious "from" in .pub
open(my $pubfh, '<', "$homedir/.ssh/$keyfile") or die "$homedir/.ssh/$keyfile: $!";
while (<$pubfh>) {
/from=/ and _err "spurious from='...' in $homedir/.ssh/$keyfile";
}
close($pubfh);
}
}
close($dh);
}
sub check_file_rights {
my $file = shift;
my $expectedOutput = shift;
my $expectedmodes = shift;
my $expectedowner = shift;
my $expectedgroup = shift;
#info "checking rights of $file";
delete $ALL_FILES{$file};
my $ok = 1;
if (not -e $file) {
_err "file $file doesn't exist!";
$ok = 0;
return $ok;
}
if (!$hasacls) {
my ($modes, $owner, $group) = (qx{ls -ld $file})[0] =~ m{(\S+)\s+\d+\s+(\S+)\s+(\S+)}; ## no critic (ProhibitBacktickOperators)
if ($modes ne $expectedmodes) { $ok = 0; _err "on $file got $modes wanted $expectedmodes"; }
if ($owner ne $expectedowner) { $ok = 0; _err "on $file got $owner wanted $expectedowner"; }
if ($group ne $expectedgroup) { $ok = 0; _err "on $file got $group wanted $expectedgroup"; }
return $ok;
}
my $param = ($islinux ? '-p' : '');
my @out = qx{getfacl $param $file 2>/dev/null}; ## no critic (ProhibitBacktickOperators)
chomp @out;
my $lineno = -1;
$expectedOutput = [sort @$expectedOutput];
@out = grep { /./ } sort @out;
foreach my $outLine (@out) {
next if not $outLine;
$lineno++;
$outLine eq $expectedOutput->[$lineno] and next;
$ok = 0;
_err "rights of $file, line$lineno, expected '" . $expectedOutput->[$lineno] . "' but got '" . $outLine . "'";
}
if (@out != @$expectedOutput) {
_err "rights of $file, number of lines unexpected (got " . @out . " instead of " . @$expectedOutput . ")";
$ok = 0;
}
return $ok;
}
# now check what is in /home/key* and the rights
foreach my $file (@keyhomesfound) {
delete $ALL_FILES{"/home/$file/.bash_logout"};
delete $ALL_FILES{"/home/$file/.bashrc"};
delete $ALL_FILES{"/home/$file/.profile"};
delete $ALL_FILES{"/home/$file/.ssh"};
delete $ALL_FILES{"/home/$file/.ssh/known_hosts"};
# check rights of /home/keytruc
if (-e "/home/$file") {
if ($file ne 'keykeeper' and $file ne 'keyreader') {
check_file_rights(
"/home/$file",
[
"# file: /home/$file", "# owner: $file", "# group: $file", "user::rwx",
"group::r-x", "group:osh-whoHasAccessTo:--x", "group:osh-auditor:--x", "group:$file-aclkeeper:--x",
"group:$file-gatekeeper:--x", "group:$file-owner:--x", "mask::r-x", "other::---",
],
"drwxr-x--x",
$file, $file
);
}
else {
check_file_rights(
"/home/$file",
[
"# file: /home/$file",
"# owner: $file",
"# group: $file",
"user::rwx",
"group::r-x",
$file eq 'keykeeper' ? "other::r-x" : "other::---", # special dir /home/keykeeper is 755
],
$file eq 'keykeeper' ? "drwxr-xr-x" : "drwxr-x---",
$file,
$file
);
}
}
else {
_err "/home/$file doesn't exist";
}
next if (grep { $file eq "key$_" } @KEY_GROUPS_IGNORE);
# check rights of /home/keytruc/allowed.ip
if (-e "/home/$file/allowed.ip") {
#not -s "/home/$file/allowed.ip" and _wrn "group $file has no servers";
check_file_rights("/home/$file/allowed.ip", ["# file: /home/$file/allowed.ip", "# owner: $file", "# group: $file-aclkeeper", "user::rw-", "group::rw-", "other::r--",],
"-rw-rw-r--", $file, "$file-aclkeeper");
}
else {
_err "/home/$file/allowed.ip doesn't exist";
}
# check rights of /home/keykeeper/keytruc/
if (-e "/home/keykeeper/$file") {
check_file_rights("/home/keykeeper/$file", ["# file: /home/keykeeper/$file", "# owner: keykeeper", "# group: $file", "user::rwx", "group::r-x", "other::r-x",],
"drwxr-xr-x", "keykeeper", $file);
}
else {
_err "/home/keykeeper/$file doesn't exist";
}
# check rights of /home/keykeeper/keytruc/id_*
opendir(my $dh, "/home/keykeeper/$file") or die "/home/keykeeper/$file: $!";
while (my $keyfile = readdir($dh)) {
next unless $keyfile =~ /^id_/; # spurious files will be reported below
my $ret = check_file_rights(
"/home/keykeeper/$file/$keyfile",
["# file: /home/keykeeper/$file/$keyfile", "# owner: keykeeper", "# group: $file", "user::r--", "group::r--", $keyfile =~ /\.pub$/ ? "other::r--" : "other::---",],
$keyfile =~ /\.pub$/ ? "-r--r--r--" : "-r--r-----",
"keykeeper", $file
);
if ($keyfile !~ /\.pub$/) {
if (not $ret) {
# wow ! private key readable ?
_crit "due to above error, private key /home/keykeeper/$file/$keyfile might be readable !!";
}
}
else {
# check for spurious "from" in .pub
open(my $pubfh, '<', "/home/keykeeper/$file/$keyfile") or die "/home/keykeeper/$file/$keyfile: $!";
while (<$pubfh>) {
/from=/ and _err "spurious from='...' in /home/keykeeper/$file/$keyfile";
}
close($pubfh);
}
}
close($dh);
}
# check some special dirs
check_file_rights("/home/allowkeeper", ["# file: /home/allowkeeper", "# owner: allowkeeper", "# group: allowkeeper", "user::rwx", "group::r-x", "other::r-x",],
"drwxr-xr-x", "allowkeeper", "allowkeeper");
check_file_rights("/home/keykeeper", ["# file: /home/keykeeper", "# owner: keykeeper", "# group: keykeeper", "user::rwx", "group::r-x", "other::r-x",],
"drwxr-xr-x", "keykeeper", "keykeeper");
check_file_rights("/home/logkeeper", ["# file: /home/logkeeper", "# owner: $UID0", "# group: bastion-users", "user::rwx", "group::-wx", "other::---",],
"drwx-wx---", $UID0, "bastion-users");
check_file_rights("/home/passkeeper", ["# file: /home/passkeeper", "# owner: $UID0", "# group: $GID0", "user::rwx", "group::r-x", "other::r-x",], "drwxr-xr-x", $UID0, $GID0);
check_file_rights("/home/oldkeeper", ["# file: /home/oldkeeper", "# owner: $UID0", "# group: $GID0", "user::rwx", "group::---", "other::---",], "drwx------", $UID0, $GID0)
if -e "/home/oldkeeper";
# now get all bastion users
my %users;
my %usersbyid;
setpwent();
while (my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $home, $shell, $expire) = getpwent()) {
if ($shell =~ /osh.pl$|diverter.sh$/) {
if (exists $users{$name}) {
_err "duplicate user $name";
}
if (exists $usersbyid{$uid}) {
_err "duplicate uid for user $name";
}
if ($home ne "/home/$name") {
_err "bad home for $name: $home";
}
if (!-d $home) {
_err "home of $name doesn't exist ($home)";
}
$users{$name} = {name => $name, uid => $uid, gid => $gid, shell => $shell};
$usersbyid{$uid} = {name => $name, uid => $uid, gid => $gid, shell => $shell};
}
# TODO check who has shell access
}
info "found " . (scalar keys %users) . " bastion users";
my %groups;
my %usergroups;
setgrent();
while (my ($name, $passwd, $gid, $members) = getgrent()) {
$groups{$name} = {name => $name, gid => $gid, members => [split(/ /, $members)]};
foreach my $member (split(/ /, $members)) {
push @{$usergroups{$member}}, $name;
}
}
info "found " . (scalar keys %groups) . " groups";
# check that user keyreader is a member of all bastion users primary groups
my %keyreaderuserseen;
foreach my $group (@{$usergroups{'keyreader'}}) {
$keyreaderuserseen{$group} = 1;
}
foreach my $user (keys %users) {
next if (exists $keyreaderuserseen{$user});
_err "user $user primary group doesn't have keyreader as member";
if ($ENV{'FIX_KEYREADER'}) {
system("usermod -a -G $user keyreader");
_err "... fixed!";
}
}
# check if user has /home/allowkeeper/testuser4/allowed.private
foreach my $account (keys %users) {
check_file_rights("/home/allowkeeper/$account",
["# file: /home/allowkeeper/$account", "# owner: allowkeeper", "# group: allowkeeper", "user::rwx", "group::r-x", "other::r-x",],
"drwxr-xr-x", "allowkeeper", "allowkeeper");
check_file_rights(
"/home/allowkeeper/$account/allowed.ip",
["# file: /home/allowkeeper/$account/allowed.ip", "# owner: allowkeeper", "# group: allowkeeper", "user::rw-", "group::r--", "other::r--",],
"-rw-r--r--", "allowkeeper", "allowkeeper"
);
check_file_rights(
"/home/allowkeeper/$account/allowed.private",
["# file: /home/allowkeeper/$account/allowed.private", "# owner: allowkeeper", "# group: allowkeeper", "user::rw-", "group::r--", "other::r--",],
"-rw-r--r--", "allowkeeper", "allowkeeper"
);
if (!-e "/home/allowkeeper/$account/allowed.private" && $ENV{'FIX_MISSING_PRIVATE_FILES'}) {
if (open(my $fh_priv, '>', "/home/allowkeeper/$account/allowed.private")) {
close($fh_priv);
}
chmod 0644, "/home/allowkeeper/$account/allowed.private";
my (undef, undef, $allowkeeperuid, $allowkeepergid) = getpwnam("allowkeeper");
chown $allowkeeperuid, $allowkeepergid, "/home/allowkeeper/$account/allowed.private";
_err "... fixed!";
}
# check all allowed.ip.GROUP symlinks
my $dh;
if (-d "/home/allowkeeper/$account") {
opendir($dh, "/home/allowkeeper/$account");
while (my $file = readdir($dh)) {
if ($file =~ /^config\.[a-zA-Z0-9_-]+$/) {
delete $ALL_FILES{"/home/allowkeeper/$account/$file"};
next;
}
elsif ($file !~ /^allowed\.(ip|partial)\.([a-zA-Z0-9_-]+)$/) {
next;
}
if (not grep { $2 eq $_ } keys %keygroupsbyname) {
_err "file /home/allowkeeper/$account/$file has no corresponding known group";
}
if ($1 eq 'ip') {
if (not -l "/home/allowkeeper/$account/$file") {
_err "file /home/allowkeeper/$account/$file should have been a symlink";
}
}
elsif ($1 eq 'partial') {
if (not -f "/home/allowkeeper/$account/$file") {
_err "file /home/allowkeeper/$account/$file should have been a plain file";
}
}
else {
_err "hmm, bug in the script ? got a '$1'";
}
delete $ALL_FILES{"/home/allowkeeper/$account/$file"};
}
close($dh);
}
}
delete $ALL_FILES{'/home/allowkeeper'};
delete $ALL_FILES{'/home/allowkeeper/.bash_logout'};
delete $ALL_FILES{'/home/allowkeeper/.bashrc'};
delete $ALL_FILES{'/home/allowkeeper/.profile'};
delete $ALL_FILES{'/home/allowkeeper/.ssh'};
delete $ALL_FILES{'/home/allowkeeper/activeLogin.json'};
delete $ALL_FILES{'/home/allowkeeper/expirationGrant.json'};
if (keys %ALL_FILES) {
_err "got some potentially unknown files:";
print Dumper(sort keys %ALL_FILES);
}
# for new code, check sudo stuff
sub _tocheck {
my $file = shift;
my $filesuffix = shift;
my $tocheckref = shift;
my %tocheck = %$tocheckref;
if (exists $tocheck{'NEEDGROUP'}) {
my $group = $tocheck{'NEEDGROUP'}[0];
my $gid = getgrnam($group);
if (not defined $gid) {
_err "missing group $group";
}
elsif ($gid > 1000) {
_err "group $group has a too high gid ($gid)";
}
}
my @stat = stat($file);
if (exists $tocheck{'FILEMODE'}) {
my $mode = sprintf '%04o', $stat[2] & oct(7777);
if ($mode ne $tocheck{'FILEMODE'}[0]) {
_err "bad file mode on $file, got $mode but expected " . $tocheck{'FILEMODE'}[0];
}
}
if (exists $tocheck{'FILEOWN'}) {
my $uid = $stat[4];
my $gid = $stat[5];
my $wantuser = (split / /, $tocheck{'FILEOWN'}[0])[0];
my $wantgroup = (split / /, $tocheck{'FILEOWN'}[0])[1];
$wantuser = $UID0 if ($wantuser eq 'root' || $wantuser eq '0');
$wantgroup = $GID0 if ($wantgroup eq 'root' || $wantgroup eq '0');
my $wantuid = getpwnam($wantuser);
my $wantgid = getgrnam($wantgroup);
if ($uid ne $wantuid) {
_err "bad owner on file $file (got $uid but wanted $wantuid aka $wantuser)";
}
if ($gid ne $wantgid) {
_err "bad group on file $file (got $gid but wanted $wantgid aka $wantgroup)";
}
}
if (exists $tocheck{'SUDOERS'}) {
my $sudoersfile = "$sudoers_dir/osh-plugin-" . $filesuffix;
if (not -f $sudoersfile) {
_err "sudoers file $sudoersfile doesn't exists";
}
else {
my $mode = sprintf '%04o', (stat($sudoersfile))[2] & oct(7777);
if ($mode ne "0440") {
_err "sudoers file $sudoersfile has a bad mode $mode";
}
if (!open(my $fh_sudoers, '<', $sudoersfile)) {
_err "can't open sudoers file $sudoersfile to check";
}
else {
my @contents = <$fh_sudoers>;
close($fh_sudoers);
chomp @contents;
foreach my $wantedline (@{$tocheck{'SUDOERS'}}) {
if (not grep { $_ eq $wantedline } @contents) {
_err "missing line in plugin $sudoersfile: $wantedline";
}
}
}
}
}
if (exists $tocheck{'KEYSUDOERS'}) {
my @contents;
foreach my $sudoersfile (sort <$BASEDIR/etc/sudoers.group.template.d/*>) {
if (!open(my $fh_sudoers, '<', $sudoersfile)) {
_err "can't open sudoers file template $sudoersfile to check";
}
else {
my @lines = <$fh_sudoers>;
close($fh_sudoers);
chomp @lines;
push @contents, @lines;
}
}
if (@contents) {
foreach my $wantedline (@{$tocheck{'KEYSUDOERS'}}) {
$wantedline =~ s'@KEYGROUP@'%GROUP%'g;
if (not grep { $_ eq $wantedline } @contents) {
_err "missing line in plugin sudoers.group.template: $wantedline";
}
}
}
}
foreach my $key (qw{ FILEMODE FILEOWN SUDOERS NEEDGROUP KEYSUDOERS }) {
delete $tocheck{$key};
}
if (keys %tocheck) {
_err "hum sparse tocheck key: " . join(" ", sort keys %tocheck);
}
return 1;
}
while (my $file = glob "$BASEDIR/bin/helper/*") {
my ($filesuffix) = $file =~ m{/osh-([a-zA-Z0-9_-]+$)};
if (!$filesuffix) {
_err "helper file has a strange name ($file)";
next;
}
my $fh_helper;
if (!open($fh_helper, '<', $file)) {
_err "can't open helper file $file to check";
next;
}
my %tochecklocal;
while (<$fh_helper>) {
/^#/ or last;
if (/^\s*#\s*$/) {
_tocheck($file, $filesuffix, \%tochecklocal);
%tochecklocal = ();
next;
}
/^# ([A-Z0-9]+) (.+)$/ or next;
my ($keyword, $line) = ($1, $2);
push @{$tochecklocal{$keyword}}, $line;
}
close($fh_helper);
if (%tochecklocal) {
_tocheck($file, $filesuffix, \%tochecklocal);
}
}
# check /etc/sudoers.d vs $BASEDIR/etc/sudoers.d
# FIXME won't see if we have too many / old files in /etc/sudoers.d
while (my $distfile = glob "$BASEDIR/etc/sudoers.d/*") {
my $prodfile = $distfile;
$prodfile =~ s=^\Q$BASEDIR\E/etc/sudoers.d=$sudoers_dir=;
if (-e $prodfile) {
my @md5sums;
foreach my $file ($prodfile, $distfile) {
if (open(my $fh, '<', $file)) {
binmode($fh);
push @md5sums, Digest::MD5->new->addfile($fh)->hexdigest;
close($fh);
}
else {
push @md5sums, "ERR($file)";
}
}
if ($md5sums[0] ne $md5sums[1]) {
_err "sudoers file $distfile and $prodfile differ";
}
}
else {
_err "sudoers file $prodfile not found";
}
}
if (1) {
my @template;
foreach my $sudoersfile (sort <$BASEDIR/etc/sudoers.group.template.d/*>) {
if (!open(my $fh_sudoers, '<', $sudoersfile)) {
_err "can't open sudoers file template $sudoersfile to check";
}
else {
my @lines = <$fh_sudoers>;
close($fh_sudoers);
chomp @lines;
push @template, @lines;
}
}
my %seensudogroupfile;
while (my $sudoersfile = glob "$sudoers_dir/osh-group-*") {
# TODO check 0440
# TODO check there's a matching group (and the other way around)
# the unescaped group should be present in the file (see GROUPNAME below),
# but to backwards compatible, try to guess it by ourselves if it's not
my $group = $sudoersfile;
$group =~ s/^.*osh-group-key//;
my $fh_sudoers;
if (!open($fh_sudoers, '<', $sudoersfile)) {
_err "can't open $sudoersfile file to check: $!";
next;
}
my @contents = <$fh_sudoers>;
close($fh_sudoers);
chomp @contents;
# now try to get it from the file
foreach (@contents) {
/^# GROUPNAME=key(.+)/ and $group = $1;
}
$seensudogroupfile{$group} = 1;
my @expected = @template;
do { s/%GROUP%/key$group/g; s=%BASEPATH%=/opt/bastion=g; }
for @expected;
foreach my $wantedline (@expected) {
if (not grep { $_ eq $wantedline } @contents) {
_err "missing line in $sudoersfile: $wantedline";
}
}
}
foreach my $group (keys %keygroupsbyname) {
next if exists $seensudogroupfile{$group};
_err "missing $sudoers_dir/osh-group-key$group file";
}
}
exit($bad > 255 ? 255 : $bad);