]> git.ipfire.org Git - people/stevee/guardian.git/blame - modules/Parser.pm
Parser: Adjust HTTP parser to be compatible with newer log format.
[people/stevee/guardian.git] / modules / Parser.pm
CommitLineData
88d9af2c
SS
1package Guardian::Parser;
2use strict;
3use warnings;
4
5use Exporter qw(import);
6
cfe5a220 7our @EXPORT_OK = qw(IsSupportedParser Parser);
88d9af2c 8
cfe5a220
SS
9# This hash contains all supported parsers and which function
10# has to be called to parse messages in the right way.
88d9af2c 11my %logfile_parsers = (
55852ce7 12 "httpd" => \&message_parser_httpd,
7306aaf8 13 "owncloud" => \&message_parser_owncloud,
cfe5a220 14 "snort" => \&message_parser_snort,
0b1fe046 15 "ssh" => \&message_parser_ssh,
88d9af2c
SS
16);
17
df8eb305
SS
18#
19## The "Init" (Parser) function.
20#
21## This function is responsible to initialize the Parser as a class based object.
22## It has to be called once before any parsing of messages can be done.
23#
24sub Init (%) {
25 my ( $class, %args ) = @_;
26 my $self = \%args;
27
28 # Use bless to make "$self" to an object of class "$class".
29 bless($self, $class);
30
31 # Return the class object.
32 return $self;
33}
34
35#
36## The "Update" Parser settings function.
37#
38## This object based function is called to update various class settings.
39#
40sub Update (\%) {
41 my $self = shift;
42
43 # Dereference the given hash-ref and store
44 # the values into a new temporary hash.
45 my %settings = %{ $_[0] };
46
47 # Update snort priority level settings or disable it.
48 if ((defined($self->{SnortPriorityLevel})) && (exists($settings{SnortPriorityLevel}))) {
49 # Change settings.
50 $self->{SnortPriorityLevel} = $settings{SnortPriorityLevel};
51 } else {
52 # Remove setting.
53 delete $self->{SnortPriorityLevel};
54 }
55
56 # Return modified class object.
57 return $self;
58}
59
88d9af2c
SS
60#
61## The main parsing function.
62#
63## It is used to determine which sub-parser has to be used to
64## parse the given message in the right way and to return if
65## any action should be performed.
66#
67sub Parser ($$) {
df8eb305
SS
68 my $self = shift;
69 my ($parser, @message) = @_;
88d9af2c
SS
70
71 # If no responsible message parser could be found, just return nothing.
cfe5a220 72 unless (exists($logfile_parsers{$parser})) {
88d9af2c
SS
73 return;
74 }
75
cfe5a220 76 # Call responsible message parser.
df8eb305 77 my @actions = $logfile_parsers{$parser}->($self, @message);
88d9af2c 78
8fba3c57 79 # In case an action has been returned, return it too.
43fdb161
SS
80 if (@actions) {
81 # Return which actions should be performed.
82 return @actions;
8fba3c57
SS
83 }
84
43fdb161 85 # Return undef, if no actions are required.
8fba3c57 86 return undef;
88d9af2c
SS
87}
88
cfe5a220
SS
89#
90## IsSupportedParser function.
91#
92## This very tiny function checks if a given parser name is available and
93## therefore a supported parser.
94#
95## To perform these check, the function is going to lookup if a key in the
96## hash of supported parsers is available
97#
98sub IsSupportedParser ($) {
99 my $parser = $_[0];
100
101 # Check if a key for the given parser exists in the hash of logfile_parsers.
102 if(exists($logfile_parsers{$parser})) {
103 # Found a valid parser, so return nothing.
104 return 1;
105 }
106
107 # Return "False" if we got here, and therefore no parser
108 # is available.
109 return;
110}
111
88d9af2c
SS
112#
113## The Snort message parser.
114#
115## This subfunction is responsible for parsing sort alerts and determine if
116## an action should be performed.
117#
53a02397
SS
118## XXX Currently the parser only supports IPv4. Add support for IPv6 at a
119## later time.
120#
121sub message_parser_snort(@) {
df8eb305 122 my $self = shift;
88d9af2c 123 my @message = @_;
8bc363c5
SS
124 my @actions;
125
126 # Temporary array to store single alerts.
127 my @alert;
88d9af2c 128
53a02397
SS
129 # The name of the parser module.
130 my $name = "SNORT";
131
53a02397
SS
132 # Default returned message in case no one could be grabbed
133 # from the snort alert.
134 my $message = "An active snort rule has matched and gained an alert.";
135
8bc363c5
SS
136 # Snort uses a log buffer and a result of this, when detecting multiple
137 # events at once, multiple alerts will be written at one time to the alert
138 # file. They have to be seperated from each, to be able to parse them
139 # individually.
53a02397 140 foreach my $line (@message) {
8bc363c5
SS
141 # Remove any newlines.
142 chomp($line);
143
144 # A single alert contains multiple lines, push all of them
145 # a temporary array.
146 push(@alert, $line);
147
148 # Each alert ends with an empty line, if one is found,
149 # all lines of the current processed alert have been found
150 # and pushed to the temporary array.
151 if($line =~ /^\s*$/) {
152 # Variable to store the grabbed IP-address.
153 my $address;
df8eb305 154 my $classification;
8bc363c5
SS
155
156 # Loop through all lines of the current alert.
157 foreach my $line (@alert) {
df8eb305
SS
158 # Determine if the alert has been classified.
159 if ($line =~ /.*\[Classification: .*\] \[Priority: (\d+)\].*/) {
160 my $priority = $1;
161
162 # Set classification to true.
163 $classification = "1";
164
165 # Obtain configured priority level.
166 my $priority_level = $self->{SnortPriorityLevel};
167
168 # Skip alerts if the priority is to low.
169 if ($priority < $priority_level) {
170 last;
171 }
172 }
8bc363c5
SS
173
174 # Search for a line like xxx.xxx.xxx.xxx -> xxx.xxx.xxx.xxx
175 if ($line =~ /(\d+\.\d+\.\d+\.\d+)+ -\> (\d+\.\d+\.\d+\.\d+)+/) {
176 # Store the grabbed IP-address.
177 $address = $1;
178 }
179
180 # Search for a line like xxx.xxx.xxx.xxx:xxx -> xxx.xxx.xxx.xxx:xxx
181 elsif ($line =~ /(\d+\.\d+\.\d+\.\d+):\d+ -\> (\d+\.\d+\.\d+\.\d+):\d+/) {
182 # Store the obtained IP-address.
183 $address = $1;
184 }
185
df8eb305
SS
186 # Grab the reason from a msg field of the alert.
187 if ($line =~ /.*msg:\"(.*)\".*/) {
8bc363c5
SS
188 # Store the extracted message.
189 $message = $1;
190 }
191
df8eb305
SS
192 # If the reason could not be determined, try to obtain it from the headline of the alert.
193 elsif ($line =~ /.*\] (.*) \[\*\*\]/) {
8bc363c5
SS
194 # Store the extracted message.
195 $message = $1;
196 }
197 }
198
199 # Check if at least the IP-address information has been extracted.
df8eb305 200 if ((defined ($classification)) && (defined ($address))) {
8bc363c5
SS
201 # Add the extracted values and event message for the computed
202 # event to the actions array.
203 push(@actions, "count $address $name $message");
204 }
205
206 # The alert has been processed, clear the temporary array for storing
207 # the next alert.
208 @alert = ();
53a02397
SS
209 }
210 }
211
8bc363c5
SS
212 # If any actions are required, return the array.
213 if (@actions) {
214 return (@actions);
53a02397
SS
215 }
216
8bc363c5
SS
217 # If we got here, the alert could not be parsed correctly, or did not match any filter.
218 # Therefore it can be skipped - return nothing.
53a02397 219 return;
88d9af2c
SS
220}
221
0b1fe046
SS
222#
223## The SSH message parser.
224#
225## This subfunction is used for parsing and detecting different attacks
226## against the SSH service.
227#
228sub message_parser_ssh (@) {
df8eb305 229 my $self = shift;
0b1fe046 230 my @message = @_;
43fdb161 231 my @actions;
0b1fe046
SS
232
233 # The name of the parser module.
234 my $name = "SSH";
235
236 # Variable to store the grabbed IP-address.
237 my $address;
238
239 # Variable to store the parsed event.
240 my $message;
241
242 # Loop through all lines, in case multiple one have
243 # been passed.
244 foreach my $line (@message) {
245 # Check for failed password attempts.
246 if ($line =~/.*sshd.*Failed password for (.*) from (.*) port.*/) {
247 # Store the grabbed IP-address.
248 $address = $2;
249
250 # Set event message.
251 $message = "Possible SSH-Bruteforce Attack for user: $1.";
252 }
253
254 # This should catch Bruteforce Attacks with enabled preauth
255 elsif ($line =~ /.*sshd.*Received disconnect from (.*):.*\[preauth\]/) {
256 # Store obtained IP-address.
257 $address = $1;
258
259 # Set event message.
260 $message = "Possible SSH-Bruteforce Attack - failed preauth.";
261 }
43fdb161
SS
262
263 # Check if at least the IP-address information has been extracted.
264 if (defined ($address)) {
265 # Add the extracted values and event message for the computed
266 # event to the actions array.
267 push(@actions, "count $address $name $message");
268 }
0b1fe046
SS
269 }
270
43fdb161
SS
271 # If any actions are required, return the array.
272 if (@actions) {
273 return (@actions);
0b1fe046
SS
274 }
275
276 # If we got here, the provided message is not affected by any filter and
277 # therefore can be skipped. Return nothing (False) in this case.
278 return;
279}
280
55852ce7
SS
281#
282## The HTTPD message parser.
283#
284## This subfunction is used for parsing and detecting different attacks
285## against a running HTTPD service.
286#
287sub message_parser_httpd (@) {
df8eb305 288 my $self = shift;
55852ce7 289 my @message = @_;
43fdb161 290 my @actions;
55852ce7
SS
291
292 # The name of the parser module.
293 my $name = "HTTPD";
294
295 # Variable to store the grabbed IP-address.
296 my $address;
297
298 # Variable to store the parsed event.
299 my $message;
300
301 # Loop through all lines, in case multiple one have
302 # been passed.
303 foreach my $line (@message) {
304 # This will catch brute-force attacks against htaccess logins (username).
5028c7fd 305 if ($line =~ /.*\[client (.*)\] .* user(.*) not found:.*/) {
55852ce7
SS
306 # Store the grabbed IP-address.
307 $address = $1;
308
309 # Set event message.
310 $message = "Possible WUI brute-force attack, wrong user: $2.";
311 }
312
313 # Detect htaccess password brute-forcing against a username.
5028c7fd 314 elsif ($line =~ /.*\[client (.*)\] .* user(.*): authentication failure for.*/) {
55852ce7
SS
315 # Store the extracted IP-address.
316 $address = $1;
317
318 # Set event message.
319 $message = "Possible WUI brute-force attack, wrong password for user: $2.";
320 }
43fdb161
SS
321
322 # Check if at least the IP-address information has been extracted.
323 if (defined ($address)) {
5028c7fd
SS
324 # Check if the address also contains a port value.
325 if ($address =~ m/:/) {
326 my ($add_address, $port) = split(/:/, $address);
327
328 # Only process the address.
329 $address = $add_address;
330 }
331
43fdb161
SS
332 # Add the extracted values and event message to the actions array.
333 push(@actions, "count $address $name $message");
334 }
55852ce7
SS
335 }
336
43fdb161
SS
337 # If any actions are required, return the array.
338 if (@actions) {
339 return @actions;
55852ce7
SS
340 }
341
342 # If we got here, the provided message is not affected by any filter and
343 # therefore can be skipped. Return nothing (False) in this case.
344 return;
345}
346
7306aaf8
SS
347#
348## The Owncloud message parser.
349#
350## This subfunction is used for parsing and detecting brute-force login
351## attempts against a local running owncloud instance.
352#
353sub message_parser_owncloud (@) {
354 my @message = @_;
355 my @actions;
356
357 # The name of the parser module.
358 my $name = "Owncloud";
359
360 # Variable to store the grabbed IP-address.
361 my $address;
362
363 # Variable to store the parsed event.
364 my $message;
365
366 # Loop through all lines, in case multiple one have
367 # been passed.
368 foreach my $line (@message) {
369 # This will catch brute-force attacks against the login (username).
370 if ($line =~/.*\"Login failed: \'(.*)\' \(Remote IP: \'(.*)\'\,.*/) {
371 # Store the grabbed user name.
372 my $user = $1;
373
374 # Store the grabbed IP-address.
375 $address = $2;
376
377 # Set event message.
378 $message = "Possible brute-force attack, wrong password for user: $user.";
379 }
380
381 # Check if at least the IP-address information has been extracted.
382 if (defined ($address)) {
383 # Add the extracted values and event message to the actions array.
384 push(@actions, "count $address $name $message");
385 }
386 }
387
388 # If any actions are required, return the array.
389 if (@actions) {
390 return @actions;
391 }
392
393 # If we got here, the provided message is not affected by any filter and
394 # therefore can be skipped. Return nothing (False) in this case.
395 return;
396}
397
88d9af2c 3981;