#! /usr/bin/perl

use strict;
use warnings;

#Load IPC::System::Simple
use IPC::System::Simple qw(capturex);

#Load Data::Validate::IP
use Data::Validate::IP qw(is_ipv4 is_ipv6);

#Load NetAddr::IP::Util
use NetAddr::IP::Util qw(shiftleft inet_4map6 ipv4to6);

#Load NetAddr::IP
use NetAddr::IP qw(:nofqdn Ones);

#Load POSIX
use POSIX qw(EXIT_SUCCESS EXIT_FAILURE);

#IP v4 hash
my %ip4s = ();
#IP v6 hash
my %ip6s = ();
#Blacklist v4 array
my @blrule4s = ();
#Blacklist v6 array
my @blrule6s = ();

#Init block list
my $blocklist = 0;

#Init spam list
my $spamlist = 1;

#Filter options
@ARGV = map {
	if ($_ eq '-b' || $_ eq '--blocklist') {
		$blocklist = 1; ();
	} elsif ($_ eq '-nb' || $_ eq '--noblocklist') {
		$blocklist = 0; ();
	} elsif ($_ eq '-s' || $_ eq '--spamlist') {
		$spamlist = 1; ();
	} elsif ($_ eq '-ns' || $_ eq '--nospamlist') {
		$spamlist = 0; ();
	} else {
		$_;
	}
} @ARGV;

#Show usage with invalid argument
if (scalar(@ARGV)) {
	print "Usage: $0 [-b|--blocklist|-nb|--noblocklist|-s|--spamlist|-ns|--nospamlist]\n";
	exit EXIT_FAILURE;
}

#TODO: add google/microsoft/etc mail range ?

#TODO: add all configuration in a json config ?

#IP whitelist
#my $iplist = qr/^(?:127\.|::1|2a01:4f8:190:22a6:|5\.9\.143\.173|85\.68\.|81\.67\.|89\.157\.|82\.241\.255\.46)/;
my %iplist = (
	ipv4 => [
		#Localhost
		'127.0.0.0/8',
		#Aurae
		'144.76.27.210/32',
		#Toulouse
		'82.241.255.46/32',
		#Akasha
		#'89.157.132.244/32'
		#'89.3.145.115/32'
		'89.3.147.209/32',
		#Ygg tracker (tracker.yggtracker.cc)
		'31.220.0.116/32',
		#Coppersurfer tracker (tracker.coppersurfer.tk)
		'31.14.40.30/32',
		#Pussytorrent tracker (tracker.pussytorrents.org)
		'217.23.12.105/32'
	],
	ipv6 => [
		#Localhost
		'::1/32',
		#Aurae
		'2a01:4f8:191:1405::/64'
	]
);

#Create a new NetAddr::IP object without calling slow gethostbyname (load /etc/resolv.conf)
sub new_ipv4($) {
	#Extract ip and mask
	my ($ip, $mask) = split('/', shift);
	#Build base struct
	my $self = {
		#Set mask
		mask => !defined($mask)||$mask==32?Ones:shiftleft(Ones, 32 - $mask),
		#Mark as ipv4
		isv6 => 0,
		#Generate fake address
		#XXX: NetAddr::IP expect a faked Socket6 gethostbyname struct
		#XXX: see /usr/lib64/perl5/vendor_perl/NetAddr/IP/Util.pm +235
		addr => inet_4map6(ipv4to6(pack('C4', split('\.', $ip))))
	};
	#Return fake NetAddr::IP object
	return bless $self, 'NetAddr::IP';
}

#User whitelist
my @userlist = ('rapsys');

#Extract sshd.service scan
#map {
#	#Extract user and ip
#	if (/Failed password for (?:invalid user )?(.+) from (.+) port [0-9]+ ssh2/ && grep($_ ne $1, @userlist)) {
#		#Save ip
#		my $ip = $2;
#		#Check if v4 ip and not in whitelist
#		if (is_ipv4($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv4}}) {
#			#Add ip in v4 blacklist
#			$ip4s{$ip}=1;
#		#Check if v6 ip
#		} elsif (is_ipv6($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv6}}) {
#			$ip6s{$ip}=1;
#		}
#	}
#} capturex('journalctl', '-u', 'sshd.service');

#Extract kernel port scan
map {
	#oct. 04 19:10:30 aurae.aoihime.eu kernel: net-fw DROP IN=enp3s0 OUT= MAC=50:46:5d:a1:a1:85:0c:86:10:f5:c6:4b:08:00 SRC=61.227.52.153 DST=144.76.27.210 LEN=52 TOS=0x00 PREC=0x00 TTL=116 ID=29123 DF PROTO=TCP SPT=64349 DPT=445 WINDOW=8192 RES=0x00 SYN URGP=0
	#net-fw DROP IN=enp3s0 OUT= MAC=50:46:5d:a1:a1:85:0c:86:10:f5:c6:4b:08:00 SRC=110.34.70.110 DST=144.76.27.210 LEN=40 TOS=0x00 PREC=0x00 TTL=50 ID=17488 PROTO=TCP SPT=58225 DPT=34567 WINDOW=53283 RES=0x00 SYN URGP=0
	if (/net-fw DROP .* SRC=([^\s]+) .* PROTO=([^\s]+) .* DPT=([^\s]+)/) {
		#Save ip
		my $ip = $1;
		#Save proto
		my $proto = lc($2);
		#Save dpt
		my $dpt = $3;
		#Check if v4 ip and not in whitelist
		#if (is_ipv4($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv4}}) {
		if (is_ipv4($ip) && not scalar map { my $network = new_ipv4($_); my $netip = new_ipv4($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv4}}) {
			if (!defined $ip4s{$ip}) {
				%{$ip4s{$ip}} = ('tcp' => {}, 'udp' => {});
			}
			#Add ip in v4 blacklist
			$ip4s{$ip}{$proto}{$dpt}=1;
		} elsif (is_ipv6($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv6}}) {
			if (!defined $ip6s{$ip}) {
				%{$ip6s{$ip}} = ('tcp' => {}, 'udp' => {});
			}
			#Add ip in v6 blacklist
			$ip6s{$ip}{$proto}{$dpt}=1;
		}
	#oct. 04 19:17:10 aurae.aoihime.eu kernel: audit: type=1100 audit(1570209430.543:17321294): pid=5890 uid=0 auid=4294967295 ses=4294967295 msg='op=PAM:authentication grantors=? acct="root" exe="/usr/sbin/sshd" hostname=195.154.112.70 addr=195.154.112.70 terminal=ssh res=failed'
	#audit: type=1100 audit(1570291573.615:8660): pid=3225 uid=0 auid=4294967295 ses=4294967295 msg='op=PAM:authentication grantors=? acct="root" exe="/usr/sbin/sshd" hostname=222.186.180.9 addr=222.186.180.9 terminal=ssh res=failed'
	} elsif (/op=PAM:authentication grantors=\? acct="(.+)" exe="\/usr\/(?:libexec\/dovecot\/auth|sbin\/sshd)" hostname=.+ addr=(.+) terminal=(dovecot|ssh) res=failed/ && grep($_ ne $1, @userlist)) {
		#Save ip
		my $ip = $2;
		#Init blacklist
		my %blacklist = ('tcp' => [], 'udp' => []);
		#Set ssh proto and port tuple
		if ($3 eq 'ssh') {
			#Set blacklist for 22 dest port on tcp
			#$blacklist{'tcp'}[$#{$blacklist{'tcp'}}+1] = 22;
			%blacklist = ('tcp' => [ 22 ]);
		#Set dovecot proto and port tuples
		} elsif ($3 eq 'dovecot') {
			#Set blacklist for 25, 143, 587, 993 dest ports on tcp and udp
			%blacklist = ('tcp' => [ 25, 143, 587, 993 ], 'udp' => [ 25, 143, 587, 993 ]);
		#Set other proto and port tuples
		} else {
		}

		#Check if v4 ip and not in whitelist
		if (is_ipv4($ip) && not scalar map { my $network = new_ipv4($_); my $netip = new_ipv4($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv4}}) {
			if (!defined $ip4s{$ip}) {
				%{$ip4s{$ip}} = ('tcp' => {}, 'udp' => {});
			}
			#Add ip tuples in v4 blacklist
			map { my $proto = $_; map { $ip4s{$ip}{$proto}{$_}=1; } @{$blacklist{$proto}}; } keys %blacklist;
		#Check if v6 ip
		} elsif (is_ipv6($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv6}}) {
			if (!defined $ip6s{$ip}) {
				%{$ip6s{$ip}} = ('tcp' => {}, 'udp' => {});
			}
			#Add ip tuples in v6 blacklist
			map { my $proto = $_; map { $ip6s{$ip}{$proto}{$_}=1; } @{$blacklist{$proto}}; } keys %blacklist;
		}
	#nov. 30 15:30:07 aurae.aoihime.eu kernel: audit: type=1100 audit(1575124207.371:38129): pid=685 uid=985 auid=4294967295 ses=4294967295 msg='op=PAM:authentication grantors=? acct="toto" exe="/usr/bin/pwauth" hostname=? addr=? terminal=? res=failed'
	#XXX: Until mod_authnz pass to pwauth the (SERVER_NAME|SERVER_ADDR) + REMOTE_ADDR+REMOTE_PORT in env it's impossible to know who did a failed auth
	#XXX: see https://github.com/phokz/mod-auth-external/blob/master/mod_authnz_external/TODO
	#} elsif (/op=PAM:authentication grantors=\? acct="(.+)" exe="\/usr\/bin\/pwauth" hostname=.+ addr=(.+) terminal=\? res=failed/ && grep($_ ne $1, @userlist)) {
	#	...
	}
} capturex('journalctl', '-m', '-t', 'kernel', '-o', 'cat', '--no-hostname');

#With spamlist
if ($spamlist) {
	#Extract originating ip in spam
	map {
		#Open spam file for reading
		open (my $fh, '<', $_) or die "Can't open < $_: $!";

		#Lookup for X-Originating-IP header
		while (<$fh>) {
			if (/X-Originating-IP: (.+)$/) {
				#Set ip
				my $ip = ${1};

				#Set blacklist for 80 and 443 dest ports on tcp
				my %blacklist = ('tcp' => [ 80, 443, 8000, 8080, 8443 ]);

				#Check if v4 ip and not in whitelist
				#if (is_ipv4($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv4}}) {
				if (is_ipv4($ip) && not scalar map { my $network = new_ipv4($_); my $netip = new_ipv4($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv4}}) {
					if (!defined $ip4s{$ip}) {
						%{$ip4s{$ip}} = ('tcp' => {}, 'udp' => {});
					}
					#Add ip tuples in v4 blacklist
					map { my $proto = $_; map { $ip4s{$ip}{$proto}{$_}=1; } @{$blacklist{$proto}}; } keys %blacklist;
				} elsif (is_ipv6($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv6}}) {
					if (!defined $ip6s{$ip}) {
						%{$ip6s{$ip}} = ('tcp' => {}, 'udp' => {});
					}
					#Add ip tuples in v6 blacklist
					map { my $proto = $_; map { $ip6s{$ip}{$proto}{$_}=1; } @{$blacklist{$proto}}; } keys %blacklist;
				}
			}
		}

		#Close spam file
		close $fh or die "Can't close fh: $!";
	} glob('/var/spool/mail/*/.Junk/{new,cur,tmp}/*');
}

#Process each ipv4s keys
map {
	#Set proto as either tcp or udp
	for my $proto (('tcp', 'udp')) {
		#Check if branch is empty
		if (!scalar keys %{$ip4s{$_}{$proto}}) {
			#Prune it
			delete $ip4s{$_}{$proto};
		}
	}
} keys %ip4s;

#Process each ipv6s keys
map {
	#Set proto as either tcp or udp
	for my $proto (('tcp', 'udp')) {
		#Check if branch is empty
		if (!scalar keys %{$ip6s{$_}{$proto}}) {
			#Prune it
			delete $ip6s{$_}{$proto};
		}
	}
} keys %ip6s;

#Open blrule4s file for reading
open (my $fh, '<', '/etc/shorewall/blrules') or die "Can't open < /etc/shorewall/blrules: $!";

#Populate with comments
@blrule4s = map { chomp($_); if (/^#/) { $_; } else { (); } } <$fh>;

#Prepend each specific ip from whitelist
map { push @blrule4s, "WHITELIST\tnet:$1\tall" if (/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/32$/); } @{$iplist{ipv4}};

#Close blrule4s file
close $fh or die "Can't close fh: $!";

#With blocklist
if ($blocklist) {
	#Open bt_blocklists.ipv4 file for reading
	open ($fh, '<', '/usr/local/share/blacklist/shorewall.ipv4') or die "Can't open < /usr/local/share/blacklist/shorewall.ipv4: $!";

	#Prepend bt_blocklists.ipv4 drop
	map { chomp $_; push @blrule4s, "DROP\t\tnet:$_\tfw"; } <$fh>;

	#Close bt_blocklists.ipv4 file
	close $fh or die "Can't close fh: $!";
}

#Build blacklist
map {
	#Set proto from hash
	for my $proto (sort keys %{$ip4s{$_}}) {
		#Push rule
		push @blrule4s, "DROP\t\tnet:".$_.(length($_)<12?"\t":'')."\tfw\t$proto\t".(scalar keys %{$ip4s{$_}{$proto}}>5||defined $ip4s{$_}{$proto}{0}?'#':'').join(",", sort { $a <=> $b } keys %{$ip4s{$_}{$proto}});
	}
} sort keys %ip4s;

#Open blrule4s file for writing
open ($fh, '>', '/etc/shorewall/blrules') or die "Can't open > /etc/shorewall/blrules: $!";

#Inject content of blacklist
map { print $fh $_."\n"; } @blrule4s;

#Close blrule4s file
close $fh or die "Can't close fh: $!";

#Print ipv6 to update hash
#XXX; right now it don't seems scanned at all...
for (sort keys %ip6s) {
	#Set proto from hash
	for my $proto (keys %{$ip6s{$_}}) {
		#Print the ipv6 scanner
		print $_."\t$proto\t".join(",", keys %{$ip6s{$_}{$proto}})."\n";
	}
}

#TODO: add ipv6 ?

#Restart shorewall service
capturex('systemctl', 'restart', 'shorewall.service');