#!/usr/local/bin/perl
#
# $Id: PassPolicyCheck.pl,v 1.12 2005/03/02 22:43:27 rdilley Exp $
#
# author: ron dilley
#
# desc: this perl script verifies that passwords conform to a specified policy
#
# Copyright (C) 2004  Ron A. Dilley
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; version 2
# of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
# ron.dilley@uberadmin.com
#
############################################################################

#
# modules
#
use Getopt::Std;

#
# pragmas
#
use strict;

#
# set environment
#
$ENV{PATH} = "/usr/bin:/bin:/usr/sbin:/sbin:/usr/ucb";

#
# turn on autoflush
#
select STDERR; $| = 1;
select STDOUT; $| = 1;

#
# defines
#
$::TRUE = 1;
$::FALSE = 0;
$::FAILED = -1;

$::VERSION = '$Id: PassPolicyCheck.pl,v 1.12 2005/03/02 22:43:27 rdilley Exp $';
$::PROGNAME = "PassPolicyCheck.pl";

%::Config = ();
$::Config{'debug'} = $::FALSE;

$::MIN_PASS_LEN = 6;

#
# main routine
#
if ( &main() != $::TRUE ) {
  exit( 1 );
}

exit( 0 );

############################################################################
#
# sub-routines
#

#
# main routine
#
sub main {
  my $arg;

  #
  # display script banner
  #
  &show_banner();

  #
  # parse command-line
  #
  &parse_command_line();

  # process args that are left
  while( $arg = shift( @::ARGV ) ) {
    # load crackfile
    &load_crack_report( $arg );
  }

  # done
  return $::TRUE;
}

#
# display banner info
#
sub show_banner {
  print "$::PROGNAME v$::VERSION\n";
  print "By: Ron Dilley\n";
  print "\n";
  print "$::PROGNAME comes with ABSOLUTELY NO WARRANTY.\n";
  print "This is free software, and you are welcome\n";
  print "to redistribute it under certain conditions;\n";
  print "See the GNU General Public License for details.\n";
  print "\n";

  return $::TRUE;
}

#
# display help info
#
sub show_help {
  print "Syntax:\n";
  print "\n";
  print "$::PROGNAME [options] {file} [{file} ...]\n";
  print "\n";
  print "-d {0-9}   Display debug information during program run\n";
  print "-r         Print simple report\n";
  print "-R         Print verbose report as csv\n";
  print "\n";

  return $::TRUE;
}

#
# parse command-line arguments
#
sub parse_command_line {
  no strict 'vars';

  if ( getopts( 'd:rR' ) == $::FALSE ) {
    &show_help();
    return $::FAILED;
  }
  if ( defined $opt_d ) {
    if ( $opt_d > 0 ) {
      # set debug mode
      $::Config{'debug'} = $opt_d;
    }
  }
  if ( defined $opt_R ) {
    # set verbose report mode
    $::Config{'verbose_report'} = $::TRUE;
  }
  if ( defined $opt_r ) {
    # set simple report mode
    $::Config{'simple_report'} = $::TRUE;
  }
  return $::TRUE;
}

#
# load crack report
#
sub load_crack_report {
  my ( $fname ) = @_;
  my $line;
  my $login;
  my $passwd;
  my $gcos;
  my $passwd_weak = 0;
  my $passwd_weak_str;
  my $passwd_short = 0;
  my $passwd_short_str;
  my $passwd_joe = 0;
  my $passwd_joe_str;
  my $passwd_password = 0;
  my $passwd_password_str;
  my $passwd_policy = 0;
  my $passwd_policy_str;
  my $passwd_adm_policy = 0;
  my $passwd_svc_policy = 0;
  my $passwd_gcos = 0;
  my $passwd_gcos_str;
  my $passwd_match_one = 0;
  my $passwd_match_one_str;
  my $passwd_match_two = 0;
  my $passwd_match_two_str;
  my $passwd_match_three = 0;
  my $passwd_match_three_str;
  my $passwd_match_four = 0;
  my $passwd_match_four_str;
  my $tmp_pass;
  my $match;
  my $policy_violation;
  my $group_match_count;
  my $strength;
  my $min_str;
  my $max_str;

  if ( ! defined open( CRACKFILE, $fname ) ) {
    print "ERROR - Unable to open [$fname]\n";
    return $::FAILED;
  }

  if ( $::Config{'report'} ) {
    printf( "Count,Login,PwLength,JoeAccount,IsPassword,GCosMatch,OneType,TwoTypes,ThreeTypes,FourTypes,PolicyViolation\n" );
  }

  while( $line = <CRACKFILE> ) {
    $policy_violation = $::FALSE;
    if ( $line =~ m/^\d+\:Guessed (\S+) \[(\S+)\]\s+(.*)\s+\[.*\].*$/ ) {
      $login = $1;
      $passwd = $2;
      $gcos = $3;
      $gcos =~ s/,/ /g;
      $gcos =~ s/-/ /g;
      $gcos =~ tr/a-z/A-Z/;
      $gcos =~ s/\ [A-Z]\.//g;
      $gcos =~ s/\ [A-Z]$//g;
      $gcos =~ s/\(/\\\(/;
      $gcos =~ s/\)/\\\)/;
    } elsif ( $line =~ m/(.*)\:(.*)\:(.*)\:(.*)\:(.*)\:(.*)\:(.*)$/ ) {
      $login = $1;
      $passwd = $2;
      $gcos = $5;
      $gcos =~ s/,/ /g;
      $gcos =~ s/-/ /g;
      $gcos =~ tr/a-z/A-Z/;
      $gcos =~ s/\ [A-Z]\.//g;
      $gcos =~ s/\ [A-Z]$//g;
      $gcos =~ s/\(/\\\(/;
      $gcos =~ s/\)/\\\)/;
    } else {
      ( $login,$gcos,$passwd ) = split( /\t/, $line );
      chomp( $login );
      chomp( $gcos );
      chomp( $passwd );
      $gcos =~ s/,/ /g;
      $gcos =~ s/-/ /g;
      $gcos =~ tr/a-z/A-Z/;
      $gcos =~ s/\ [A-Z]\.//g;
      $gcos =~ s/\ [A-Z]$//g;
      $gcos =~ s/\(/\\\(/;
      $gcos =~ s/\)/\\\)/;
    }

    if ( length( $passwd ) <= 0 ) {
      next;
    }

    $strength = &score_password( $passwd );
    if ( $strength < $min_str ) {
      $min_str = $strength;
    } elsif ( $strength > $max_str ) {
      $max_str = $strength;
    }

    if ( $::Config{'debug'} >= 3 ) {
      print "DEBUG - login[$login] password[$passwd] strength[$strength]\n";
    }

    #
    # collect statistics
    #

    # count
    $passwd_weak++;
    $passwd_weak_str += $strength;

    if ( $::Config{'verbose_report'} ) {
      printf( "%04d,%s,", $passwd_weak, $login );
    }

    # short password
    if ( length( $passwd ) < $::MIN_PASS_LEN ) {
      if ( $::Config{'debug'} >= 6 ) {
	print "DEBUG - Too short\n";
      }
      $passwd_short++;
      $passwd_short_str += $strength;
      $policy_violation = $::TRUE;
      if ( $::Config{'verbose_report'} ) {
	print "Yes,";
      }
    } elsif ( $::Config{'verbose_report'} ) {
      print ",";
    }

    # joe account
    if ( $login eq $passwd ) {
      if ( $::Config{'debug'} >= 6 ) {
	print "DEBUG - Joe account\n";
      }
      $passwd_joe++;
      $passwd_joe_str += $strength;
      $policy_violation = $::TRUE;
      if ( $::Config{'verbose_report'} ) {
	print "Yes,";
      }
    } elsif ( $::Config{'verbose_report'} ) {
      print ",";
    }

    # 'password' account
    if ( ( $passwd eq 'PASSWORD' ) | ( $passwd eq 'password' ) | ( $passwd eq 'Password' ) ) {
      if ( $::Config{'debug'} >= 6 ) {
	print "DEBUG - Matched password\n";
      }
      $passwd_password++;
      $passwd_password_str += $strength;
      if ( $::Config{'verbose_report'} ) {
	print "Yes,";
      }
    } elsif ( $::Config{'verbose_report'} ) {
      print ",";
    }

    if ( defined $gcos ) {
      # gcos match
      $match = $::FALSE;
      if ( $::Config{'debug'} >= 3 ) {
	print "DEBUG - GCOS [$gcos]\n";
      }
      @_ = split( /\ /, $gcos );
      $tmp_pass = $passwd;
      $tmp_pass =~ tr/a-z/A-Z/;
      foreach $_ ( @_ ) {
	if ( length( $_ ) > 2 ) {
	  if ( $::Config{'debug'} >= 4 ) {
	    print "DEBUG - GCOS[$_] PW[$tmp_pass]\n";
	  }
	  if ( $tmp_pass =~ m/.*$_.*/ ) {
	    $match = $::TRUE;
	  }
	}
      }
      if ( $match == $::TRUE ) {
	if ( $::Config{'debug'} >= 6 ) {
	  print "DEBUG - Matches gcos\n";
	}
	$passwd_gcos++;
	$passwd_gcos_str += $strength;
	$policy_violation = $::TRUE;
	if ( $::Config{'verbose_report'} ) {
	  print "Yes,";
	}
      } elsif ( $::Config{'verbose_report'} ) {
	print ",";
      }
    } elsif ( $::Config{'verbose_report'} ) {
      print ",";
    }

    $group_match_count = 0;

    # group 1 match
    if ( $passwd =~ m/.*[A-Z].*/ ) {
      $group_match_count++;
    }

    # group 2 match
    if ( $passwd =~ m/.*[a-z].*/ ) {
      $group_match_count++;
    }

    # group 3 match
    if ( $passwd =~ m/.*\d.*/ ) {
      $group_match_count++;
    }

    # group 4 match
    if ( $passwd =~ m/.*[^a-zA-Z0-9].*/ ) {
      $group_match_count++;
    }

    if ( $group_match_count == 1 ) {
      if ( $::Config{'debug'} >= 6 ) {
	print "DEBUG - Has one type\n";
      }
      $passwd_match_one++;
      $passwd_match_one_str += $strength;
      $policy_violation = $::TRUE;
      if ( $::Config{'verbose_report'} ) {
	print "Yes,,,,";
      }
    } elsif ( $group_match_count == 2 ) {
      if ( $::Config{'debug'} >= 6 ) {
	print "DEBUG - Has two types\n";
      }
      $passwd_match_two++;
      $passwd_match_two_str += $strength;
      $policy_violation = $::TRUE;
      if ( $::Config{'verbose_report'} ) {
	print ",Yes,,,";
      }
    } elsif ( $group_match_count == 3 ) {
      if ( $::Config{'debug'} >= 6 ) {
	print "DEBUG - Has three types\n";
      }
      $passwd_match_three++;
      $passwd_match_three_str += $strength;
      if ( $::Config{'verbose_report'} ) {
	print ",,Yes,,";
      }
    } elsif ( $group_match_count == 4 ) {
      if ( $::Config{'debug'} >= 6 ) {
	print "DEBUG - Has four types\n";
      }
      $passwd_match_four++;
      $passwd_match_four_str += $strength;
      if ( $::Config{'verbose_report'} ) {
	print ",,,Yes,";
      }
    }

    if ( $policy_violation == $::TRUE ) {
      if ( $::Config{'debug'} >= 6 ) {
	print "DEBUG - Fails policy check\n";
      }
      $passwd_policy++;
      $passwd_policy_str += $strength;

      if ( $login =~ m/^svc-.*/ ) {
	$passwd_svc_policy++;
      } elsif ( $login =~ m/.*-adm$/ ) {
	$passwd_adm_policy++;
      } elsif ( $login =~ m/.*-admn$/ ) {
	$passwd_adm_policy++;
      } elsif ( $login =~ m/.*-admin$/ ) {
	$passwd_adm_policy++;
      }
      if ( $::Config{'verbose_report'} ) {
	printf( "Yes\n" );
      }
      if ( $::Config{'simple_report'} ) {
	print "$login fails policy check\n";
      }
    } elsif ( $::Config{'verbose_report'} ) {
      printf( "\n" );
    }
  }

  close( CRACKFILE );

  if ( $passwd_weak <= 0 ) {
    return $::FAILED;
  }

  if ( ! $::Config{'verbose_report'} & ! $::Config{'simple_report'} ) {
    printf( "Guessed Passwords: %i (avg str: %0.2f)\n", $passwd_weak, $passwd_weak_str / $passwd_weak );
    print "Password Strength: min ($min_str) max ($max_str)\n";
    print "==============================================\n";
    if ( $passwd_short > 0 ) {
      printf( "                    Passwords too short: %-4i (avg str: %0.2f)\n", $passwd_short, $passwd_short_str / $passwd_short );
    }
    if ( $passwd_joe > 0 ) {
      printf( "                  Passwords match login: %-4i (avg str: %0.2f)\n", $passwd_joe, $passwd_joe_str / $passwd_joe );
    }
    if ( $passwd_password ) {
      printf( "             Passwords match \'password\': %-4i (avg str: %0.2f)\n", $passwd_password, $passwd_password_str / $passwd_password );
    }
    if ( defined $gcos ) {
      if ( $passwd_gcos > 0 ) {
	printf( "             Passwords contain username: %-4i (avg str: %0.2f)\n", $passwd_gcos, $passwd_gcos_str / $passwd_gcos );
      }
    }
    if ( $passwd_match_one > 0 ) {
      printf( "   Passwords contain one character type: %-4i (avg str: %0.2f)\n", $passwd_match_one, $passwd_match_one_str / $passwd_match_one );
    }
    if ( $passwd_match_two > 0 ) {
      printf( "   Passwords contain two character type: %-4i (avg str: %0.2f)\n", $passwd_match_two, $passwd_match_two_str / $passwd_match_two );
    }
    if ( $passwd_match_three > 0 ) {
       printf( " Passwords contain three character type: %-4i (avg str: %0.2f)\n", $passwd_match_three, $passwd_match_three_str / $passwd_match_three );
    }
    if ( $passwd_match_four > 0 ) {
      printf( "  Passwords contain four character type: %-4i (avg str: %0.2f)\n", $passwd_match_four, $passwd_match_four_str / $passwd_match_four );
    }
    print "----------------------------------------------\n";
    if ( $passwd_policy > 0 ) {
      printf( "      Total passwords fail policy check: %-4i (avg str: %0.2f)\n", $passwd_policy, $passwd_policy_str / $passwd_policy );
    }
    if ( $passwd_adm_policy > 0 ) {
      print "      Admin Passwords fail policy check: $passwd_adm_policy\n";
    }
    if ( $passwd_svc_policy > 0 ) {
      print "    Service Passwords fail policy check: $passwd_svc_policy\n";
    }
  }

  return $::TRUE;
}

#
# score password
#
sub score_password {
  my ( $passwd ) = @_;
  my $strength;
  my $lc;
  my $c;
  my $count;
  my $max_a_run;
  my $a_run;
  my $max_n_run;
  my $n_run;
  my @tokens;

  $strength = 0;
  $lc = '';
  $count = 0;
  $a_run = 0;
  $max_a_run = 0;
  $n_run = 0;
  $max_n_run = 0;
  # break up the password into characters
  @tokens = split( //, $passwd );
  foreach $c ( @tokens ) {
    $count++;
    if ( $c =~ m/[A-Z]/ ) { # uppercase alpha
      if ( $lc eq 'l' | $lc eq 'd' | $lc eq 's' ) {
	# new type
	$strength += 2;
	if ( $max_n_run < $n_run ) {
	  $max_n_run = $n_run;
	}
	$n_run = 0;
      } else {
	$strength++;
      }
      $a_run++;
      $lc = 'u';
    } elsif ( $c =~ m/[a-z]/ ) { # lowercase alpha
      if ( $lc eq 'u' | $lc eq 'd' | $lc eq 's' ) {
	# new type
	$strength += 2;
	if ( $max_n_run < $n_run ) {
	  $max_n_run = $n_run;
	}
	$n_run = 0;
      } else {
	$strength++;
      }
      $a_run++;
      $lc = 'l';
    } elsif ( $c =~ m/\d/ ) { # decimal
      if ( $count == 1 ) {
	# number first, penalty
	$strength++;
      } elsif ( $count >= scalar( @tokens ) ) {
	# number last, penalty
	# don't increment stength
      } elsif ( $lc eq 'u' | $lc eq 'l' | $lc eq 's' ) {
	# new type
	$strength += 3;
	if ( $max_a_run < $a_run ) {
	  $max_a_run = $a_run;
	}
	$a_run = 0;
      } else {
	$strength++;
      }
      $n_run++;
      $lc = 'd';
    } elsif ( $c =~ m/[^a-zA-Z0-9]/ ) { # special
      if ( $count == 1 ) {
	# special character first, penalty
	$strength += 3;
      } elsif ( $count >= scalar( @tokens ) ) {
	# special character last, penalty
	$strength += 2;
      } elsif ( $lc eq 'u' | $lc eq 'l' | $lc eq 'd' ) {
	# new type
	$strength += 5;
	if ( $max_a_run < $a_run ) {
	  $max_a_run = $a_run;
	}
	$a_run = 0;
	if ( $max_n_run < $n_run ) {
	  $max_n_run = $n_run;
	}
	$n_run = 0;
      } else {
	$strength += 4;
      }
      $lc = 's';
    }
    if ( $::Config{'debug'} >= 5 ) {
      print "DEBUG - tok: $c c: $count str: $strength\n";
    }
  }
  # closeout token run counts
  if ( $max_a_run < $a_run ) {
    $max_a_run = $a_run;
  }
  if ( $max_n_run < $n_run ) {
    $max_n_run = $n_run;
  }
  if ( ( $max_a_run * 2 ) > $count ) {
    # too many alphas in a row, penalty
    $strength--;
  }
  if ( ( $max_n_run * 2 ) > $count ) {
    # too many nums in a row, penaly
    $strength -= 2;
  }
  # special formatting penalties
  if ( $passwd =~ m/^[a-z]+$/ ) {
    # aaa penalty
    $strength -= 4;
  }
  if ( $passwd =~ m/^[A-Z]+$/ ) {
    # AAA penalty
    $strength -= 4;
  }
  if ( $passwd =~ m/^[0-9]+$/ ) {
    # 000 penalty
    $strength -= 4;
  }
  if ( $passwd =~ m/^[a-zA-z]+[0-9]+$/ ) {
    # blah123 penalty
    $strength -= 3;
  }
  if ( $passwd =~ m/^[0-9]+[a-zA-z]+$/ ) {
    # 123blah penalty
    $strength -= 2;
  }
  if ( $passwd =~ m/^[a-zA-Z]+[^a-zA-z0-9]+$/ ) {
    # blah%^& penalty
    $strength -= 2;
  }
  if ( length( $passwd ) <= 4 ) {
    # too short, penalty
    $strength--;
  }
  @tokens = ();

  return $strength;
}
