#! /usr/bin/env perl # vim: set filetype=perl ts=4 sw=4 sts=4 et: use common::sense; use Term::ANSIColor; use JSON; use File::Basename; use lib dirname(__FILE__) . '/../../../lib/perl'; use OVH::Result; use OVH::Bastion; use OVH::Bastion::Plugin qw( :DEFAULT help ); # globally allow sys_getpw* and sys_getgr* cache use $ENV{'PW_GR_CACHE'} = 1; my $remainingOptions = OVH::Bastion::Plugin::begin( argv => \@ARGV, header => "list bastion accounts", options => { "inactive-only" => \my $inactiveOnly, "realm-only" => \my $realmOnly, "account=s" => \my $account, "audit" => \my $audit, "no-password-info" => \my $noPasswordInfo, "no-output" => \my $noOutput, 'exclude=s' => \my @excludes, 'include=s' => \my @includes, }, helptext => <<'EOF', List the bastion accounts Usage: --osh SCRIPT_NAME [OPTIONS] --account ACCOUNT Only list the specified account. This is an easy way to check whether the account exists --inactive-only Only list inactive accounts --audit Show more verbose information (SLOW!), you need to be a bastion auditor --no-password-info Don't gather password info in audit mode (makes --audit way faster) --no-output Don't print human-readable output (faster, use with --json) --include PATTERN Only show accounts whose name match the given PATTERN (see below) This option can be used multiple times to refine results --exclude PATTERN Omit accounts whose name match the given PATTERN (see below) This option can be used multiple times. Note that --exclude takes precedence over --include **Note:** PATTERN supports the ``*`` and ``?`` wildcards. If PATTERN is a simple string without wildcards, then names containing this string will be considered. EOF ); sub tristate2str { my $v = shift; my $r = shift; return ( defined $v ? ($v ? colored('yes', $r ? 'red' : 'green') : colored('no', $r ? 'green' : 'red')) : colored('-', 'blue') ); } if ($realmOnly) { osh_exit(R('ERR_INVALID_PARAMETER'), "Option --realm-only is no longer supported, use realmList instead"); } my $fnret; if ($account) { $fnret = OVH::Bastion::get_account_list(accounts => [$account]); } else { $fnret = OVH::Bastion::get_account_list(); } $fnret or osh_exit $fnret; my $accounts = $fnret->value; if ($audit && !OVH::Bastion::is_auditor(account => $self)) { osh_exit(R('ERR_PERMISSION_DENIED', msg => "You need to be a bastion auditor to use --audit")); } my $fnretPassword; if ($audit && !$noPasswordInfo) { # get UNIX password info for all accounts my @command = qw{ sudo -n -u root -- /usr/bin/env perl -T }; push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-accountGetPasswordInfo', '--all'; $fnretPassword = OVH::Bastion::helper(cmd => \@command); } # if we have excludes and/or includes, transform those into regexes my $includere = OVH::Bastion::build_re_from_wildcards(wildcards => \@includes, implicit_contains => 1)->value; my $excludere = OVH::Bastion::build_re_from_wildcards(wildcards => \@excludes, implicit_contains => 1)->value; my $result_hash = {}; foreach my $account (sort keys %$accounts) { # if we have excludes, match name against the built regex next if ($excludere && $account =~ $excludere); # same for includes next if ($includere && $account !~ $includere); my %states; $states{'is_active'} = undef; $fnret = OVH::Bastion::is_account_active(account => $account); if ($fnret->is_ok) { next if $inactiveOnly; $states{'is_active'} = 1; } elsif ($fnret->is_ko) { $states{'is_active'} = 0; } if ($audit) { $fnret = OVH::Bastion::is_account_nonfrozen(account => $account); $states{'is_frozen'} = undef; if ($fnret->is_ok) { $states{'is_frozen'} = 0; } elsif ($fnret->is_ko) { $states{'is_frozen'} = 1; $states{'freeze_info'} = $fnret->value; } $fnret = OVH::Bastion::is_account_ttl_nonexpired(sysaccount => $account, account => $account); $states{'is_ttl_expired'} = undef; if ($fnret->is_ok) { $states{'is_ttl_expired'} = 0; } elsif ($fnret->is_ko) { $states{'is_ttl_expired'} = 1; $states{'ttl_expired_details'} = $fnret->value->{'details'}; } $fnret = OVH::Bastion::is_account_nonexpired(sysaccount => $account); $states{'is_expired'} = undef; if ($fnret->is_ok) { $states{'is_expired'} = 0; } elsif ($fnret->is_ko) { $states{'is_expired'} = 1; $states{'expired_days'} = $fnret->value->{'days'}; } $states{'already_seen_before'} = undef; if ($fnret->value && defined $fnret->value->{'already_seen_before'}) { $states{'already_seen_before'} = $fnret->value->{'already_seen_before'} ? 1 : 0; } $states{'last_activity'} = undef; $states{'last_activity_timestamp'} = undef; my $seconds = $fnret->value->{'seconds'}; if (defined $seconds) { $fnret = OVH::Bastion::duration2human(seconds => $seconds, tense => "past"); if ($fnret) { $states{'last_activity_timestamp'} = time() - $seconds; $states{'last_activity'} = sprintf( "%s on %s (%s ago)", ( defined $states{'already_seen_before'} ? ($states{'already_seen_before'} ? "Last seen" : "Created") : "Last activity" ), $fnret->value->{'date'}, $fnret->value->{'duration'} ); } } $states{'can_connect'} = 1; $states{'can_connect'} = 0 if (!defined $states{'is_active'} || $states{'is_active'} == 0); $states{'can_connect'} = 0 if (!defined $states{'is_frozen'} || $states{'is_frozen'} == 0); $states{'can_connect'} = 0 if (!defined $states{'is_expired'} || $states{'is_expired'} == 0); $states{'can_connect'} = 0 if (!defined $states{'is_ttl_expired'} || $states{'is_ttl_expired'} == 0); $states{'mfa_password_required'} = OVH::Bastion::is_user_in_group( user => $account, group => OVH::Bastion::MFA_PASSWORD_REQUIRED_GROUP, ) ? 1 : 0; $states{'mfa_password_configured'} = OVH::Bastion::is_user_in_group( user => $account, group => OVH::Bastion::MFA_PASSWORD_CONFIGURED_GROUP, ) ? 1 : 0; $states{'mfa_password_bypass'} = OVH::Bastion::is_user_in_group( user => $account, group => OVH::Bastion::MFA_PASSWORD_BYPASS_GROUP, ) ? 1 : 0; $states{'mfa_totp_required'} = OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_REQUIRED_GROUP) ? 1 : 0; $states{'mfa_totp_configured'} = OVH::Bastion::is_user_in_group( user => $account, group => OVH::Bastion::MFA_TOTP_CONFIGURED_GROUP, ) ? 1 : 0; $states{'mfa_totp_bypass'} = OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_BYPASS_GROUP) ? 1 : 0; $states{'pam_auth_bypass'} = OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP) ? 1 : 0; $states{'pubkey_auth_optional'} = OVH::Bastion::is_user_in_group( user => $account, group => OVH::Bastion::OSH_PUBKEY_AUTH_OPTIONAL_GROUP, ) ? 1 : 0; if ($fnretPassword) { $states{"password_$_"} = $fnretPassword->value->{$account}{$_} for (keys %{$fnretPassword->value->{$account}}); } } $fnret = OVH::Bastion::account_config(account => $account, key => "creation_info"); if ($fnret && $fnret->value) { eval { my $data = decode_json($fnret->value); $states{'created_by'} = $data->{'by'}; }; if ($@) { osh_warn("Error decoding creation_info of account '$account' ($@)"); } } $result_hash->{$account} = \%states; $result_hash->{$account}{'name'} = $account; $result_hash->{$account}{'uid'} = $accounts->{$account}{'uid'}; # don't print human-readable version (usually used with --json) next if $noOutput; if ($audit) { my @mfaPassword; push @mfaPassword, 'required' if $states{'mfa_password_required'}; push @mfaPassword, 'enabled' if $states{'mfa_password_configured'}; push @mfaPassword, 'bypass' if $states{'mfa_password_bypass'}; my @mfaTOTP; push @mfaTOTP, 'required' if $states{'mfa_totp_required'}; push @mfaTOTP, 'enabled' if $states{'mfa_totp_configured'}; push @mfaTOTP, 'bypass' if $states{'mfa_totp_bypass'}; osh_info sprintf( "%-18s %6d active:%-12s expired:%-12s frozen:%-12s ttl_expired:%-12s" . "can_connect:%-12s already_seen:%-12s mfa_password:%-25s " . "mfa_totp:%-25s pam_bypass:%-12s pubkey_auth_optional:%-12s " . "pass_status:%-15s pass_changed:%-10s pass_min_days:%-3d " . "pass_max_days:%-3d pass_warn_days:%-3d created_by:%-12s " . " %s\n", $account, $accounts->{$account}{'uid'}, tristate2str($states{'is_active'}), tristate2str($states{'is_expired'}, 1), tristate2str($states{'is_frozen'}, 1), tristate2str($states{'is_ttl_expired'}, 1), tristate2str($states{'can_connect'}), tristate2str($states{'already_seen_before'}), @mfaPassword ? colored(join(',', @mfaPassword), 'green') : colored('-', 'blue'), @mfaTOTP ? colored(join(',', @mfaTOTP), 'green') : colored('-', 'blue'), tristate2str($states{'pam_auth_bypass'}, 1), tristate2str($states{'pubkey_auth_optional'}, 1), ( $states{'password_password'} eq 'locked' ? colored('locked', 'blue') : ( $states{'password_password'} eq 'set' ? colored('set', 'green') : colored($states{'password_password'}, 'red') ) ), $states{'password_date_changed'}, $states{'password_min_days'}, $states{'password_max_days'}, $states{'password_warn_days'}, $states{'created_by'}, $states{'last_activity'}, ); } else { osh_info sprintf("%-18s %6d\n", $account, $accounts->{$account}{'uid'}); } } if ($noOutput) { osh_info "No-output requested, if you see only this message, you might have omitted --json"; } osh_ok $result_hash;