]> git.ipfire.org Git - people/stevee/guardian.git/blob - modules/Parser.pm
Introduce priority level for snort alerts.
[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 "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 #
24 sub 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 #
40 sub 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
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 #
67 sub Parser ($$) {
68 my $self = shift;
69 my ($parser, @message) = @_;
70
71 # If no responsible message parser could be found, just return nothing.
72 unless (exists($logfile_parsers{$parser})) {
73 return;
74 }
75
76 # Call responsible message parser.
77 my @actions = $logfile_parsers{$parser}->($self, @message);
78
79 # In case an action has been returned, return it too.
80 if (@actions) {
81 # Return which actions should be performed.
82 return @actions;
83 }
84
85 # Return undef, if no actions are required.
86 return undef;
87 }
88
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 #
98 sub 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
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 #
118 ## XXX Currently the parser only supports IPv4. Add support for IPv6 at a
119 ## later time.
120 #
121 sub message_parser_snort(@) {
122 my $self = shift;
123 my @message = @_;
124 my @actions;
125
126 # Temporary array to store single alerts.
127 my @alert;
128
129 # The name of the parser module.
130 my $name = "SNORT";
131
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
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.
140 foreach my $line (@message) {
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;
154 my $classification;
155
156 # Loop through all lines of the current alert.
157 foreach my $line (@alert) {
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 }
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
186 # Grab the reason from a msg field of the alert.
187 if ($line =~ /.*msg:\"(.*)\".*/) {
188 # Store the extracted message.
189 $message = $1;
190 }
191
192 # If the reason could not be determined, try to obtain it from the headline of the alert.
193 elsif ($line =~ /.*\] (.*) \[\*\*\]/) {
194 # Store the extracted message.
195 $message = $1;
196 }
197 }
198
199 # Check if at least the IP-address information has been extracted.
200 if ((defined ($classification)) && (defined ($address))) {
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 = ();
209 }
210 }
211
212 # If any actions are required, return the array.
213 if (@actions) {
214 return (@actions);
215 }
216
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.
219 return;
220 }
221
222 #
223 ## The SSH message parser.
224 #
225 ## This subfunction is used for parsing and detecting different attacks
226 ## against the SSH service.
227 #
228 sub message_parser_ssh (@) {
229 my $self = shift;
230 my @message = @_;
231 my @actions;
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 }
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 }
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 HTTPD message parser.
283 #
284 ## This subfunction is used for parsing and detecting different attacks
285 ## against a running HTTPD service.
286 #
287 sub message_parser_httpd (@) {
288 my $self = shift;
289 my @message = @_;
290 my @actions;
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).
305 if ($line =~ /.*\[error\] \[client (.*)\] user(.*) not found:.*/) {
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.
314 elsif ($line =~ /.*\[error\] \[client (.*)\] user(.*): authentication failure for.*/) {
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 }
321
322 # Check if at least the IP-address information has been extracted.
323 if (defined ($address)) {
324 # Add the extracted values and event message to the actions array.
325 push(@actions, "count $address $name $message");
326 }
327 }
328
329 # If any actions are required, return the array.
330 if (@actions) {
331 return @actions;
332 }
333
334 # If we got here, the provided message is not affected by any filter and
335 # therefore can be skipped. Return nothing (False) in this case.
336 return;
337 }
338
339 #
340 ## The Owncloud message parser.
341 #
342 ## This subfunction is used for parsing and detecting brute-force login
343 ## attempts against a local running owncloud instance.
344 #
345 sub message_parser_owncloud (@) {
346 my @message = @_;
347 my @actions;
348
349 # The name of the parser module.
350 my $name = "Owncloud";
351
352 # Variable to store the grabbed IP-address.
353 my $address;
354
355 # Variable to store the parsed event.
356 my $message;
357
358 # Loop through all lines, in case multiple one have
359 # been passed.
360 foreach my $line (@message) {
361 # This will catch brute-force attacks against the login (username).
362 if ($line =~/.*\"Login failed: \'(.*)\' \(Remote IP: \'(.*)\'\,.*/) {
363 # Store the grabbed user name.
364 my $user = $1;
365
366 # Store the grabbed IP-address.
367 $address = $2;
368
369 # Set event message.
370 $message = "Possible brute-force attack, wrong password for user: $user.";
371 }
372
373 # Check if at least the IP-address information has been extracted.
374 if (defined ($address)) {
375 # Add the extracted values and event message to the actions array.
376 push(@actions, "count $address $name $message");
377 }
378 }
379
380 # If any actions are required, return the array.
381 if (@actions) {
382 return @actions;
383 }
384
385 # If we got here, the provided message is not affected by any filter and
386 # therefore can be skipped. Return nothing (False) in this case.
387 return;
388 }
389
390 1;