#!/usr/bin/perl -w

=head1 NAME 
 
user_access_audit.pl

=head1 SYNOPSIS

C<user_access_audit.pl>

=head1 DESCRIPTION

C<user_access_audit.pl> reports on which ActiveDirectory users are allowed
to login to an HP-UX systems based on the policy set by pam_authz.

=head1 BUGS

C<user_access_audit.pl> doesn't understand the full pam_authz policy syntax.
It does not check to see if there are different pam policies for different
services.

It wouldn't be very difficult to adapt this to work with Linux boxes which
authenticate to ActiveDirectory, but I haven't done that yet.

=head1 AUTHOR

Greg Baker C<gregb@ifost.org.au>

=cut

use strict;
my %allowed_groups = ();
my $g;
my $m;
my $local_users_allowed = "Local user accounts do not have access automatically.";
my $include_local_users_automatically = 0;
my $pam_authz = "/etc/opt/ldapux/pam_authz.policy";
my $groups;
my $imaginary_user = "-";

die "Must run as root.\n" unless $> == 0;

open(NSSWITCH_CONF,"/etc/nsswitch.conf")||die "Can't read /etc/nsswitch.conf";
while(<NSSWITCH_CONF>) {
  s/#.*//;
  next if /^\s*$/;
  next if (/^passwd:\s*files\s*ldap/);
  die "$0 only makes sense to run if /etc/nsswitch.conf contains passwd: files ldap" if /^passwd:/;
  next if (/^group:\s*files\s*ldap/);
  die "$0 only makes sense to run if /etc/nsswitch.conf contains group: files ldap" if /^passwd:/;
}

open(PAM_CONF,"/etc/pam.conf")||die "Can't read /etc/pam.conf";
my $seen_authz = 0;
while(<PAM_CONF>) {
  next unless /account/;
  s/#.*//;
  next unless /required.*libpam_authz/;
  $seen_authz = 1;
  last;
}
die "$0 only makes sense to run if /etc/pam.conf mentions libpam_authz"
  unless $seen_authz;


open(AUTHZ_POLICY,$pam_authz) || die "Can't read $pam_authz";
while (<AUTHZ_POLICY>) {
  chomp;
  s/#.*//;
  next if /^\s*$/;
  if (/^allow:unix_local_user$/) { 
    $local_users_allowed = "Local users accounts are automatically allowed"; 
    $include_local_users_automatically = 1;
    next; 
  }
  if (/^allow:unix_group:(.*)$/) { 
    my @groups = split(/,/,$1);
    foreach $g (@groups) { $allowed_groups{$g} = 1; }
    next;
  }
  die "Didn't understand line $. of $pam_authz: '$_'";
}

my %allowed_users;
my %email_address;

my @all_groups;
my %gid_lookup;


# Yes, I really want to use grget. I want to make sure this runs
# correctly even if Perl was compiled against a different libc to the system.
open(GRGET,"grget|") || die "Can't run grget";
while (<GRGET>) {
  chomp;
  next unless /:.*:.*:/;
  my ($group_name,$group_password,$gid,$members) = split(/:/);
  $gid_lookup{$gid} = $group_name;
  if (exists $allowed_groups{$group_name} and 
      $allowed_groups{$group_name}==1) { 
    my @member_list = split(/,/,$members);
    foreach $m (@member_list) { 
      $allowed_users{$m} = { 'directory source' => $imaginary_user,
			     'groups' => [] 
			   } unless exists $allowed_users{$m};
      push(@{$allowed_users{$m}->{'groups'}},$group_name);
    }
  }
}

open(PWGET,"pwget|") || die "Can't run pwget";
while (<PWGET>) {
  chomp;
  my ($username,$passwd_placeholder,$userid,$groupid,$gecos,$homedir,$shell) = split(/:/);
  #my $directory_source = "ActiveDirectory";
  my $directory_source;
  if ($gecos =~ /@/) {
    my ($lhs,$rhs) = split(/@/,$gecos);
    my ($firstname,$lastname) = split(/\./,$lhs);
    $directory_source = "AD: ".(ucfirst $firstname)." ".(ucfirst $lastname);
  } else {
    $directory_source = "AD: (no email)";
  }
  if (exists $allowed_users{$username}) {
    $allowed_users{$username}->{"directory source"} = $directory_source;
  }
  if ($shell =~ m:/false$: or $shell = m:true$:) {
    # User can't log on.
    delete $allowed_users{$username};
    next;
  }

  next unless exists $gid_lookup{$groupid};
  my $primary_group = $gid_lookup{$groupid};
  next unless exists $allowed_groups{$primary_group};
  next unless $allowed_groups{$primary_group} == 1;
  $allowed_users{$username} = { 
			       'directory source' => $directory_source,
			       'groups' => [] 
			      } unless exists $allowed_users{$username};
  push(@{$allowed_users{$username}->{'groups'}},$primary_group);
}


open(SHADOW,"/etc/shadow")|| die "Can't open /etc/shadow (which is necessary to figure out what users have passwords";
while (<SHADOW>) {
  my ($username,$encrypted_password,@other_junk) = split(/:/);
  if ($encrypted_password eq "*" or $encrypted_password eq "x" 
      or $encrypted_password eq "!!") {
    # User can't log on.
    delete $allowed_users{$username};
    next;
  }
  if (exists $allowed_users{$username}) {
    $allowed_users{$username}->{"directory source"} = "/etc/passwd";
  } elsif ($include_local_users_automatically) {
      $allowed_users{$username} = {
				   "directory source" => "/etc/passwd",
				   'groups' => []
				  };
  }
}

my %local_groups = ();
open(LOCAL_GROUPS,"/etc/group") || die "Can't read /etc/group";
while (<LOCAL_GROUPS>) {
  chomp;
  my ($groupname,@junk) = split(/:/);
  $local_groups{$groupname} = 1;
}


my %last_login = ();
open(LAST,"last|") || die "Can't run last";
while(<LAST>) {
  my @line_fields = split(/\s+/);
  next if $#line_fields == -1;
  my ($username,$tty,$day,$month,$date,$time,@junk) = @line_fields;
  next if exists $last_login{$username};
  $last_login{$username} = "$day $month $date $time";
}

my $username;
my $hostname_string = `hostname`;
my $run_date = uc scalar localtime;
my $locgroups;
my $adgroups;
my $last_login;
my $allowed_groups_string = join(" ",keys %allowed_groups);

format STDOUT_TOP =
-------------------------------------------------------------------------------

 USER ACCESS AUDIT REPORT FOR @||||||||||||||||||| ON @>>>>>>>>>>>>>>>>>>>>>>>>
$hostname_string, $run_date

   +------------------------------------------------------------------------+
~  | Users in these groups have access: ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
$allowed_groups_string
~~ |                                    ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
$allowed_groups_string
~  | ^||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| |
$local_users_allowed
   +------------------------------------------------------------------------+

                                    PAGE @<<
$%
-------------------------------------------------------------------------------
Username Directory source      What gives this user access?          Last login
                                Local groups   AD groups
-------------------------------------------------------------------------------
.
format STDOUT =
@<<<<<<< @<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<< @<<<<<<<<<<<<<< @>>>>>>>>>>>>>>>
$username, ${allowed_users{$username}->{"directory source"}}, $locgroups, $adgroups, $last_login{$username}
.

foreach $username (sort keys %allowed_users) {
  my @locgroups = ();
  my @adgroups = ();
  my %seen_groups = ();
  foreach $g (@{$allowed_users{$username}->{"groups"}}) {
    if (exists $seen_groups{$g}) { next; } else { $seen_groups{$g} = 1; }
    if (exists $local_groups{$g}) {
      push(@locgroups,$g);
    } else {
      push(@adgroups,$g);
    }
  }
  $locgroups = join(" ",sort @locgroups);
  $adgroups = join(" ",sort @adgroups);
  if ($locgroups eq "" and $adgroups eq "") { 
    $locgroups = "-";
    $adgroups = "-";
  }
  $last_login{$username} = "(no record)" unless exists $last_login{$username};
  write;
}
