package Guardian::Parser;
use strict;
use warnings;

use Exporter qw(import);

our @EXPORT_OK = qw(IsSupportedParser Parser);

# This hash contains all supported parsers and which function
# has to be called to parse messages in the right way.
my %logfile_parsers = (
	"httpd" => \&message_parser_httpd,
	"owncloud" => \&message_parser_owncloud,
	"snort" => \&message_parser_snort,
	"ssh" => \&message_parser_ssh,
);

#
## The "Init" (Parser) function.
#
## This function is responsible to initialize the Parser as a class based object.
## It has to be called once before any parsing of messages can be done.
#
sub Init (%) {
	my ( $class, %args ) = @_;
	my $self = \%args;

	# Use bless to make "$self" to an object of class "$class".
	bless($self, $class);

	# Return the class object.
	return $self;
}

#
## The "Update" Parser settings function.
#
## This object based function is called to update various class settings.
#
sub Update (\%) {
	my $self = shift;

	# Dereference the given hash-ref and store
	# the values into a new temporary hash.
	my %settings = %{ $_[0] };

	# Update snort priority level settings or disable it.
	if ((defined($self->{SnortPriorityLevel})) && (exists($settings{SnortPriorityLevel}))) {
			# Change settings.
			$self->{SnortPriorityLevel} = $settings{SnortPriorityLevel};
	} else {
		# Remove setting.
		delete $self->{SnortPriorityLevel};
	}

	# Return modified class object.
	return $self;
}

#
## The main parsing function.
#
## It is used to determine which sub-parser has to be used to
## parse the given message in the right way and to return if
## any action should be performed.
#
sub Parser ($$) {
	my $self = shift;
	my ($parser, @message) = @_;

	# If no responsible message parser could be found, just return nothing.
	unless (exists($logfile_parsers{$parser})) {
		return;
	}

	# Call responsible message parser.
	my @actions = $logfile_parsers{$parser}->($self, @message);

	# In case an action has been returned, return it too. 
	if (@actions) {
		# Return which actions should be performed.
		return @actions;
	}

	# Return undef, if no actions are required.
	return undef;
}

#
## IsSupportedParser function.
#
## This very tiny function checks if a given parser name is available and
## therefore a supported parser.
#
## To perform these check, the function is going to lookup if a key in the
## hash of supported parsers is available
#
sub IsSupportedParser ($) {
	my $parser = $_[0];

	# Check if a key for the given parser exists in the hash of logfile_parsers.
	if(exists($logfile_parsers{$parser})) {
		# Found a valid parser, so return nothing.
		return 1;
	}

	# Return "False" if we got here, and therefore no parser
	# is available.
	return;
}

#
## The Snort message parser.
#
## This subfunction is responsible for parsing sort alerts and determine if
## an action should be performed.
#
## XXX Currently the parser only supports IPv4. Add support for IPv6 at a
## later time.
#
sub message_parser_snort(@) {
	my $self = shift;
	my @message = @_;
	my @actions;

	# Temporary array to store single alerts.
	my @alert;

	# The name of the parser module.
	my $name = "SNORT";

	# Default returned message in case no one could be grabbed
	# from the snort alert.
	my $message = "An active snort rule has matched and gained an alert.";

	# Snort uses a log buffer and a result of this, when detecting multiple
	# events at once, multiple alerts will be written at one time to the alert
	# file. They have to be seperated from each, to be able to parse them
	# individually.
	foreach my $line (@message) {
		# Remove any newlines.
		chomp($line);

		# A single alert contains multiple lines, push all of them
		# a temporary array.
		push(@alert, $line);

		# Each alert ends with an empty line, if one is found,
		# all lines of the current processed alert have been found
		# and pushed to the temporary array.
		if($line =~ /^\s*$/) {
			# Variable to store the grabbed IP-address.
			my $address;
			my $classification;

			# Loop through all lines of the current alert.
			foreach my $line (@alert) {
				# Determine if the alert has been classified.
				if ($line =~ /.*\[Classification: .*\] \[Priority: (\d+)\].*/) {
					my $priority = $1;

					# Set classification to true.
					$classification = "1";

					# Obtain configured priority level.
					my $priority_level = $self->{SnortPriorityLevel};

					# Skip alerts if the priority is to low.
					if ($priority < $priority_level) {
						last;
					}
				}

				# Search for a line like xxx.xxx.xxx.xxx -> xxx.xxx.xxx.xxx
				if ($line =~ /(\d+\.\d+\.\d+\.\d+)+ -\> (\d+\.\d+\.\d+\.\d+)+/) {
					# Store the grabbed IP-address.
					$address = $1;
				}

				# Search for a line like xxx.xxx.xxx.xxx:xxx -> xxx.xxx.xxx.xxx:xxx
				elsif ($line =~ /(\d+\.\d+\.\d+\.\d+):\d+ -\> (\d+\.\d+\.\d+\.\d+):\d+/) {
					# Store the obtained IP-address.
					$address = $1;
				}

				# Grab the reason from a msg field of the alert.
				if ($line =~ /.*msg:\"(.*)\".*/) {
					# Store the extracted message.
					$message = $1;
				}

				# If the reason could not be determined, try to obtain it from the headline of the alert.
				elsif ($line =~ /.*\] (.*) \[\*\*\]/) {
					# Store the extracted message.
					$message = $1;
				}
			}

			# Check if at least the IP-address information has been extracted.
			if ((defined ($classification)) && (defined ($address))) {
				# Add the extracted values and event message for the computed
				# event to the actions array.
				push(@actions, "count $address $name $message");
			}

			# The alert has been processed, clear the temporary array for storing
			# the next alert.
			@alert = ();
		}
	}

	# If any actions are required, return the array.
	if (@actions) {
		return (@actions);
	}

	# If we got here, the alert could not be parsed correctly, or did not match any filter.
	# Therefore it can be skipped - return nothing.
	return;
}

#
## The SSH message parser.
#
## This subfunction is used for parsing and detecting different attacks
## against the SSH service.
#
sub message_parser_ssh (@) {
	my $self = shift;
	my @message = @_;
	my @actions;

	# The name of the parser module.
	my $name = "SSH";

	# Variable to store the grabbed IP-address.
	my $address;

	# Variable to store the parsed event.
	my $message;

	# Loop through all lines, in case multiple one have
	# been passed.
	foreach my $line (@message) {
		# Check for failed password attempts.
		if ($line =~/.*sshd.*Failed password for (.*) from (.*) port.*/) {
			# Store the grabbed IP-address.
			$address = $2;

			# Set event message.
			$message = "Possible SSH-Bruteforce Attack for user: $1.";
		}

		# This should catch Bruteforce Attacks with enabled preauth
		elsif ($line =~ /.*sshd.*Received disconnect from (.*):.*\[preauth\]/) {
			# Store obtained IP-address.
			$address = $1;

			# Set event message.
			$message = "Possible SSH-Bruteforce Attack - failed preauth.";
		}

		# Check if at least the IP-address information has been extracted.
		if (defined ($address)) {
			# Add the extracted values and event message for the computed
			# event to the actions array.
			push(@actions, "count $address $name $message");
		}
	}

	# If any actions are required, return the array.
	if (@actions) {
		return (@actions);
	}

	# If we got here, the provided message is not affected by any filter and
	# therefore can be skipped. Return nothing (False) in this case.
	return;
}

#
## The HTTPD message parser.
#
## This subfunction is used for parsing and detecting different attacks
## against a running HTTPD service.
#
sub message_parser_httpd (@) {
	my $self = shift;
	my @message = @_;
	my @actions;

	# The name of the parser module.
	my $name = "HTTPD";

	# Variable to store the grabbed IP-address.
	my $address;

	# Variable to store the parsed event.
	my $message;

	# Loop through all lines, in case multiple one have
	# been passed.
	foreach my $line (@message) {
		# This will catch brute-force attacks against htaccess logins (username).
		if ($line =~ /.*\[client (.*)\] .* user(.*) not found:.*/) {
			# Store the grabbed IP-address.
			$address = $1;

			# Set event message.
			$message = "Possible WUI brute-force attack, wrong user: $2.";
		}

		# Detect htaccess password brute-forcing against a username.
		elsif ($line =~ /.*\[client (.*)\] .* user(.*): authentication failure for.*/) {
			# Store the extracted IP-address.
			$address = $1;

			# Set event message.
			$message = "Possible WUI brute-force attack, wrong password for user: $2.";
		}

		# Check if at least the IP-address information has been extracted.
		if (defined ($address)) {
			# Check if the address also contains a port value.
			if ($address =~ m/:/) {
				my ($add_address, $port) = split(/:/, $address);

				# Only process the address.
				$address = $add_address;
			}

			# Add the extracted values and event message to the actions array.
			push(@actions, "count $address $name $message");
		}
	}

	# If any actions are required, return the array.
	if (@actions) {
		return @actions;
	}

	# If we got here, the provided message is not affected by any filter and
	# therefore can be skipped. Return nothing (False) in this case.
	return;
}

#
## The Owncloud message parser.
#
## This subfunction is used for parsing and detecting brute-force login
## attempts against a local running owncloud instance.
#
sub message_parser_owncloud (@) {
	my @message = @_;
	my @actions;

	# The name of the parser module.
	my $name = "Owncloud";

	# Variable to store the grabbed IP-address.
	my $address;

	# Variable to store the parsed event.
	my $message;

	# Loop through all lines, in case multiple one have
	# been passed.
	foreach my $line (@message) {
		 # This will catch brute-force attacks against the login (username).
		if ($line =~/.*\"Login failed: \'(.*)\' \(Remote IP: \'(.*)\'\,.*/) {
			# Store the grabbed user name.
			my $user = $1;

			# Store the grabbed IP-address.
			$address = $2;

			# Set event message.
			$message = "Possible brute-force attack, wrong password for user: $user.";
		}

		# Check if at least the IP-address information has been extracted.
		if (defined ($address)) {
			# Add the extracted values and event message to the actions array.
			push(@actions, "count $address $name $message");
		}
	}

	# If any actions are required, return the array.
	if (@actions) {
		return @actions;
	}

	# If we got here, the provided message is not affected by any filter and
	# therefore can be skipped. Return nothing (False) in this case.
	return;
}

1;
