File Coverage

blib/lib/Shadowd/Connector.pm
Criterion Covered Total %
statement 64 157 40.7
branch 0 58 0.0
condition 0 6 0.0
subroutine 21 31 67.7
pod 14 14 100.0
total 99 266 37.2


line stmt bran cond sub pod time code
1             package Shadowd::Connector;
2              
3 4     4   95613 use 5.010;
  4         17  
4 4     4   24 use strict;
  4         7  
  4         99  
5              
6 4     4   4397 use JSON;
  4         68068  
  4         23  
7 4     4   5398 use Config::IniFiles;
  4         149133  
  4         208  
8 4     4   3771 use IO::Socket;
  4         66418  
  4         18  
9 4     4   8217 use IO::Socket::SSL;
  4         306394  
  4         36  
10 4     4   4100 use Crypt::Mac::HMAC qw(hmac_hex);
  4         23184  
  4         244  
11 4     4   2991 use Attribute::Abstract;
  4         19859  
  4         146  
12 4     4   29 use POSIX qw(strftime);
  4         10  
  4         41  
13              
14             use constant {
15 4         1086 SHADOWD_CONNECTOR_VERSION => '2.0.0-perl',
16             SHADOWD_CONNECTOR_CONFIG => '/etc/shadowd/connectors.ini',
17             SHADOWD_CONNECTOR_CONFIG_SECTION => 'shadowd_perl',
18             SHADOWD_LOG => '/var/log/shadowd.log',
19             STATUS_OK => 1,
20             STATUS_BAD_REQUEST => 2,
21             STATUS_BAD_SIGNATURE => 3,
22             STATUS_BAD_JSON => 4,
23             STATUS_ATTACK => 5,
24             STATUS_CRITICAL_ATTACK => 6
25 4     4   425 };
  4         9  
26              
27             =head1 NAME
28              
29             Shadowd::Connector - Shadow Daemon Connector (Base)
30              
31             =head1 VERSION
32              
33             Version 2.0.0
34              
35             =cut
36              
37             our $VERSION = '2.0.0';
38              
39             =head1 SYNOPSIS
40              
41             B is a collection of tools to I, I and I I on I.
42             Technically speaking, Shadow Daemon is a B that intercepts requests and filters out malicious parameters.
43             It is a modular system that separates web application, analysis and interface to increase security, flexibility and expandability.
44              
45             I is the base class to connect Perl applications with the Shadow Daemon background server. It is not possible
46             to use this module directly, because there are abstract methods that have to be implemented.
47              
48             =cut
49              
50             =head1 METHODS
51              
52             =head2 new()
53              
54             This method is a simple constructor for an object oriented interface.
55              
56             =cut
57              
58             sub new {
59 3     3 1 191 my ($class) = @_;
60 3         7 my $self = {};
61              
62 3         7 bless $self, $class;
63 3         11 return $self;
64             }
65              
66             =head2 get_client_ip()
67              
68             This is an abstract method that has to be implemented by a subclass. It has to return the IP address of the client.
69              
70             =cut
71              
72 4     4   23 sub get_client_ip: Abstract;
  4         8  
  4         20  
73              
74             =head2 get_caller()
75              
76             This is an abstract method that has to be implemented by a subclass. It has to return the name of the caller.
77              
78             =cut
79              
80 4     4   737 sub get_caller: Abstract;
  4         8  
  4         16  
81              
82             =head2 get_resource()
83              
84             This is an abstract method that has to be implemented by a subclass. It has to return the the requested resource.
85              
86             =cut
87              
88 4     4   661 sub get_resource: Abstract;
  4         8  
  4         16  
89              
90             =head2 gather_input()
91              
92             This is an abstract method that has to be implemented by a subclass. It has to save the user input in the class attribute I<_input>.
93              
94             =cut
95              
96 4     4   675 sub gather_input: Abstract;
  4         9  
  4         17  
97              
98             =head2 defuse_input($threats)
99              
100             This is an abstract method that has to be implemented by a subclass. It has to remove threats from the user input.
101              
102             =cut
103              
104 4     4   658 sub defuse_input: Abstract;
  4         14  
  4         17  
105              
106             =head2 gather_hashes()
107              
108             This is an abstract method that has to be implemented by a subclass. It has to save the cryptographically secure checksums of the
109             executed script in the class attribute I<_hashes>.
110              
111             =cut
112              
113 4     4   645 sub gather_hashes: Abstract;
  4         9  
  4         17  
114              
115             =head2 error()
116              
117             This is an abstract method that has to be implemented by a subclass. It has to display an error message.
118              
119             =cut
120              
121 4     4   670 sub error: Abstract;
  4         8  
  4         15  
122              
123             =head2 init_config()
124              
125             This method initializes and loads the configuration.
126              
127             =cut
128              
129             sub init_config {
130 0     0 1 0 my ($self) = @_;
131              
132 0 0       0 if (defined $ENV{'SHADOWD_CONNECTOR_CONFIG'}) {
133 0         0 $self->{'_config_file'} = $ENV{'SHADOWD_CONNECTOR_CONFIG'};
134             } else {
135 0         0 $self->{'_config_file'} = SHADOWD_CONNECTOR_CONFIG;
136             }
137              
138 0         0 $self->{'_config'} = Config::IniFiles->new(-file => $self->{'_config_file'});
139              
140 0 0       0 if (!$self->{'_config'}) {
141 0         0 die('config error');
142             }
143              
144 0 0       0 if (defined $ENV{'SHADOWD_CONNECTOR_CONFIG_SECTION'}) {
145 0         0 $self->{'_config_section'} = $ENV{'SHADOWD_CONNECTOR_CONFIG_SECTION'};
146             } else {
147 0         0 $self->{'_config_section'} = SHADOWD_CONNECTOR_CONFIG_SECTION;
148             }
149             }
150              
151             =head2 get_config($key, $required, $default)
152              
153             This method returns values from the configuration.
154              
155             =cut
156              
157             sub get_config {
158 0     0 1 0 my ($self, $key, $required, $default) = @_;
159              
160 0 0       0 if (!$self->{'_config'}->exists($self->{'_config_section'}, $key)) {
161 0 0       0 if ($required) {
162 0         0 die($key . ' in config missing');
163             } else {
164 0         0 return $default;
165             }
166             } else {
167 0         0 return $self->{'_config'}->val($self->{'_config_section'}, $key);
168             }
169             }
170              
171             =head2 get_input()
172              
173             This method returns the user input that is brought together by I.
174              
175             =cut
176              
177             sub get_input {
178 0     0 1 0 my ($self) = @_;
179              
180 0         0 return $self->{'_input'}
181             }
182              
183             =head2 get_hashes()
184              
185             This method returns the hashes that are brought together by I.
186              
187             =cut
188              
189             sub get_hashes {
190 0     0 1 0 my ($self) = @_;
191              
192 0         0 return $self->{'_hashes'}
193             }
194              
195             =head2 remove_ignored($file)
196              
197             The method removes user input that should be ignored from the class attribute I<_input>.
198              
199             =cut
200              
201             sub remove_ignored {
202 0     0 1 0 my ($self, $file) = @_;
203              
204 0         0 local $/ = undef;
205 0 0       0 open my $handler, $file or die('could not open ignore file: ' . $!);
206 0         0 binmode $handler;
207              
208 0         0 my $content = <$handler>;
209 0         0 my $json = decode_json($content);
210              
211 0         0 foreach my $entry (@$json) {
212 0 0 0     0 if (!defined $entry->{'path'} && defined $entry->{'caller'}) {
213 0 0       0 if ($self->{'_caller'} eq $entry->{'caller'}) {
214 0         0 $self->{'_input'} = {};
215              
216 0         0 last;
217             }
218             } else {
219 0 0       0 if (defined $entry->{'caller'}) {
220 0 0       0 if ($self->{'_caller'} ne $entry->{'caller'}) {
221 0         0 next;
222             }
223             }
224              
225 0 0       0 if (defined $entry->{'path'}) {
226 0         0 delete $self->{'_input'}->{$entry->{'path'}};
227             }
228             }
229             }
230              
231 0         0 close $handler;
232             }
233              
234             =head2 send_input($host, $port, $profile, $key, $ssl)
235              
236             This method sends the user input to the background server and return the parsed response.
237              
238             =cut
239              
240             sub send_input {
241 0     0 1 0 my ($self, $host, $port, $profile, $key, $ssl) = @_;
242              
243 0         0 my $connection;
244              
245 0 0       0 if ($ssl) {
246 0 0       0 $connection = IO::Socket::SSL->new(
247             PeerHost => $host,
248             PeerPort => $port,
249             SSL_verify_mode => SSL_VERIFY_PEER,
250             SSL_ca_file => $ssl
251             ) or die('network error (ssl): ' . $!);
252             } else {
253 0 0       0 $connection = IO::Socket::INET->new(
254             PeerAddr => $host,
255             PeerPort => $port
256             ) or die('network error: ' . $!);
257             }
258              
259 0         0 $connection->autoflush(1);
260              
261 0         0 my %input_data = (
262             'version' => SHADOWD_CONNECTOR_VERSION,
263             'client_ip' => $self->get_client_ip,
264             'caller' => $self->get_caller,
265             'resource' => $self->get_resource,
266             'input' => $self->get_input,
267             'hashes' => $self->get_hashes
268             );
269              
270 0         0 my $json = JSON->new->allow_nonref;
271 0         0 $json->allow_blessed();
272 0         0 my $json_text = $json->encode(\%input_data);
273              
274 0         0 print $connection $profile . "\n" . $self->sign($key, $json_text) . "\n" . $json_text . "\n";
275              
276 0         0 my $output = <$connection>;
277              
278 0         0 close $connection;
279              
280 0         0 return $self->parse_output($output);
281             }
282              
283             =head2 parse_output($output)
284              
285             This method parses the response of the background server.
286              
287             =cut
288              
289             sub parse_output {
290 0     0 1 0 my ($self, $output) = @_;
291              
292 0         0 my $output_data = decode_json($output);
293              
294 0 0       0 if ($output_data->{'status'} eq STATUS_OK) {
    0          
    0          
    0          
    0          
    0          
295             return {
296 0         0 'attack' => 0
297             };
298             } elsif ($output_data->{'status'} eq STATUS_BAD_REQUEST) {
299 0         0 die('bad request');
300             } elsif ($output_data->{'status'} eq STATUS_BAD_SIGNATURE) {
301 0         0 die('bad signature');
302             } elsif ($output_data->{'status'} eq STATUS_BAD_JSON) {
303 0         0 die('bad json');
304             } elsif ($output_data->{'status'} eq STATUS_ATTACK) {
305             return {
306             'attack' => 1,
307             'critical' => 0,
308 0         0 'threats' => $output_data->{'threats'}
309             };
310             } elsif ($output_data->{'status'} eq STATUS_CRITICAL_ATTACK) {
311             return {
312 0         0 'attack' => 1,
313             'critical' => 1
314             };
315             } else {
316 0         0 die('processing error');
317             }
318             }
319              
320             =head2 sign($key, $json)
321              
322             This method signs the input with a secret key to authenticate requests without having to send the password.
323              
324             =cut
325              
326             sub sign {
327 0     0 1 0 my ($self, $key, $json) = @_;
328              
329 0         0 return hmac_hex('SHA256', $key, $json);
330             }
331              
332             =head2 log($message)
333              
334             This method writes messages to a log file.
335              
336             =cut
337              
338             sub log {
339 0     0 1 0 my ($self, $message) = @_;
340              
341 0         0 my $file = $self->get_config('log', 0, SHADOWD_LOG);
342 0 0       0 open my $handler, '>>' . $file or die('could not open log file: ' . $!);
343              
344 0         0 chomp($message);
345 0         0 my $datetime = strftime('%Y-%m-%d %H:%M:%S', localtime);
346 0         0 print $handler $datetime . "\t" . $message . "\n";
347              
348 0         0 close $handler;
349             }
350              
351             =head2 escape_key($key)
352              
353             This method escapes keys, i.e. single elements of a path.
354              
355             =cut
356              
357             sub escape_key {
358 5     5 1 13 my ($self, $key) = @_;
359              
360 5         13 $key =~ s/\\/\\\\/g;
361 5         31 $key =~ s/\|/\\|/g;
362              
363 5         24 return $key;
364             }
365              
366             =head2 unescape_key($key)
367              
368             This method unescapes keys, i.e. single elements of a path.
369              
370             =cut
371              
372             sub unescape_key {
373 4     4 1 14 my ($self, $key) = @_;
374              
375 4         13 $key =~ s/\\\\/\\/g;
376 4         34 $key =~ s/\\\|/|/g;
377              
378 4         26 return $key;
379             }
380              
381             =head2 split_path($path)
382              
383             This method splits a path into keys.
384              
385             =cut
386              
387             sub split_path {
388 6     6 1 1301 my ($self, $path) = @_;
389              
390 6         52 return split(/\\.(*SKIP)(*FAIL)|\|/s, $path);
391             }
392              
393             =head2 start()
394              
395             This method connects the different components of the module and starts the complete protection process.
396              
397             =cut
398              
399             sub start {
400 0     0 1   my ($self) = @_;
401              
402 0           eval {
403 0           $self->init_config;
404              
405 0           $self->gather_input;
406 0           $self->gather_hashes;
407              
408 0           my $ignored = $self->get_config('ignore');
409 0 0         if ($ignored) {
410 0           $self->remove_ignored($ignored);
411             }
412              
413 0           my $status = $self->send_input(
414             $self->get_config('host', 0, '127.0.0.1'),
415             $self->get_config('port', 0, '9115'),
416             $self->get_config('profile', 1),
417             $self->get_config('key', 1),
418             $self->get_config('ssl')
419             );
420              
421 0 0 0       if (!$self->get_config('observe') && $status->{'attack'}) {
422 0 0         if ($status->{'critical'}) {
423 0           die('shadowd: stopped critical attack from client: ' . $self->get_client_ip);
424             }
425              
426 0 0         if (!$self->defuse_input($status->{'threats'})) {
427 0           die('shadowd: stopped attack from client: ' . $self->get_client_ip);
428             }
429              
430 0 0         if ($self->get_config('debug')) {
431 0           $self->log('shadowd: removed threat from client: ' . $self->get_client_ip);
432             }
433             }
434             };
435              
436 0 0         if ($@) {
437 0 0         if ($self->get_config('debug')) {
438 0           $self->log($@);
439             }
440              
441 0 0         unless ($self->get_config('observe')) {
442 0           $self->error;
443 0           return undef;
444             }
445             }
446              
447 0           return 1;
448             }
449              
450             =head1 AUTHOR
451              
452             Hendrik Buchwald, C<< >>
453              
454             =head1 BUGS
455              
456             Please report any bugs or feature requests to C, or through the web interface at
457             L. I will be notified, and then you'll automatically
458             be notified of progress on your bug as I make changes.
459              
460             It is also possible to report bugs via Github at L.
461              
462             =head1 SUPPORT
463              
464             You can find documentation for this module with the perldoc command.
465              
466             perldoc Shadowd::Connector
467              
468              
469             You can also look for information at:
470              
471             =over 4
472              
473             =item * RT: CPAN's request tracker (report bugs here)
474              
475             L
476              
477             =item * AnnoCPAN: Annotated CPAN documentation
478              
479             L
480              
481             =item * CPAN Ratings
482              
483             L
484              
485             =item * Search CPAN
486              
487             L
488              
489             =back
490              
491             =head1 LICENSE AND COPYRIGHT
492              
493             Shadow Daemon -- Web Application Firewall
494              
495             Copyright (C) 2014-2016 Hendrik Buchwald C<< >>
496              
497             This file is part of Shadow Daemon. Shadow Daemon is free software: you can
498             redistribute it and/or modify it under the terms of the GNU General Public
499             License as published by the Free Software Foundation, version 2.
500              
501             This program is distributed in the hope that it will be useful, but WITHOUT
502             ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
503             FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
504             details.
505              
506             You should have received a copy of the GNU General Public License
507             along with this program. If not, see L.
508              
509             =cut
510              
511             1; # End of Shadowd::Connector