#! /usr/local/bin/perl

#
# kuang - rule based analysis of Unix security
#
# Perl version by Steve Romig of the CIS department, The Ohio State
# University, October 1990. 
# 
# Based on the shell script version by Dan Farmer from his COPS
# package, which in turn is based on a shell version by Robert
# Baldwin. 
#

do 'yagrip.pl' ||
  die "can't do yagrip.pl";

$options = "vdlf:D";
$usage = "usage: kuang [-v] [-d] [-l] [-D] [-f filedata] [u.username|g.groupname]\n";

#
# Simple Unix Kuang, a security checking program.
#
# This is a perl version of Dan Farmer's version of Bob Baldwin's
# shell scripts. 
#

#
# passwd_byuid lookup a password entry by uid, return as 
# (name, password, directory, shell) list.
#
sub passwd_byuid {
    local($uid) = @_;
    local($name, $passwd, $gid, $quota, $comment, $gcos, $dir, $shell);

    if (! defined($passwd_byuid_list{$uid})) {
	($name, $passwd, $t_uid, $gid, $quota, $comment, $gcos, $dir, $shell) =
	    getpwuid($uid);

	if ($t_uid eq "") {
	    return();
	}

	$passwd_byuid_list{$uid} = join(':', $name, $passwd, $dir, $shell);
	$passwd_byname_list{$name} = $uid;
    }

    return(split(/:/, $passwd_byuid_list{$uid}));
}

sub passwd_byname {
    local($name) = @_;
    local($passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell);

    if (! defined($passwd_byname_list{$name})) {
	($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) =
	    getpwnam($name);

	if ($uid eq "") {
	    return();
	}

	$passwd_byuid_list{$uid} = join(':', $name, $passwd, $dir, $shell);
	$passwd_byname_list{$name} = $uid;
    }

    return($passwd_byname_list{$name});
}

#
# group_bygid lookup a group entry by gid, return as 
# (name, members) list.
#
sub group_bygid {
    local($gid) = @_;
    local($name, $t_passwd, $t_gid, $members);

    if (! defined($group_bygid_list{$gid})) {
	($name, $t_passwd, $t_gid, $members) = getgrgid($gid);

	if ($t_gid eq "") {
	    return();
	}

	$group_bygid_list{$gid} = join(':', $name, $members);
	$group_byname_list{$name} = $gid;
    }

    return(split(/:/, $group_bygid_list{$gid}));
}

sub group_byname {
    local($name) = @_;
    local($gname, $passwd, $gid, $members);

    if (! defined($group_byname_list{$name})) {
	($gname, $passwd, $gid, $members) = getgrnam($name);

	if ($gid eq "") {
	    printf(stderr "A group named '$name' does not exist!\n");
	    exit(1);
        }

	$group_bygid_list{$gid} = join(':', $name, $members);
	$group_byname_list{$name} = $gid;
    }

    return($group_byname_list{$name});
}

#
# Do various initialization type things.
#

sub init_kuang {
    local($which, $name, $uid, $gid);
    local($f_type, $f_uid, $f_gid, $f_mode, $f_name);
    local($count);

    $which = "u";
    $name = "root";

    #
    # Deal with args...
    #

    &getopt($options) ||
      die $usage;

    if ($#ARGV == 0) {
	($which, $name) = split(/\./, $ARGV[0]);

	if ($name eq "") {
	    $name = $which;
	    $which = "u";
	}

	if ($which ne "u" && $which ne "g") {
	    printf(stderr "target must be given as u.user or g.group\n");
	    exit(1);
	}
    } elsif ($#ARGV > 0) {
	printf(stderr $usage);
	exit(1);
    }

    #
    # Preload the file data...
    #
    if (defined($opt_f)) {
	#
	# If we are dumping the file data to a DBM file, nuke the existing 
	# ones and open the dbm file.  Otherwise, open the DBM file
 	# only if they exist. 
	#
	$read_from_file = 1;

	if (defined($opt_D)) {	
	    unlink("$opt_f.dir", "$opt_f.pag");

	    dbmopen(files, $opt_f, 0644) ||
	      die sprintf("can't open DBM file '%s'", $opt_f);
	} elsif (-f "$opt_f.dir") {
	    dbmopen(files, $opt_f, 0644) ||
	      die sprintf("can't open DBM file '%s'", $opt_f);

	    $read_from_file = 0;
	}

	if ($read_from_file) {
	    open(FILEDATA, $opt_f) || 
	      die sprintf("kuang: can't open '%s'", $opt_f);

	    $count = 0;
	    while (<FILEDATA>) {
		$count++;

		chop;
		($f_type, $f_uid, $f_gid, $f_mode, $f_name) = split;

		if ($count % 1000 == 0) {
		    printf("line $count, reading entry for $f_name\n");
		}
		$files{$f_name} = join(' ', $f_uid, $f_gid, $f_mode);
	    }

	    close(FILEDATA);
	}
    }

    if (defined($opt_D)) {
	dbmclose(files);

	exit(0);
    }
if (defined($opt_v)) {
    printf("done with files\n");
    }
    #
    # Need some of the password and group stuff.  Suck in passwd and 
    # group info, store by uid and gid in an associative array of strings
    # which consist of fields corresponding to the passwd and group file 
    # entries (and what the heck, we'll use : as a delimiter also...:-)
    #

    $passwd_byuid_list{-1} = "OTHER:::";	# add an entry for OTHER
    $passwd_byname_list{"OTHER"} = -1;

    $uids_known{-1} = "";			# we can access OTHER
    %uids_new = ();

    %gids_known = ();
    %gids_new = ();

    %files_new = ();

    #
    # Set up initial goal: become target user or group
    #
    if ($which eq "u") {
	$uid = &passwd_byname($name);
	if ($uid ne "") {
	    &addto("uids", $uid, "grant u.$name do anything");
	} else {
	    printf(stderr "There is no user with username '$name'.\n");
	    exit(1);
	}
    } else {
	$gid = &group_byname($name);
	if ($gid ne "") {
	    &addto("gids", $gid, "grant g.$name");
	} else {
	    printf(stderr "There is no group named '$name'.\n");
	    exit(1);
	}
    }
}

#
# Get the home directory for this UID from the passwd file cache.
#
sub gethome {
    local($uid) = @_;
    local($tmp, $home);

    ($tmp, $tmp, $home, $tmp) = &passwd_byuid($uid);
    return($home);
}

#
# Get the writers of the named file - return as (UID, GID, OTHER)
# triplet.  Owner can always write, since he can chmod the file if he
# wants. 
#
# (fixme) are there any problems in this sort of builtin rule?  should
# we make this knowledge more explicit?
#
sub filewriters {
    local($name) = @_;
    local($tmp, $mode, $uid, $gid, $other);
    
    #
    # Check the file cache - avoid disk lookups for performance and 
    # to avoid shadows...
    #
    if (defined($files{$name})) {
	$cache_hit++;
	
	($uid, $gid, $mode) = split(/ /, $files{$name});
	$mode = oct($mode);
    } else {
	$cache_miss++;

	if (! -e $name && $read_from_file) {
	    $files{$name} = "";
	    return;
	}

	($tmp,$tmp,$mode,$tmp,$uid,$gid) = stat(_);
	if ($read_from_file) {
	    $files{$name} = join(' ', $uid, $gid, $mode);
	}
    }

    if (($mode & 020) != 020) {
	$gid = "";
    }
    
    if (($mode & 02) == 02) {
	$other = 1;
    } else {
	$other = 0;
    }

    return($uid, $gid, $other);
}

#
# return # of entries in given associative array.
#
sub sizeof {
    local(*which) = @_;
    local(@keywords);

    @keywords = keys %which;
    return($#keywords + 1);
}

#
# return appropriate entry from named associative array of given type.
# returns a (key, value) pair - if key is "", there was no entry.
#
sub getentry {
    local($which, $type, $key) = @_;
    local($newkey, $value);

    $newkey = "";
    $value = "";

    which: {
	if ($which eq "uids") {
	    type0: {
	        if ($type eq "known") {
		    if (defined($uids_known{$key})) {
		        $newkey = $key;	$value = $uids_known{$key};
		    }
	            last type0;
	        }

	        if ($type eq "new") {
		    if (defined($uids_new{$key})) {
		        $newkey = $key;	$value = $uids_new{$key};
		    }
	            last type0;
	        }

	        if ($type eq "pending") {
		    if (defined($uids_pending{$key})) {
		        $newkey = $key;	$value = $uids_pending{$key};
		    }
	            last type0;
	        }

	        if ($type eq "old") {
		    if (defined($uids_old{$key})) {
		        $newkey = $key;	$value = $uids_old{$key};
		    }
	            last type0;
	        }

	        printf(stderr "kuang: fatal error in getentry: type is wrong (%s)\n", 
			$type);
	        exit(1);
	    }

	    last which;
        }

	if ($which eq "gids") {
	    type1: {
	        if ($type eq "known") {
		    if (defined($gids_known{$key})) {
		        $newkey = $key;	$value = $gids_known{$key};
		    }
	            last type1;
	        }

	        if ($type eq "new") {
		    if (defined($gids_new{$key})) {
		        $newkey = $key;	$value = $gids_new{$key};
		    }
	            last type1;
	        }

	        if ($type eq "pending") {
		    if (defined($gids_pending{$key})) {
		        $newkey = $key;	$value = $gids_pending{$key};
		    }
	            last type1;
	        }

	        if ($type eq "old") {
		    if (defined($gids_old{$key})) {
		        $newkey = $key;	$value = $gids_old{$key};
		    }
	            last type1;
	        }

	        printf(stderr "kuang: fatal error in getentry: type is wrong (%s)\n", 
			$type);
	        exit(1);
	    }

	    last which;
	}

	if ($which eq "files") {
	    type2: {
	        if ($type eq "known") {
		    if (defined($files_known{$key})) {
		        $newkey = $key;	$value = $files_known{$key};
		    }
	            last type2;
	        }

	        if ($type eq "new") {
		    if (defined($files_new{$key})) {
		        $newkey = $key;	$value = $files_new{$key};
		    }
	            last type2;
	        }

	        if ($type eq "pending") {
		    if (defined($files_pending{$key})) {
		        $newkey = $key;	$value = $files_pending{$key};
		    }
	            last type2;
	        }

	        if ($type eq "old") {
		    if (defined($files_old{$key})) {
		        $newkey = $key;	$value = $files_old{$key};
		    }
	            last type2;
	        }

	        printf(stderr "kuang: fatal error in getentry: type is wrong (%s)\n", 
			$type);
	        exit(1);
	    }

	    last which;
	}

	printf(stderr "kuang: fatal error in getentry: which is wrong (%s)\n",
		$which);
	exit(1);
    }

    return($newkey, $value);
}


#
# stores a (key, value) in the associative array of the given type.
#
sub putentry {
    local($which, $type, $key, $value) = @_;

    which: {
	if ($which eq "uids") {
	    type0: {
	        if ($type eq "known") {
		    $uids_known{$key} = $value;	last type0;
	        }

	        if ($type eq "new") {
		    $uids_new{$key} = $value;	last type0;
	        }

	        if ($type eq "pending") {
		    $uids_pending{$key} = $value;	last type0;
	        }

	        if ($type eq "old") {
		    $uids_old{$key} = $value;	last type0;
	        }

	        printf(stderr "kuang: fatal error in putentry: type is wrong (%s)\n", 
			$type);
	        exit(1);
	    }

	    last which;
        }

	if ($which eq "gids") {
	    type1: {
	        if ($type eq "known") {
		    $gids_known{$key} = $value;	last type1;
	        }

	        if ($type eq "new") {
		    $gids_new{$key} = $value;	last type1;
	        }

	        if ($type eq "pending") {
		    $gids_pending{$key} = $value;	last type1;
	        }

	        if ($type eq "old") {
		    $gids_old{$key} = $value;	last type1;
	        }

	        printf(stderr "kuang: fatal error in putentry: type is wrong (%s)\n", 
			$type);
	        exit(1);
	    }

	    last which;
	}

	if ($which eq "files") {
	    type2: {
	        if ($type eq "known") {
		    $files_known{$key} = $value;	last type2;
	        }

	        if ($type eq "new") {
		    $files_new{$key} = $value;	last type2;
	        }

	        if ($type eq "pending") {
		    $files_pending{$key} = $value;	last type2;
	        }

	        if ($type eq "old") {
		    $files_old{$key} = $value;	last type2;
	        }

	        printf(stderr "kuang: fatal error in putentry: type is wrong (%s)\n", 
			$type);
	        exit(1);
	    }

	    last which;
	}

	printf(stderr "kuang: fatal error in putentry: which is wrong (%s)\n",
		$which);
	exit(1);
    }
}


sub addto {
    local($which, $key, $plan) = @_;
    local($tkey, $tvalue);

    #
    # See whether there's an entry for $key in the known list for the
    # $which array.  If so - success, we've found a suitable breakin
    # path. 
    #

    ($tkey, $tvalue) = &getentry($which, "known", $key);
    if ($tkey eq $key) {
	printf("Success! $key $plan\n");
	return;
    }

    #
    # Check to see if its a duplicate - if so, don't need to do anything.
    #
    ($tkey, $tvalue) = &getentry($which, "pending", $key);
    if ($tkey eq $key) {
	return;
    }

    #
    # Add to pending list for $which...
    #
    &putentry($which, "pending", $key, $plan);

    #
    # Add to next goal list for $which...
    #
    &putentry($which, "new", $key, $plan);

    if (defined($opt_v)) {
	printf("addto: $which --> $plan\n");
    }

    #
    # If this is a uid goal, then add the plan to the accessible list.
    #
    if ($which eq "uids" && $key ne "0" && defined($opt_l)) {
	$accessible{$plan} = "1";
    }
}

#
#----------------------------------------------------------------------
#Main program follows...initialize and loop till we're done.
#

&init_kuang();

#
# While there's still something to pursue...
#
while (&sizeof(*uids_new) != 0 || 
       &sizeof(*gids_new) != 0 || 
       &sizeof(*files_new) != 0) {

    #
    # Deal with uids first...
    #
    if (&sizeof(*uids_new) != 0) {
        %uids_old = %uids_new;
        %uids_new = ();

        foreach $uid (keys %uids_old) {
	    $plan = $uids_old{$uid};

	    if (defined($opd_d)) {
		printf("uids evel: $uid '$plan'\n");
	    }
            
            &addto("files", "/etc/passwd", "replace /etc/passwd $plan");
            &addto("files", "/usr/lib/aliases", "replace /usr/lib/aliases $plan");

	    #
	    # Add controlling files for this user.  There are probably 
	    # others (such as .logout, X tool start things and so on).
	    # (fixme) add other CF's...
	    #
	    $home = &gethome($uid);

	    if ($home ne "") {
		if ($home eq "/") {
		    $home = "";
		}

		if (-e "$home/.rhosts") {
		    &addto("files", "$home/.rhosts", "write $home/.rhosts $plan");
		}

		if (-e "$home/.login") {
		    &addto("files", "$home/.login", "replace $home/.login $plan");
		}

		if (-e "$home/.logout") {
		    &addto("files", "$home/.logout", "replace $home/.logout $plan");
		}

		if (-e "$home/.cshrc") {
		    &addto("files", "$home/.cshrc", "replace $home/.cshrc $plan");
		}

		if (-e "$home/.profile") {
		    &addto("files", "$home/.profile", "replace $home/.profile $plan");
		}
	    }

	    #
	    # Controlling files for root...
	    #
	    if ($uid+0 == 0) {
		foreach $file ("/etc/rc", "/etc/rc.boot", "/etc/rc.single", "/etc/rc.config", "/etc/rc.local", "/usr/lib/crontab", "/usr/spool/cron/crontabs") {
		    if (-e $file) {
			&addto("files", $file, "replace $file $plan");
		    }
		}
	    }

	    if ($uid+0 != 0) {
		&addto("files", "/etc/hosts.equiv", "replace /etc/hosts.equiv allow rlogin $plan");

		if (-s "/etc/hosts.equiv") {
		    &addto("files", "/etc/hosts", "replace /etc/hosts fake hostaddress allow rlogin $plan");
		}
	    }
	}
    }

    #
    # Deal with groups...
    #
    if (&sizeof(*gids_new) != 0) {
        %gids_old = %gids_new;
        %gids_new = ();

bar_loop:
        foreach $gid (keys %gids_old) {
	    $plan = $gids_old{$gid};
	    if (defined($opt_d)) {
		printf("gids eval: $gid '$plan'\n");
	    }
            
	    ($gname, $members) = &group_bygid($gid);
	    if ($gname eq "") {
		printf("There is no group with gid $gid.\n");
		next bar_loop;
	    }

foo_loop:
	    foreach $uname (split(/[ \t\n]+/, $members)) {
		$uid = &passwd_byname($uname);

		if ($uid eq "") {
		    printf(stderr "Group $gname has an unknown user $uname\n");
		    next foo_loop;
		}

		&addto("uids", "$uid", "grant u.$uname $plan");
	    }

	    &addto("files", "/etc/group", "replace /etc/group $plan");
	}
    }

    #
    # Deal with files...
    #
    if (&sizeof(*files_new) != 0) {
        %files_old = %files_new;
        %files_new = ();

file_loop:
        foreach $file (keys %files_old) {
	    $plan = $files_old{$file};
	    ($mode) = split(/[ \t\n]+/, $plan);

	    if (defined($opt_d)) {
		printf("files eval: $file '$plan'\n");
	    }

	    ($owner, $group, $other) = &filewriters($file);

	    if ($owner eq "") {
		printf("%s does not exist\n", $file);
		next file_loop;
	    }

	    ($uname) = &passwd_byuid($owner);
	    if ($uname eq "") {
		$uname = $owner;
	    }

	    &addto("uids", $owner, "grant u.$uname $plan");

	    if ($group ne "") {
		($gname, $tmp) = &group_bygid($group);

		if ($gname ne "") {
		    &addto("gids", $group, "grant g.$gname $plan");
		} else {
		    printf(stderr "There is no group with gid $group.\n");
		}
	    }

	    if ($other) {
		&addto("uids", -1, "grant u.OTHER $plan");
	    }

	    if ($mode eq "replace") {
		$parent = $file;
		$parent =~ s|/[^/]*$||;		# strip last / and remaining

		if ($parent eq "") {		# if nothing left, use /
		    $parent = "/";
		}

		if ($parent ne $file) {		# since $file might've been /
		    &addto("files", $parent, "replace $parent $plan");
		}
	    }
	}
    }
}

if (defined($opt_l)) {
    foreach $key (keys %accessible) {
	printf("$key\n");
    }
}

if (defined($opt_v) || $cache_hit) {
    printf("File info cache hit/access ratio: %g\n", 
            $cache_hit / ($cache_hit + $cache_miss));
    }
