#
# perl/targets.pl
#
# get_proximity(target) returns proximity by hostname.
#
# new_target(target, proximity) figures out what (still) needs to be done.
#
# save_all_hosts(path) saves the all_hosts table to file.
#
# read_all_hosts(path) reads the all_hosts table from file.
#
# drop_all_hosts(host) forget this host.
#
# merge_all_hosts(path) merge with in-core tables.
#
# set_host_time(host) set host access time (UTC).
#
# get_host_time(host) get host access time (UTC).
#
# version 3, Mon Mar 20 19:43:59 1995, last mod by wietse
#

require 'perl/policy-engine.pl';
require 'perl/subnets.pl';
require 'perl/domains.pl';
require 'perl/get_host.pl';
require 'perl/shell.pl';

#
# Get proximity for this host.
#
sub get_proximity
{
	local($target) = @_;

	return 100 unless defined($all_hosts{$target});
	return $all_hosts{$target}{'proximity'};
}

#
# Set up a list of primary targets. Primaries are special. They may be
# expanded. After all the primaries are known we may have to do something
# special, like forgetting everything we know about them.
#
sub add_primary_target {
	local($target) = @_;

	# Do we expand or do we scan a host.
	if ($target =~ /^\d+\.\d+\.\d+$/ || 
	    $target =~ /\d+\.\d+\.\d+-\d+\.\d+\.\d+/ ||
	    $attack_proximate_subnets) {
		print "Add-primary: expand $target...\n" if $debug;
		&target_acquisition($target, 0, $attack_level);
	} else {
		print "Add-primary: $target\n" if $debug;
		&new_target($target, 0);
	}
}

#
# Forget everything we know about primary targets (well, almost everything).
# We must re-read the data base, or memory would be polluted with stale
# inferences.
#
sub fix_primary_targets {
	local(%temp_targets);

	if ($primaries_deleted) {
		print "Primaries being rescanned, rebuilding tables.\n" if $debug;
		%temp_targets = %new_targets;
		&save_saint_data();
		&read_saint_data();
		%new_targets = %temp_targets;
		$primaries_deleted = 0;
	}
}

#
# Take a new potential target and let it cool down. This saves lots of
# duplicate work.
#
sub new_target
{
	local($target, $proximity) = @_;

	return unless $target;

	# Make sure all current primaries are rescanned, not the hosts that
	# were primaries during some earlier scan.
	if ($proximity == 0) {
		$primaries_deleted = defined($all_hosts{$target});
		drop_old_todos($target);
		drop_old_facts($target);
	}

	# Toss circular paths.
	if (defined($all_hosts{$target})
	    && $all_hosts{$target}{'proximity'} < $proximity) {
		return;
	}
	if (exists($new_targets{$target}) 
	    && $new_targets{$target} <= $proximity) {
		return;
	}

	# Keep primary, keep shortest path.
	if ($proximity == 0 || !exists($new_targets{$target}) 
	    || !defined($all_hosts{$target}) 
	    || $all_hosts{$target}{'proximity'} > $proximity) {
		$new_targets{$target} = $proximity;
		print "Add-target: $target prox $proximity\n" if $debug;
	}
}

#
# Probe targets.
#
sub process_targets
{
local($target, $proximity, $level, %temp_targets, %probe_targets);

while(sizeof(*new_targets) > 0) {
	%temp_targets = %new_targets;
	%new_targets = ();
	%probe_targets = ();

	# Generate probes according to policy restrictions.
	for $target (keys %temp_targets) {
		$proximity = $temp_targets{$target};

		# Instantiate new host.
		if (!defined($all_hosts{$target})) {
			&add_new_host($target, $proximity);
			}

		# Proximity may have changed.
		if ($all_hosts{$target}{'proximity'} > $proximity) {
			$all_hosts{$target}{'proximity'} = $proximity;
			}

		# Skip attack and target expansion if this host is off-limits.
		if (($level = &policy_engine($target, $proximity)) < 0) {
			print "process_targets: skip $target...\n" if $debug;
			next;
			}

		# Attack level contents may have changed.
		$all_hosts{$target}{'attack'} = $level;
		$check_alive_list .= "$target "
			if !defined($host_is_alive{$target});
		$probe_targets{$target} = $level;
		}
	}

	# Parallelize liveness checks.
	&check_alive();

	# Generate the probes.
	for $target (keys %probe_targets) {
		print "process_targets: probe $target...\n" if $debug;
		&target_attack($target, $probe_targets{$target});
	}
}

#
# Add one target to the %all_hosts list. Set proximity, IP, preliminary
# attack level, preliminary expansion flag.
#
sub add_new_host
{
local($target, $proximity) = @_;

$all_hosts{$target}{'proximity'} = $proximity;
$all_hosts{$target}{'attack'} = -1;
$all_hosts{$target}{'expand'} = 0;
$all_hosts{$target}{'IP'} = &get_host_addr($target);
$subnet_flag = 0;
$domain_flag = 0;
}

#
# ping a list of hosts and find out what hosts are alive.
#
sub check_alive {
	local ($host);

	if ($check_alive_list eq "") {
		return;
	}
	print "Check-pulse: $check_alive_list\n" if $debug;

	#
	# Cheat when ICMP is broken.
	#
	if ($dont_use_ping || $firewall_flag) {
		for $host (split(/\s+/, $check_alive_list)) {
			$host_is_alive{$host} = 1;
			$live_hosts++;
		}
		$check_alive_list = "";
		return;
	}

	#
	# Fping or bust.
	#
	&open_cmd(FPING, $long_timeout, "$FPING $check_alive_list") 
		|| die "cannot run $FPING: $!\n";
	while(<FPING>) {
		if (/(\S+) is alive/) {
			$host_is_alive{$1} = 1;
			$live_hosts++;
		} elsif ($debug && /(\S+) is unreachable/) {
			print "Skipping dead host - $1\n";
		}
	}
	close(FPING);
	$check_alive_list = "";
}

#
# target attack; assign attacks, throw into todo queue
#
sub target_attack
{
local($target, $level) = @_;

if ($host_is_alive{$target}) {
	print "Prox: $all_hosts{$target}{'proximity'}\n" if $debug;
	print "AL  : $all_hosts{$target}{'attack'}\n" if $debug;
	print "ALC : $all_hosts{$parent}{'attack'}\n" if $parent && $debug;

	for (@{$all_attacks[$level]}) {
		&add_todo($target, $_, "") if ! /\?/;
		}
	}
}

#
# target acq; get subnets
#
sub target_acquisition
{
local($target, $proximity, $level) = @_;
local($targets_found);

# Expand and then collect. Pass results through new_target() for
# consistent handling of constraints and policies.
if ($firewall_flag) {
  $ret = &open_cmd (TARGETS, 1000, "$GET_TARGETS $target FW");
} else {
  $ret = &open_cmd (TARGETS, 120, "$GET_TARGETS $target");
}
while (<TARGETS>) {
	chop;
	$target_addr = &get_host_addr($_);
	$target_name = &getfqdn($target_addr);
	$_ = $target_name
	    if &get_host_addr($target_name) eq $target_addr;
	if (!defined($all_hosts{$_})) {
		&add_new_host($_, $proximity);
		}
	$all_hosts{$_}{'proximity'} = $proximity 
		if ($all_hosts{$_}{'proximity'} > $proximity);
	$all_hosts{$_}{'expand'} = 1;
	$host_is_alive{$_} = 1;
	$live_hosts++;
	&new_target($_, $proximity);
	$targets_found++;
	}
close(TARGETS);
die "$GET_TARGETS failed - unable to expand subnet $target\n" 
	if ($ret < 1 || $targets_found < 1)
}

#
# Save the %all_hosts array
#
sub save_all_hosts {
local($all_hosts_file) = @_;
local($record, $host, $ip, $proximity, $level, $expand, $time);

open(ALL_HOSTS, ">$all_hosts_file") ||
	die "Can't open $all_hosts_file (cache for all-hosts) for writing\n";

for $host (keys %all_hosts) {
	$ip = $all_hosts{$host}{'IP'};
	$proximity = $all_hosts{$host}{'proximity'};
	$level = $all_hosts{$host}{'attack'};
	$expand = $all_hosts{$host}{'expand'};
	$time = $all_hosts{$host}{'time'};
	$record = "$host|$ip|$proximity|$level|$expand|$time";
	print ALL_HOSTS "$record\n";
	# print "save_all_hosts: $record\n" if $debug;
	}
close(ALL_HOSTS);
 
}

#
# Reset host info and derivatives
#
sub clear_all_hosts {
	%all_hosts = ();
	%new_targets = ();
	&clear_subnet_info();
	&clear_domain_info();
}

#
# suck in the all-hosts array from file
#
sub read_all_hosts {
	local($all_hosts_file) = @_;

	&clear_all_hosts();
	&merge_all_hosts($all_hosts_file);
}

#
# merge the in-core all-hosts array with one from file
#
sub merge_all_hosts {
local($all_hosts_file) = @_;
local($count, $host, $ip, $proximity, $level, $time);

open(ALL_HOSTS, $all_hosts_file) ||
	die "Can't open $all_hosts_file (cache for all-hosts) for reading\n";
	print "Reading all hosts info from $all_hosts_file...\n" if $debug;

# read one line at a time, herky jerky motion... one trick pony, man...
while (<ALL_HOSTS>) {
	chop;
	$count = ($host, $ip, $proximity, $level, $expand, $time) = split(/\|/);
	if ($count != 6) {
		warn "corrupted $all_hosts_file: $_\n";
		next;
		}
	if (!defined($all_hosts{$host}{'proximity'})
	    || $all_hosts{$host}{'proximity'} > $proximity) {
		$all_hosts{$host}{'IP'} = $ip;
		$all_hosts{$host}{'proximity'} = $proximity;
		$all_hosts{$host}{'attack'} = $level;
		$all_hosts{$host}{'expand'} = $expand;
		$all_hosts{$host}{'time'} = $time;
		}
	}
close(ALL_HOSTS);
}

#
# set the last access time for this host. Use numerical form for easy sorting.
#
sub set_host_time {
	local($host) = @_;

	$all_hosts{$host}{'time'} = time()
		if defined($all_hosts{$host});
}

#
# get the last access time for this host. Use numerical form for easy sorting.
#
sub get_host_time {
	local($host) = @_;

	return $all_hosts{$host}{'time'};
}

#
# get the network address for this host
# using the largest netmask
# (added 8/00 for smurf test, s.k.)
#
sub get_network {

my($host, $netmask) = @_;
my(@ip, $network);

@ip = split /\./, &get_host_addr($host);
if ($netmask eq "") {
    $netmask = "255.255.255.0";
}
$netmask =~ s/,/ /;

# for safety, netmask is at least a class B
@mask = (255,255,0,0);

# get the largest netmask
for (split /\s+/,$netmask) {
    @this_mask = split /\./;
    $mask[2] = $this_mask[2] if $this_mask[2] > $mask[2];
    $mask[3] = $this_mask[3] if $this_mask[3] > $mask[3];
}

$ip[3] &= $mask[3] + 0;
$ip[2] &= $mask[2] + 0;

$network = join ".", @ip[0..3];
return $network;
}

#
# get all the network and broadcast addresses for this host
# using the list of netmasks
# (added 8/00 for smurf test, s.k.)
#
sub get_all_networks {

my($host, $netmask) = @_;
my(@ip, $network);

@ip = split /\./, &get_host_addr($host);
if ($netmask eq "") {
    $netmask = "255.255.255.0";
}
$netmask =~ s/,/ /;
@networks = ();

# get the largest netmask
for (split /\s+/,$netmask) {
    @mask = split /\./;
    @this_ip = @ip;
    $this_ip[3] &= $mask[3] + 0;
    $this_ip[2] &= $mask[2] + 0;
    push @networks, (join ".", @this_ip);
    @this_ip = @ip;
    $this_ip[3] |= ($mask[3] ^ 255) + 0;
    $this_ip[2] |= ($mask[2] ^ 255) + 0;
    push @networks, (join ".", @this_ip);
}

return @networks;
}

1;
