Blame tools/parse_modsec.pl

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