|
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();
|