Blame tools/parse_modsec.pl

Packit 284210
#!/usr/bin/perl
Packit 284210
Packit 284210
use strict;
Packit 284210
use warnings;
Packit 284210
Packit 284210
use DateTime::Format::Strptime;
Packit 284210
use Getopt::Long qw(:config no_ignore_case bundling);
Packit 284210
use JSON;
Packit 284210
use List::MoreUtils qw(any);
Packit 284210
use NetAddr::IP;
Packit 284210
use Try::Tiny;
Packit 284210
Packit 284210
=pod
Packit 284210
Packit 284210
=head1 NAME
Packit 284210
Packit 284210
parse_modsec.pl
Packit 284210
Packit 284210
=head1 SYNOPSIS
Packit 284210
Packit 284210
Parse ModSecurity logs generated as JSON
Packit 284210
Packit 284210
=head1 USAGE
Packit 284210
Packit 284210
Usage: $0 [h] [Htsrfdbalspjv]
Packit 284210
    -H|--host             Search rules based on the Host request header
Packit 284210
    -t|--transaction-id   Search rules based on the unique transaction ID
Packit 284210
    -s|--source-ip        Search rules based on the client IP address (can be presented as an address or CIDR block)
Packit 284210
    -r|--rule-id          Search rules based on the rule ID
Packit 284210
    -f|--filter           Define advanced filters to walk through JSON tree
Packit 284210
    -d|--delim            Define a delimiter for advanced filters. Default is '.'
Packit 284210
    -b|--before           Search rules before this timeframe
Packit 284210
    -a|--after            Search rules after this timeframe
Packit 284210
    -l|--logpath          Define a path to read JSON logs from. Default is '/var/log/modsec_audit.log'
Packit 284210
    -S|--stdin            Read rules from stdin instead of an on-disk file
Packit 284210
    -p|--partial-chains   Do not prune partial chain matches
Packit 284210
    -j|--json             Print rule entries as a JSON blob, rather than nice formatting
Packit 284210
    -v|--verbose          Be verbose about various details such as JSON parse failures and log data
Packit 284210
Packit 284210
Packit 284210
=head2 FILTERS
Packit 284210
Packit 284210
ModSecurity JSON audit logs are written as a series of atomic JSON documents, as opposed to a single, monolithic structure. This program will read through all JSON documents provided, making certain assumptions about the structure of each document, and will print out relevent entries based on the parameters provided. Log entries can be filtered by key-value pairs; given a key at an arbitrary level in the document, test the value of the key against an expected expression. The best way to understand this is with examples (see EXAMPLES for further details).
Packit 284210
Packit 284210
Filter values are treated as regular expressions. Each match is anchored by '^' and'$', meaning that values that do not contain PCRE metacharacters will essentially match by string equality.
Packit 284210
Packit 284210
Filters can be used to search a specific key-pair value, or an array of values. Arrays containing sub-elements can also be traversed. Arrays are identified in a filter key expression through the use of the '%' metacharacter. See EXAMPLES for further discussion of filter key expression syntax.
Packit 284210
Packit 284210
Multiple filters can be provided, and are used in a logical AND manner (that is, an entry must match all given filters).
Packit 284210
Packit 284210
Packit 284210
=head2 FILTER EXAMPLES
Packit 284210
Packit 284210
Examine the following entry:
Packit 284210
Packit 284210
	{
Packit 284210
		"foo": "bar",
Packit 284210
		"foo2": "bar2",
Packit 284210
		"qux": {
Packit 284210
			"quux": "corge",
Packit 284210
			"grault": "garply",
Packit 284210
			"wal.do": "fred"
Packit 284210
		},
Packit 284210
		"baz": [
Packit 284210
			"bat",
Packit 284210
			"bam",
Packit 284210
			"bif"
Packit 284210
		],
Packit 284210
		"bal": [
Packit 284210
			{ "hello": "world" },
Packit 284210
			{ "how": "are" },
Packit 284210
			{ "you": "doing" }
Packit 284210
		]
Packit 284210
	}
Packit 284210
Packit 284210
A search for the top level key "foo" containing the value "bar" would look like:
Packit 284210
Packit 284210
	-f foo=bar
Packit 284210
Packit 284210
However, the following will not result in the entry being matched:
Packit 284210
Packit 284210
	-f foo=bar2
Packit 284210
Packit 284210
This is because the value of "foo" in the JSON document does not match the regex "^bar2$"
Packit 284210
Packit 284210
Searching sub-keys is possible by providing the traversal path as the filter key, separated by a delimiter. By default the delimiter is '.'. For example, to search the value of the "grault" subkey within the "qux" key:
Packit 284210
Packit 284210
	-f qux.grault=<expression>
Packit 284210
Packit 284210
Search arrays is also possible with the use of the '%' metacharacter, which should be used in place of a key name in the filter expression. For example, to search through all the values in the "baz" top-level key:
Packit 284210
Packit 284210
	-f baz.%=<expression>
Packit 284210
Packit 284210
Searching for specific keys that are live in an array is also possible. For example, to search for the value of the "hello" key within the top-level key "bal" array:
Packit 284210
Packit 284210
	-f bal.%.hello=<expression>
Packit 284210
Packit 284210
If any key contains a period character (.), you can specify an alternative delimiter using the '-d' option. To search the "wal.do" key within "qux":
Packit 284210
Packit 284210
	-d @ quz@wal.do=<expression>
Packit 284210
Packit 284210
Packit 284210
=head2 SHORTCUTS
Packit 284210
Packit 284210
Quick searches of on-disk log files likely will be performed using simple queries. Rather than forcing users to write a filter for common parameters, we provide a few shortcuts as options. These shortcuts can be combined with additional filters for complex searches. Provided shortcuts (and the matching filter key expression) are listed below:
Packit 284210
Packit 284210
	Host:             request.headers.Host
Packit 284210
	Transaction ID:   transaction.transaction_id
Packit 284210
	Rule ID:          matched_rules.%.rules.%.actionset.id
Packit 284210
Packit 284210
Additionally, the '--source-ip' argument allows for searching rule entries based on the remote IP address. This option searches based on CIDR blocks, instead of the filter searching described above.
Packit 284210
Packit 284210
=head2 TIMEFRAME
Packit 284210
Packit 284210
Log entries can further be narrowed by time range. The --before and --after flags can be used to return only entries that returned before or after (or both) a given date and time. Values for these options can be provided by the following syntax:
Packit 284210
Packit 284210
	^\d+[dDhHmM]?$
Packit 284210
Packit 284210
For example, to limit the search of entries to between one and 4 days ago:
Packit 284210
Packit 284210
	-a 4d -b 1d
Packit 284210
Packit 284210
You may provide one, both, or neither of these flags.
Packit 284210
Packit 284210
Packit 284210
=head2 USAGE EXAMPLES
Packit 284210
Packit 284210
Print all log entries from the default log location:
Packit 284210
Packit 284210
	parse_modsec.pl
Packit 284210
Packit 284210
Print all log entries and show more detailed information, such as response data and matched rule details
Packit 284210
Packit 284210
	parse_modsec.pl -v
Packit 284210
Packit 284210
Print entries matching a specific source IP:
Packit 284210
Packit 284210
	parse_modsec.pl -s 1.2.3.4
Packit 284210
Packit 284210
Print entries matching a source IP in a given subnet:
Packit 284210
Packit 284210
	parse_modsec.pl -s 1.2.3.0/24
Packit 284210
Packit 284210
Print entries matching a given host and all its sub domains:
Packit 284210
Packit 284210
	parse_modsec.pl -H .*example.com
Packit 284210
Packit 284210
Print entries matching a specific rule ID, that occurred within the last 12 hours:
Packit 284210
Packit 284210
	parse_modsec.pl -r 123456 -a 12h
Packit 284210
Packit 284210
Print entries matching a given rule ID, even if that ID was present in a partial chain:
Packit 284210
Packit 284210
	parse_modsec.pl -r 123456 -p
Packit 284210
Packit 284210
Print entries that contain an HTTP status code 403
Packit 284210
Packit 284210
	parse_modsec.pl -f response.status=403
Packit 284210
Packit 284210
Print entries that contain an HTTP GET request with a 'Content-Length' header
Packit 284210
Packit 284210
	parse_modsec.pl -f request.headers.Content-Length=.* -f request.request_line=GET.*
Packit 284210
Packit 284210
=cut
Packit 284210
Packit 284210
sub usage {
Packit 284210
	print <<"_EOF";
Packit 284210
Usage: $0 [h] [Htsrfdbalspjv]
Packit 284210
	-h|--help             Print this help
Packit 284210
	-H|--host             Search rules based on the Host request header
Packit 284210
	-t|--transaction-id   Search rules based on the unique transaction ID
Packit 284210
	-s|--source-ip        Search rules based on the client IP address (can be presented as an address or CIDR block)
Packit 284210
	-r|--rule-id          Search rules based on the rule ID
Packit 284210
	-f|--filter           Define advanced filters to walk through JSON tree
Packit 284210
	-d|--delim            Define a delimiter for advanced filters. Default is '.'
Packit 284210
	-b|--before           Search rules before this timeframe
Packit 284210
	-a|--after            Search rules after this timeframe
Packit 284210
	-l|--logpath          Define a path to read JSON logs from. Default is '/var/log/modsec_audit.log'
Packit 284210
	-S|--stdin            Read rules from stdin instead of an on-disk file
Packit 284210
	-p|--partial-chains   Do not prune partial chain matches
Packit 284210
	-j|--json             Print rule entries as a JSON blob, rather than nice formatting
Packit 284210
	-v|--verbose          Be verbose about various details such as JSON parse failures and log data
Packit 284210
Packit 284210
	For detailed explanations of various options and example usages, see 'perldoc $0'
Packit 284210
Packit 284210
_EOF
Packit 284210
	exit 1;
Packit 284210
}
Packit 284210
Packit 284210
# figure the number of seconds based on the command-line option
Packit 284210
sub parse_duration {
Packit 284210
	my ($duration) = @_;
Packit 284210
Packit 284210
	if ($duration =~ /^(\d+)[dD]$/) {
Packit 284210
		return $1 * 60 * 60 * 24;
Packit 284210
	} elsif ($duration =~ /^(\d+)[hH]$/) {
Packit 284210
		return $1 * 60 * 60;
Packit 284210
	} elsif ($duration =~ /^(\d+)[mM]$/) {
Packit 284210
		return $1 * 60;
Packit 284210
	} elsif ($duration =~ /^(\d+)[sS]?$/) {
Packit 284210
		return $1;
Packit 284210
	} else {
Packit 284210
		die "Couldn't parse duration $duration!\n";
Packit 284210
	}
Packit 284210
}
Packit 284210
Packit 284210
# build a DateTime representative of the past
Packit 284210
sub build_datetime {
Packit 284210
	my ($duration) = @_;
Packit 284210
Packit 284210
	return if !$duration;
Packit 284210
	return DateTime->now()->subtract(seconds => parse_duration($duration));
Packit 284210
}
Packit 284210
Packit 284210
# determine if the log entry occurred within the given timeframe
Packit 284210
sub within_timeframe {
Packit 284210
	my ($args)    = @_;
Packit 284210
	my $entry     = $args->{entry};
Packit 284210
	my $before    = $args->{before};
Packit 284210
	my $after     = $args->{after};
Packit 284210
	my $timestamp = parse_modsec_timestamp($entry->{transaction}->{time});
Packit 284210
Packit 284210
	return (defined $before ? $timestamp < $before : 1) &&
Packit 284210
	       (defined $after  ? $timestamp > $after  : 1);
Packit 284210
}
Packit 284210
Packit 284210
# sigh...
Packit 284210
sub parse_modsec_timestamp {
Packit 284210
	my ($input) = @_;
Packit 284210
Packit 284210
	my $format = '%d/%b/%Y:%H:%M:%S -%z';
Packit 284210
	my $locale = 'en_US';
Packit 284210
Packit 284210
	my $strp = DateTime::Format::Strptime->new(
Packit 284210
		pattern => $format,
Packit 284210
		locale  => $locale,
Packit 284210
	);
Packit 284210
Packit 284210
	return $strp->parse_datetime($input);
Packit 284210
}
Packit 284210
Packit 284210
# figure out if we're reading from a file or stdin
Packit 284210
# return a file handle representation of our data
Packit 284210
sub get_input {
Packit 284210
	my ($args)  = @_;
Packit 284210
	my $logpath = $args->{logpath};
Packit 284210
	my $stdin   = $args->{stdin};
Packit 284210
	my $fh;
Packit 284210
Packit 284210
	$stdin ?
Packit 284210
		$fh = *STDIN :
Packit 284210
		open $fh, '<', $logpath or die $!;
Packit 284210
Packit 284210
	return $fh;
Packit 284210
}
Packit 284210
Packit 284210
# figure if the target address/cidr contains the entry's remote address
Packit 284210
sub cidr_match {
Packit 284210
	my ($args)    = @_;
Packit 284210
	my $entry     = $args->{entry};
Packit 284210
	my $target    = $args->{target};
Packit 284210
	my $client_ip = $entry->{transaction}->{remote_address};
Packit 284210
Packit 284210
	return $target ? $target->contains(NetAddr::IP->new($client_ip)) : 1;
Packit 284210
}
Packit 284210
Packit 284210
# given a file handle, return an arrayref representing pertinent rule entries
Packit 284210
sub grok_input {
Packit 284210
	my ($args)    = @_;
Packit 284210
	my $fh        = $args->{fh};
Packit 284210
	my $filters   = $args->{filters};
Packit 284210
	my $delim     = $args->{delim};
Packit 284210
	my $source_ip = $args->{source_ip};
Packit 284210
	my $before    = $args->{before};
Packit 284210
	my $after     = $args->{after};
Packit 284210
	my $partial   = $args->{partial};
Packit 284210
	my $verbose   = $args->{verbose};
Packit 284210
Packit 284210
	my @ref;
Packit 284210
Packit 284210
	while (my $line = <$fh>) {
Packit 284210
		my $entry;
Packit 284210
Packit 284210
		try {
Packit 284210
			$entry = decode_json($line);
Packit 284210
		} catch {
Packit 284210
			warn "Could not decode as JSON:\n$line\n" if $verbose;
Packit 284210
		};
Packit 284210
Packit 284210
		next if !$entry;
Packit 284210
Packit 284210
		skim_entry({
Packit 284210
			entry   => $entry,
Packit 284210
			partial => $partial,
Packit 284210
		});
Packit 284210
Packit 284210
		next if !filter({
Packit 284210
			filters => $filters,
Packit 284210
			data    => $entry,
Packit 284210
			delim   => $delim,
Packit 284210
		});
Packit 284210
Packit 284210
		next if !cidr_match({
Packit 284210
			entry  => $entry,
Packit 284210
			target => $source_ip,
Packit 284210
		});
Packit 284210
Packit 284210
		next if !within_timeframe({
Packit 284210
			entry  => $entry,
Packit 284210
			before => $before,
Packit 284210
			after  => $after,
Packit 284210
		});
Packit 284210
Packit 284210
		push @ref, $entry;
Packit 284210
	}
Packit 284210
Packit 284210
	return \@ref;
Packit 284210
}
Packit 284210
Packit 284210
# get rid of partial chains and other noise
Packit 284210
sub skim_entry {
Packit 284210
	my ($args)  = @_;
Packit 284210
	my $entry   = $args->{entry};
Packit 284210
	my $partial = $args->{partial};
Packit 284210
	my $ctr     = 0;
Packit 284210
Packit 284210
	for my $matched_rule (@{$entry->{matched_rules}}) {
Packit 284210
		splice @{$entry->{matched_rules}}, $ctr++, 1
Packit 284210
			if $matched_rule->{chain} && !$matched_rule->{full_chain_match} && !$partial;
Packit 284210
	}
Packit 284210
}
Packit 284210
Packit 284210
# print entries after filtering and skimming
Packit 284210
sub print_matches {
Packit 284210
	my ($args)  = @_;
Packit 284210
	my $ref     = $args->{ref};
Packit 284210
	my $json    = $args->{json};
Packit 284210
	my $verbose = $args->{verbose};
Packit 284210
Packit 284210
	for my $entry (@{$ref}) {
Packit 284210
		if ($json) {
Packit 284210
			print encode_json($entry) . "\n";
Packit 284210
		} else {
Packit 284210
			printf "\n%s\n", '=' x 80;
Packit 284210
Packit 284210
			my $transaction   = $entry->{transaction};
Packit 284210
			my $request       = $entry->{request};
Packit 284210
			my $response      = $entry->{response};
Packit 284210
			my $audit_data    = $entry->{audit_data};
Packit 284210
			my $matched_rules = $entry->{matched_rules};
Packit 284210
Packit 284210
			if ($transaction) {
Packit 284210
				printf "%s\nTransaction ID: %s\nIP: %s\n\n",
Packit 284210
					parse_modsec_timestamp($transaction->{time}),
Packit 284210
					$transaction->{transaction_id},
Packit 284210
					$transaction->{remote_address};
Packit 284210
			}
Packit 284210
Packit 284210
			printf "%s\n", $request->{request_line}
Packit 284210
				if $request->{request_line};
Packit 284210
Packit 284210
			if ($request->{headers}) {
Packit 284210
				for my $header (sort keys %{$request->{headers}}) {
Packit 284210
					printf "%s: %s\n", $header, $request->{headers}->{$header};
Packit 284210
				}
Packit 284210
			}
Packit 284210
Packit 284210
			if ($verbose) {
Packit 284210
				print join ("\n", @{$request->{body}}) . "\n"
Packit 284210
					if $request->{body};
Packit 284210
Packit 284210
				printf "\n%s %s\n", $response->{protocol}, $response->{status}
Packit 284210
					if $response->{protocol} && $response->{status};
Packit 284210
Packit 284210
				for my $header (sort keys %{$response->{headers}}) {
Packit 284210
					printf "%s: %s\n", $header, $response->{headers}->{$header};
Packit 284210
				}
Packit 284210
Packit 284210
				printf "\n%s\n", $response->{body}
Packit 284210
					if $response->{body};
Packit 284210
			}
Packit 284210
Packit 284210
			for my $chain (@{$matched_rules}) {
Packit 284210
				print "\n";
Packit 284210
				my @extra_data;
Packit 284210
				my $ctr = 0;
Packit 284210
Packit 284210
				for my $rule (@{$chain->{rules}}) {
Packit 284210
					printf $rule->{is_matched} ? "%s%s\n" : "%s#%s\n", '  ' x $ctr++, $rule->{unparsed};
Packit 284210
					push @extra_data, $rule->{actionset}->{msg} if $rule->{actionset}->{msg};
Packit 284210
					push @extra_data, $rule->{actionset}->{logdata} if $rule->{actionset}->{logdata};
Packit 284210
				}
Packit 284210
Packit 284210
				printf "\n-- %s\n", join "\n-- ", @extra_data
Packit 284210
					if @extra_data && $verbose;
Packit 284210
			}
Packit 284210
Packit 284210
			printf "\n-- %s\n\n", $audit_data->{action}->{message}
Packit 284210
				if $audit_data->{action}->{message} && $verbose;
Packit 284210
Packit 284210
			printf "%s\n", '=' x 80;
Packit 284210
		}
Packit 284210
	}
Packit 284210
}
Packit 284210
Packit 284210
# filter out rule entries based on given filter definitions
Packit 284210
sub filter {
Packit 284210
	my ($args)  = @_;
Packit 284210
	my $filters = $args->{filters};
Packit 284210
	my $data    = $args->{data};
Packit 284210
	my $delim   = $args->{delim};
Packit 284210
Packit 284210
	my $valid_match = 1;
Packit 284210
Packit 284210
	for my $field (keys %{$filters}) {
Packit 284210
		my $args  = {
Packit 284210
			field => $field,
Packit 284210
			match => $filters->{$field},
Packit 284210
			delim => $delim,
Packit 284210
			hash  => $data,
Packit 284210
		};
Packit 284210
Packit 284210
		if (!match($args)) {
Packit 284210
			$valid_match = 0;
Packit 284210
			last;
Packit 284210
		}
Packit 284210
	}
Packit 284210
	return $valid_match;
Packit 284210
}
Packit 284210
Packit 284210
# match a hash element (may be an array of elements) against a given pattern
Packit 284210
sub match {
Packit 284210
	my ($args) = @_;
Packit 284210
	my $delim  = $args->{delim};
Packit 284210
	my $hash   = $args->{hash};
Packit 284210
	my $match  = $args->{match};
Packit 284210
	my $field  = $args->{field};
Packit 284210
Packit 284210
	my @matches = traverse($args);
Packit 284210
Packit 284210
	return any { $_ =~ m/^$match$/ } @matches;
Packit 284210
}
Packit 284210
Packit 284210
# walk a JSON structure in search of a given key
Packit 284210
# borrowed and butchered from view_signatures.pl
Packit 284210
sub traverse {
Packit 284210
	my ($args)   = @_;
Packit 284210
	my $delim    = $args->{delim};
Packit 284210
	my $hash     = $args->{hash};
Packit 284210
	my $match    = $args->{match};
Packit 284210
	my $field    = $args->{field};
Packit 284210
	my @traverse = split /\Q$delim\E/, $field;
Packit 284210
Packit 284210
	my @values;
Packit 284210
Packit 284210
	while (my $level = shift @traverse) {
Packit 284210
		if ($level eq '%') {
Packit 284210
			# match() is called in a list context
Packit 284210
			# so if we have a bad filter expression
Packit 284210
			# we need to bail in a sensible way
Packit 284210
			return () if ref $hash ne 'ARRAY';
Packit 284210
Packit 284210
			for my $subhash (@{$hash}) {
Packit 284210
				my @match = traverse({
Packit 284210
					hash  => $subhash,
Packit 284210
					delim => $delim,
Packit 284210
					match => $match,
Packit 284210
					field => join $delim, @traverse,
Packit 284210
				});
Packit 284210
				push(@values, @match) if @match;
Packit 284210
			}
Packit 284210
		} elsif (ref $hash eq 'HASH' && defined $hash->{$level}) {
Packit 284210
			$hash = $hash->{$level};
Packit 284210
		} else {
Packit 284210
			$hash = undef;
Packit 284210
			last;
Packit 284210
		}
Packit 284210
	}
Packit 284210
Packit 284210
	push @values, $hash if defined $hash;
Packit 284210
	return ref $hash eq 'ARRAY' ? @{$hash} : @values;
Packit 284210
}
Packit 284210
Packit 284210
# merge any custom-defined filters with shortcut options
Packit 284210
sub merge_filters {
Packit 284210
	my ($args)  = @_;
Packit 284210
	my $filters = $args->{filters};
Packit 284210
	my $delim   = $args->{delim};
Packit 284210
Packit 284210
	my $lookup = {
Packit 284210
		host           => [qw(request headers Host)],
Packit 284210
		transaction_id => [qw(transaction transaction_id)],
Packit 284210
		rule_id        => [qw(matched_rules % rules % actionset id)]
Packit 284210
	};
Packit 284210
Packit 284210
	for my $field (keys %{$lookup}) {
Packit 284210
		if (defined $args->{$field}) {
Packit 284210
			my $key = build_filter_key({
Packit 284210
				elements => $lookup->{$field},
Packit 284210
				delim    => $delim,
Packit 284210
			});
Packit 284210
Packit 284210
			$filters->{$key} = $args->{$field};
Packit 284210
		}
Packit 284210
	}
Packit 284210
}
Packit 284210
Packit 284210
# stub sub to build a filter key
Packit 284210
sub build_filter_key {
Packit 284210
	my ($args)   = @_;
Packit 284210
	my $elements = $args->{elements};
Packit 284210
	my $delim    = $args->{delim};
Packit 284210
Packit 284210
	return join $delim, @{$elements};
Packit 284210
}
Packit 284210
Packit 284210
sub main {
Packit 284210
	my (
Packit 284210
		$host, $transaction_id, # shortcuts
Packit 284210
		$source_ip, $rule_id,   # shortcuts
Packit 284210
		%filters, $delim,       # used by filters/match/traverse to grok the input
Packit 284210
		$before, $after,        # timeframe
Packit 284210
		$logpath, $stdin,       # input
Packit 284210
		$partial_chains, $json, # output
Packit 284210
		$verbose,               # output
Packit 284210
		$fh, $parsed_ref,       # data structures
Packit 284210
	);
Packit 284210
Packit 284210
	GetOptions(
Packit 284210
		'h|help'             => sub { usage(); },
Packit 284210
		'H|host=s'           => \$host,
Packit 284210
		't|transaction-id=s' => \$transaction_id,
Packit 284210
		's|source-ip=s'      => \$source_ip,
Packit 284210
		'r|rule-id=i'        => \$rule_id,
Packit 284210
		'f|filter=s'         => \%filters,
Packit 284210
		'd|delim=s'          => \$delim,
Packit 284210
		'b|before=s'         => \$before,
Packit 284210
		'a|after=s'          => \$after,
Packit 284210
		'l|logpath=s'        => \$logpath,
Packit 284210
		'S|stdin'            => \$stdin,
Packit 284210
		'p|partial-chains'   => \$partial_chains,
Packit 284210
		'j|json'             => \$json,
Packit 284210
		'v|verbose'          => \$verbose,
Packit 284210
	) or usage();
Packit 284210
Packit 284210
	# sanity checks
Packit 284210
	die "Cannot parse both a file and stdin\n"
Packit 284210
		if defined $logpath && defined $stdin;
Packit 284210
Packit 284210
	if (defined $source_ip) {
Packit 284210
		$source_ip = NetAddr::IP->new($source_ip);
Packit 284210
		die "Invalid IP/CIDR provided for source IP argument\n"
Packit 284210
			unless $source_ip;
Packit 284210
	}
Packit 284210
Packit 284210
	# build_datetime will bail out if an invalid format was given
Packit 284210
	$before = build_datetime($before);
Packit 284210
	$after  = build_datetime($after);
Packit 284210
Packit 284210
	# figure where we're reading from
Packit 284210
	$logpath ||= '/var/log/mod_sec/modsec_audit.log';
Packit 284210
	$fh = get_input({
Packit 284210
		logpath => $logpath,
Packit 284210
		stdin   => $stdin,
Packit 284210
	});
Packit 284210
Packit 284210
	die "Could not get a handle on your data\n"
Packit 284210
		unless $fh;
Packit 284210
Packit 284210
	# build the filters by merging shortcut options with custom filter directives
Packit 284210
	$delim ||= '.';
Packit 284210
	merge_filters({
Packit 284210
		filters        => \%filters,
Packit 284210
		host           => $host,
Packit 284210
		transaction_id => $transaction_id,
Packit 284210
		source_ip      => $source_ip,
Packit 284210
		rule_id        => $rule_id,
Packit 284210
		delim          => $delim,
Packit 284210
	});
Packit 284210
Packit 284210
	# walk through our input, getting an arrayref of valid entries based on filters and timeframe
Packit 284210
	$parsed_ref = grok_input({
Packit 284210
		fh        => $fh,
Packit 284210
		filters   => \%filters,
Packit 284210
		delim     => $delim,
Packit 284210
		source_ip => $source_ip,
Packit 284210
		before    => $before,
Packit 284210
		after     => $after,
Packit 284210
		partial   => $partial_chains,
Packit 284210
		verbose   => $verbose,
Packit 284210
	});
Packit 284210
Packit 284210
	close $fh || warn $!;
Packit 284210
Packit 284210
	# show me the money!
Packit 284210
	print_matches({
Packit 284210
		ref     => $parsed_ref,
Packit 284210
		json    => $json,
Packit 284210
		verbose => $verbose,
Packit 284210
	});
Packit 284210
}
Packit 284210
Packit 284210
main();