File Coverage

blib/lib/Ixchel/Actions/suricata_include.pm
Criterion Covered Total %
statement 20 216 9.2
branch 0 96 0.0
condition 0 21 0.0
subroutine 7 13 53.8
pod 3 6 50.0
total 30 352 8.5


line stmt bran cond sub pod time code
1             package Ixchel::Actions::suricata_include;
2              
3 1     1   87581 use 5.006;
  1         2  
4 1     1   5 use strict;
  1         1  
  1         34  
5 1     1   5 use warnings;
  1         2  
  1         51  
6 1     1   435 use File::Slurp;
  1         30909  
  1         63  
7 1     1   310 use YAML::XS qw(Dump);
  1         2571  
  1         52  
8 1     1   6 use base 'Ixchel::Actions::base';
  1         1  
  1         366  
9 1     1   398 use Sys::Hostname;
  1         997  
  1         2008  
10              
11             =head1 NAME
12              
13             Ixchel::Actions::suricata_include - Generates the instance specific include for a suricata instance.
14              
15             =head1 VERSION
16              
17             Version 0.3.2
18              
19             =cut
20              
21             our $VERSION = '0.3.1';
22              
23             =head1 CLI SYNOPSIS
24              
25             ixchel -a suricata_include [B<-i> ] [B<-d> ]
26              
27             ixchel -a suricata_include [B<-w>] [B<--np>] [B<-i> ] [B<-d> ]
28             [B<-E> ] [B<-t> ] [B<--sna> <0/1>] [B<--sno>] [B<--snl> <0/1>]
29              
30             =head1 CODE SYNOPSIS
31              
32             use Data::Dumper;
33              
34             my $results=$ixchel->action(action=>'suricata_include', opts=>{np=>1, w=>1, });
35              
36             print Dumper($results);
37              
38             =head1 DESCRIPTION
39              
40             This generates a the general purpose include for Suricata.
41              
42             The include is generated by first reading in the values under .suricata.config and
43             then if multiple instances are enabled, then .suricata.instances.$instance is merged
44             into it. Arrays are replaced with the new array while the rest are just merged using
45             L using RIGHT_PRECEDENT with the right being
46             .suricata.instances.$instance .
47              
48             If told to write it out, it will be written out to undef .suricata.config_base with the name "include.yaml"
49             or "include-$instance.yaml" if multiple instances are in use.
50              
51             =head1 AUTOMATED THREADING
52              
53             Automated threading can be enabled by setting .suricata.auto_threading.enable=1, defaults to 0.
54              
55             This requires lstopo to be available package hwloc on most OSes.
56              
57             Most cores are used for worker-cpu-set with those excluded from it being used for
58             management-cpu-set and receive-cpu-set. The count is per NUMA node. Non-NUMA are treated
59             as a single node NUMA.
60              
61             core count <= 16 = excluded 2
62             core count > 16 = excluded 3
63              
64             This can be overrided by setting .suricata.auto_threading.exclude .
65              
66             Will error if auto threading is enabled and exclude is greater than half the available cores.
67              
68             Enabled on a system with 16 cores and a exclude of 3, will result in the below.
69              
70             threading:
71             cpu-affinity:
72             - management-cpu-set:
73             cpu:
74             - 0
75             - 1
76             - 2
77             mode: balanced
78             - receive-cpu-set:
79             cpu:
80             - 0
81             - 1
82             - 2
83             mode: balanced
84             - worker-cpu-set:
85             cpu:
86             - 3
87             - 4
88             - 5
89             - 6
90             - 7
91             - 8
92             - 9
93             - 10
94             - 11
95             - 12
96             - 13
97             - 14
98             - 15
99             mode: exclusive
100             set-cpu-affinity: yes
101              
102             =head1 AUTOMATED SENSOR-NAME
103              
104             .sensor-name can be autogenerated if .suricata.auto_sensor_name.enable=1 or --sna 1 is set. For it to be
105             generated it requires .suricata.instances.$instance.sensor-name and .suricata.config.sensor-name
106             not be specified or --sno be used.
107              
108             The base of the sensor-name is set to hostname of the device, which will be shortened, removing everything
109             after the first . if .suricata.auto_sensor_name.full=0 or --snl 1.
110              
111             If this is disabled and .suricata.instances.$instance.sensor-name and .suricata.config.sensor-name
112             is not defined, this will error as there is no value for .sensor-name specified.
113              
114             =head1 FLAGS
115              
116             =head2 -w
117              
118             Write the generated services to service files.
119              
120             =head2 -i instance
121              
122             A instance to operate on.
123              
124             =head2 -d
125              
126             Use this as the base dir instead of .suricata.config_base from the config.
127              
128             =head2 -E
129              
130             Exclude per NUMA nod value.
131              
132             This will override .suricata.auto_threading.exclude as well as the default.
133              
134             =head2 -t
135              
136             Auto threading enable/disable, 0/1 boolean.
137              
138             This will override .suricata.auto_threading.enable as well as the default.
139              
140             =head2 --sna <0/1>
141              
142             Override .suricata.auto_sensor_name.enable with the specified value.
143              
144             =head2 --sno
145              
146             Disable using .suricata.instances.$instance.sensor-name or .suricata.config.sensor-name
147             for the value for .sensor-name in the generated include.
148              
149             This will automatically set --sna 1.
150              
151             =head2 --snl <0/1>
152              
153             Overrides .suricata.auto_sensor_name.full.
154              
155             =head1 RESULT HASH REF
156              
157             .errors :: A array of errors encountered.
158             .status_text :: A string description of what was done and teh results.
159             .ok :: Set to zero if any of the above errored.
160              
161             =cut
162              
163       0 0   sub new_extra { }
164              
165             sub action_extra {
166 0     0 0   my $self = $_[0];
167              
168 0           my $config_base;
169 0 0         if ( !defined( $self->{opts}{d} ) ) {
170 0           $config_base = $self->{config}{suricata}{config_base};
171             } else {
172 0 0         if ( !-d $self->{opts}{d} ) {
173             $self->status_add(
174 0           status => '-d, "' . $self->{opts}{d} . '" is not a directory',
175             error => 1,
176             );
177 0           return undef;
178             }
179 0           $config_base = $self->{opts}{d};
180             } ## end else [ if ( !defined( $self->{opts}{d} ) ) ]
181              
182 0           my $use_threading = 0;
183 0           my $threading;
184 0           eval { $threading = $self->threading; };
  0            
185 0 0         if ($@) {
186 0           $self->status_add(
187             error => 1,
188             status => 'Error figuring out threading information... ' . $@,
189             );
190             }
191 0 0         if ( ref($threading) eq 'HASH' ) {
192 0           $use_threading = 1;
193             }
194              
195 0 0         if ( $self->{config}{suricata}{multi_instance} ) {
196 0           my @instances;
197              
198 0 0         if ( defined( $self->{opts}{i} ) ) {
199 0           @instances = ( $self->{opts}{i} );
200             } else {
201 0           @instances = keys( %{ $self->{config}{suricata}{instances} } );
  0            
202             }
203 0           foreach my $instance (@instances) {
204 0           my $filled_in;
205 0           eval {
206 0           my $base_config = $self->{config}{suricata}{config};
207              
208 0 0         if ( !defined( $self->{config}{suricata}{instances}{$instance} ) ) {
209 0           $self->status_add(
210             status => $instance . ' does not exist under .suricata.instances',
211             error => 1,
212             );
213 0           return undef;
214             }
215              
216 0           my $config = $self->{config}{suricata}{instances}{$instance};
217              
218 0           my $merger = Hash::Merge->new('RIGHT_PRECEDENT');
219 0           my %tmp_config = %{$config};
  0            
220 0           my %tmp_base_config = %{$base_config};
  0            
221 0           my $merged = $merger->merge( \%tmp_base_config, \%tmp_config );
222              
223 0 0         if ($use_threading) {
224 0           $merged->{threading} = $threading;
225             }
226              
227 0           $config->{'sensor-name'} = $self->sensor_name($instance);
228              
229 0           $filled_in = '%YAML 1.1' . "\n" . Dump($merged);
230              
231 0 0         if ( $self->{opts}{w} ) {
232 0           write_file( $config_base . '/' . $instance . '-include.yaml', $filled_in );
233             }
234             };
235 0 0         if ($@) {
236 0           $self->status_add(
237             status => '-----[ Errored: '
238             . $instance
239             . ' ]-------------------------------------' . "\n" . '# '
240             . $@,
241             error => 1,
242             );
243 0           $self->{ixchel}{errors_count}++;
244             } else {
245 0           $self->status_add( status => '-----[ '
246             . $instance
247             . ' ]-------------------------------------' . "\n"
248             . $filled_in
249             . "\n" );
250             }
251             } ## end foreach my $instance (@instances)
252             } else {
253 0 0         if ( defined( $self->{opts}{i} ) ) {
254 0           $self->status_add( status => '-i may not be used in single instance mode, .suricata.multi_instance=0' );
255             }
256              
257 0           my $filled_in;
258 0           eval {
259 0           my $config = $self->{config}{suricata}{config};
260              
261 0 0         if ($use_threading) {
262 0           $config->{threading} = $threading;
263             }
264              
265 0           $config->{'sensor-name'} = $self->sensor_name;
266              
267 0           $filled_in = '%YAML 1.1' . "\n" . Dump($config);
268              
269 0 0         if ( $self->{opts}{w} ) {
270 0           write_file( $config_base . '/include.yaml', $filled_in );
271             }
272             };
273 0 0         if ($@) {
274 0           $self->status_add( status => '# ' . $@, error => 1, );
275             } else {
276 0           $self->status_add( status => $filled_in );
277             }
278             } ## end else [ if ( $self->{config}{suricata}{multi_instance...})]
279              
280 0           return undef;
281             } ## end sub action_extra
282              
283             sub short {
284 0     0 1   return 'Generates the instance specific include for a suricata instance.';
285             }
286              
287             sub opts_data {
288 0     0 1   return 'i=s
289             w
290             d=s
291             E=s
292             t=s
293             sna=s
294             sno
295             snl=s
296             ';
297             } ## end sub opts_data
298              
299             sub threading {
300 0     0 1   my $self = $_[0];
301              
302 0           my $enable = $self->{config}{suricata}{auto_threading}{enable};
303 0 0         if ( defined( $self->{opts}{t} ) ) {
304 0           $enable = $self->{opts}{t};
305             }
306              
307 0 0         if ( !$enable ) {
308 0           return undef;
309 0           $self->status_add( status => 'auto threading disabled' );
310             }
311              
312 0           $self->status_add( status => 'auto threading enabled' );
313              
314 0 0         if ( $^O eq 'linux' ) {
315 0           $self->status_add( status => 'Linux detected, checking for pinning options' );
316              
317 0           my @proc_cmdline;
318 0           eval { @proc_cmdline = read_file('/proc/cmdline'); };
  0            
319 0 0         if ($@) {
320 0           my $status = 'Failed to read /proc/cmdline ... ' . $@;
321 0           $self->status_add( error => 1, status => $status );
322 0           die($status);
323             }
324              
325 0           my @line_test = grep( /rcu_nocbs\=/, @proc_cmdline );
326 0 0         if ( defined( $line_test[0] ) ) {
327 0           my $status = '/proc/cmdline matches /rcu_nocbs=/ meaning CPU pinning is in use';
328 0           $self->status_add( error => 1, status => $status );
329 0           die($status);
330             }
331              
332 0           @line_test = grep( /nohz_full\=/, @proc_cmdline );
333 0 0         if ( defined( $line_test[0] ) ) {
334 0           my $status = '/proc/cmdline matches /nohz_full=/ meaning CPU pinning is in use';
335 0           $self->status_add( error => 1, status => $status );
336 0           die($status);
337             }
338              
339 0           @line_test = grep( /isolcpus\=/, @proc_cmdline );
340 0 0         if ( defined( $line_test[0] ) ) {
341 0           my $status = '/proc/cmdline matches /isolcpus=/ meaning CPU pinning is in use';
342 0           $self->status_add( error => 1, status => $status );
343 0           die($status);
344             }
345             } ## end if ( $^O eq 'linux' )
346              
347 0           my @lstopo_split = split( /\n/, `lstopo --no-io --no-caches` );
348 0           my @numa_check = grep( /NUMANode\ +L\#[0-9]/, @lstopo_split );
349 0           my @cores;
350 0           my $node = 0;
351 0           my $proc_count = 0;
352 0           foreach my $line (@lstopo_split) {
353 0 0         if ( $line =~ /[\ \t]+NUMANode\ +L\#[0-9]+/ ) {
    0          
354 0           $line =~ s/^.*NUMANode\ +L\#([0-9]+).*$/$1/;
355 0           $node = $line;
356             } elsif ( $line =~ /[\ \t]+PU\ +L\#[0-9]+/ ) {
357 0 0         if ( $line =~ /[\ \t]+\(P\#[0-9]+\)/ ) {
358 0           $line =~ s/^.*[\ \t]+\(P\#([0-9]+)\).*$/$1/;
359 0 0         if ( !defined( $cores[$node] ) ) {
360 0           $cores[$node] = [];
361             }
362 0           push( @{ $cores[$node] }, $line );
  0            
363 0           $proc_count++;
364             } else {
365 0           $line =~ s/^.*[\ \t]+PU\ +L\#([0-9]+).*$/$1/;
366 0 0         if ( !defined( $cores[$node] ) ) {
367 0           $cores[$node] = [];
368             }
369 0           push( @{ $cores[$node] }, $line );
  0            
370 0           $proc_count++;
371             }
372             } ## end elsif ( $line =~ /[\ \t]+PU\ +L\#[0-9]+/ )
373             } ## end foreach my $line (@lstopo_split)
374              
375 0           my $status = 'Processor Ordering: ';
376 0 0         if ( defined( $cores[1] ) ) {
377 0           $status = $status . ' [';
378             }
379 0           my $node_int = 0;
380 0           foreach my $item (@cores) {
381 0           my $core_order = join( ',', @{$item} );
  0            
382 0           $status = $status . '[' . $core_order . ']';
383 0           $node_int++;
384 0 0         if ( defined( $cores[$node_int] ) ) {
385 0           $status = $status . ',';
386             }
387             }
388 0 0         if ( defined( $cores[1] ) ) {
389 0           $status = $status . ']';
390             }
391 0           $self->status_add( status => $status );
392              
393 0           my $exclude = 2;
394 0 0         if ( !defined( $self->{opts}{E} ) ) {
395 0 0 0       if ( !defined( $self->{config}{suricata}{auto_threading}{exclude} ) && $proc_count > 16 ) {
    0          
396 0           $exclude = 3;
397             } elsif ( defined( $self->{config}{suricata}{auto_threading}{exclude} ) ) {
398 0           $exclude = $self->{config}{suricata}{auto_threading}{exclude};
399 0 0 0       if ( ref($exclude) ne '' && ref($exclude) ne 'SCALAR' ) {
400 0           die( '.suricata.auto_threading.execlude is of ref type "' . ref($exclude) . '" and not a scalar' );
401             }
402 0 0         if ( $exclude < 1 ) {
403 0           die('.suricata.auto_threading.execlude is less than 1');
404             }
405             }
406             } else {
407 0           $exclude = $self->{opts}{E};
408 0 0         if ( $exclude < 1 ) {
409 0           die('-E is less than 1');
410             }
411             }
412              
413             # Make sure $exclude is not greater than half the proc count
414 0           my $exclude_test = $proc_count / 2;
415 0 0         if ( $exclude > $exclude_test ) {
416 0           die( 'Exclude, '
417             . $exclude
418             . ', is greater than half, '
419             . $exclude_test
420             . ', the proc count,'
421             . $proc_count
422             . ',' );
423             }
424              
425             # drop by one so it is zero indexed
426 0           $exclude--;
427              
428 0           my @non_workers;
429             my @workers;
430 0           foreach my $current_node (@cores) {
431 0           my $exclude_int = 0;
432 0           foreach my $item ( @{$current_node} ) {
  0            
433 0 0         if ( $exclude_int <= $exclude ) {
434 0           push( @non_workers, $item );
435             } else {
436 0           push( @workers, $item );
437             }
438 0           $exclude_int++;
439             }
440             } ## end foreach my $current_node (@cores)
441              
442 0           $self->status_add( status => 'Non Workers: [' . join( ',', @non_workers ) . ']' );
443 0           $self->status_add( status => 'Workers: [' . join( ',', @workers ) . ']' );
444              
445 0           my $threading = {
446             'set-cpu-affinity' => 'yes',
447             'cpu-affinity' => {
448             'management-cpu-set' => {
449             mode => 'balanced',
450             cpu => [],
451             },
452             'receive-cpu-set' => {
453             mode => 'balanced',
454             cpu => [],
455             },
456             'worker-cpu-set' => {
457             mode => 'exclusive',
458             cpu => [],
459             }
460             },
461             };
462              
463 0           push( @{ $threading->{'cpu-affinity'}{'management-cpu-set'}{cpu} }, @non_workers );
  0            
464 0           push( @{ $threading->{'cpu-affinity'}{'receive-cpu-set'}{cpu} }, @non_workers );
  0            
465 0           push( @{ $threading->{'cpu-affinity'}{'worker-cpu-set'}{cpu} }, @workers );
  0            
466              
467 0           return $threading;
468             } ## end sub threading
469              
470             sub sensor_name {
471 0     0 0   my $self = $_[0];
472 0           my $instance = $_[1];
473              
474 0 0         if ( !$self->{opts}{sno} ) {
475 0 0 0       if ( defined($instance)
    0 0        
      0        
476             && defined( $self->{config}{suricata}{instances} )
477             && defined( $self->{config}{suricata}{instances}{$instance} )
478             && defined( $self->{config}{suricata}{instances}{$instance}{'sensor-name'} ) )
479             {
480 0 0 0       if ( ref( $self->{config}{suricata}{instances}{$instance}{'sensor-name'} ) ne ''
481             && ref( $self->{config}{suricata}{instances}{$instance}{'sensor-name'} ) ne 'SCALAR' )
482             {
483             die( '.suricata.instance.'
484             . $instance
485             . 'sensor-name is a ref type of '
486 0           . ref( $self->{config}{suricata}{instances}{$instance}{'sensor-name'} )
487             . ' and not ""/SCALAR' );
488             }
489 0           return $self->{config}{suricata}{instances}{$instance}{'sensor-name'};
490             } elsif ( defined( $self->{config}{suricata}{config}{'sensor-name'} ) ) {
491 0 0 0       if ( ref( $self->{config}{suricata}{config}{'sensor-name'} ) ne ''
492             && ref( $self->{config}{suricata}{config}{'sensor-name'} ) ne 'SCALAR' )
493             {
494             die( '.suricata.sensor-name is of ref type '
495 0           . ref( $self->{config}{suricata}{config}{'sensor-name'} )
496             . ' and not ""/SCALAR' );
497             }
498 0           return $self->{config}{suricata}{config}{'sensor-name'};
499             } ## end elsif ( defined( $self->{config}{suricata}{config...}))
500             } else {
501 0           $self->{opts}{sna} = 1;
502             }
503              
504 0           my $auto_sensor_name = $self->{config}{suricata}{auto_sensor_name}{enable};
505 0 0         if ( defined( $self->{opts}{sna} ) ) {
506 0           $auto_sensor_name = $self->{opts}{sna};
507             }
508              
509 0 0         if ( !$auto_sensor_name ) {
510 0           my $status
511             = 'Either --sno(or --sno --sna 0) specified or .suricata.auto_sensor_name=0 and nothing is specified by ';
512 0 0         if ( defined($instance) ) {
513 0           $status = $status . '.suricata.instances.' . $instance . '.sensor-name or ';
514             }
515 0           $status = $status . '.suricata.config.sensor-name';
516 0           die($status);
517             }
518              
519 0           my $sensor_name = hostname;
520              
521 0           my $snl = $self->{config}{suricata}{auto_sensor_name}{full};
522 0 0         if ( defined( $self->{opts}{snl} ) ) {
523 0           $snl = $self->{opts}{snl};
524             }
525 0 0         if ( !$snl ) {
526 0           $sensor_name =~ s/\..*$//;
527             }
528              
529 0 0         if ( defined($instance) ) {
530 0           $sensor_name = $sensor_name . '-' . $instance;
531             } else {
532 0           $sensor_name = $sensor_name . '-pie';
533             }
534              
535 0           return $sensor_name;
536             } ## end sub sensor_name
537              
538             1;