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