package Ixchel::Actions::suricata_include;

use 5.006;
use strict;
use warnings;
use File::Slurp;
use YAML::XS qw(Dump);
use base 'Ixchel::Actions::base';
use Sys::Hostname;

=head1 NAME

Ixchel::Actions::suricata_include - Generates the instance specific include for a suricata instance.

=head1 VERSION

Version 0.3.2

=cut

our $VERSION = '0.3.1';

=head1 CLI SYNOPSIS

ixchel -a suricata_include [B<-i> <instance>] [B<-d> <base_dir>]

ixchel -a suricata_include [B<-w>] [B<--np>] [B<-i> <instance>] [B<-d> <base_dir>]
[B<-E> <exclude>] [B<-t> <auto threading>] [B<--sna> <0/1>] [B<--sno>] [B<--snl> <0/1>]

=head1 CODE SYNOPSIS

    use Data::Dumper;

    my $results=$ixchel->action(action=>'suricata_include', opts=>{np=>1, w=>1, });

    print Dumper($results);

=head1 DESCRIPTION

This generates a the general purpose include for Suricata.

The include is generated by first reading in the values under .suricata.config and
then if multiple instances are enabled, then .suricata.instances.$instance is merged
into it. Arrays are replaced with the new array while the rest are just merged using
L<Hash::Merge> using RIGHT_PRECEDENT with the right being
.suricata.instances.$instance .

If told to write it out, it will be written out to undef .suricata.config_base with  the name "include.yaml"
or "include-$instance.yaml" if multiple instances are in use.

=head1 AUTOMATED THREADING

Automated threading can be enabled by setting .suricata.auto_threading.enable=1, defaults to 0.

This requires lstopo to be available package hwloc on most OSes.

Most cores are used for worker-cpu-set with those excluded from it being used for
management-cpu-set and receive-cpu-set. The count is per NUMA node. Non-NUMA are treated
as a single node NUMA.

    core count <= 16 = excluded 2
    core count >  16 = excluded 3

This can be overrided by setting .suricata.auto_threading.exclude .

Will error if auto threading is enabled and exclude is greater than half the available cores.

Enabled on a system with 16 cores and a exclude of 3, will result in the below.

    threading:
      cpu-affinity:
      - management-cpu-set:
          cpu:
          - 0
          - 1
          - 2
          mode: balanced
      - receive-cpu-set:
          cpu:
          - 0
          - 1
          - 2
          mode: balanced
      - worker-cpu-set:
          cpu:
          - 3
          - 4
          - 5
          - 6
          - 7
          - 8
          - 9
          - 10
          - 11
          - 12
          - 13
          - 14
          - 15
          mode: exclusive
      set-cpu-affinity: yes

=head1 AUTOMATED SENSOR-NAME

.sensor-name can be autogenerated if .suricata.auto_sensor_name.enable=1 or --sna 1 is set. For it to be
generated it requires .suricata.instances.$instance.sensor-name and .suricata.config.sensor-name
not be specified or --sno be used.

The base of the sensor-name is set to hostname of the device, which will be shortened, removing everything
after the first . if .suricata.auto_sensor_name.full=0 or --snl 1.

If this is disabled and .suricata.instances.$instance.sensor-name and .suricata.config.sensor-name
is not defined, this will error as there is no value for .sensor-name specified.

=head1 FLAGS

=head2 -w

Write the generated services to service files.

=head2 -i instance

A instance to operate on.

=head2 -d <base_dir>

Use this as the base dir instead of .suricata.config_base from the config.

=head2 -E <exclude>

Exclude per NUMA nod value.

This will override .suricata.auto_threading.exclude as well as the default.

=head2 -t <auto threading>

Auto threading enable/disable, 0/1 boolean.

This will override .suricata.auto_threading.enable as well as the default.

=head2 --sna <0/1>

Override .suricata.auto_sensor_name.enable with the specified value.

=head2 --sno

Disable using .suricata.instances.$instance.sensor-name or .suricata.config.sensor-name
for the value for .sensor-name in the generated include.

This will automatically set --sna 1.

=head2 --snl <0/1>

Overrides .suricata.auto_sensor_name.full.

=head1 RESULT HASH REF

    .errors :: A array of errors encountered.
    .status_text :: A string description of what was done and teh results.
    .ok :: Set to zero if any of the above errored.

=cut

sub new_extra { }

sub action_extra {
	my $self = $_[0];

	my $config_base;
	if ( !defined( $self->{opts}{d} ) ) {
		$config_base = $self->{config}{suricata}{config_base};
	} else {
		if ( !-d $self->{opts}{d} ) {
			$self->status_add(
				status => '-d, "' . $self->{opts}{d} . '" is not a directory',
				error  => 1,
			);
			return undef;
		}
		$config_base = $self->{opts}{d};
	} ## end else [ if ( !defined( $self->{opts}{d} ) ) ]

	my $use_threading = 0;
	my $threading;
	eval { $threading = $self->threading; };
	if ($@) {
		$self->status_add(
			error  => 1,
			status => 'Error figuring out threading information... ' . $@,
		);
	}
	if ( ref($threading) eq 'HASH' ) {
		$use_threading = 1;
	}

	if ( $self->{config}{suricata}{multi_instance} ) {
		my @instances;

		if ( defined( $self->{opts}{i} ) ) {
			@instances = ( $self->{opts}{i} );
		} else {
			@instances = keys( %{ $self->{config}{suricata}{instances} } );
		}
		foreach my $instance (@instances) {
			my $filled_in;
			eval {
				my $base_config = $self->{config}{suricata}{config};

				if ( !defined( $self->{config}{suricata}{instances}{$instance} ) ) {
					$self->status_add(
						status => $instance . ' does not exist under .suricata.instances',
						error  => 1,
					);
					return undef;
				}

				my $config = $self->{config}{suricata}{instances}{$instance};

				my $merger          = Hash::Merge->new('RIGHT_PRECEDENT');
				my %tmp_config      = %{$config};
				my %tmp_base_config = %{$base_config};
				my $merged          = $merger->merge( \%tmp_base_config, \%tmp_config );

				if ($use_threading) {
					$merged->{threading} = $threading;
				}

				$config->{'sensor-name'} = $self->sensor_name($instance);

				$filled_in = '%YAML 1.1' . "\n" . Dump($merged);

				if ( $self->{opts}{w} ) {
					write_file( $config_base . '/' . $instance . '-include.yaml', $filled_in );
				}
			};
			if ($@) {
				$self->status_add(
					status => '-----[ Errored: '
						. $instance
						. ' ]-------------------------------------' . "\n" . '# '
						. $@,
					error => 1,
				);
				$self->{ixchel}{errors_count}++;
			} else {
				$self->status_add( status => '-----[ '
						. $instance
						. ' ]-------------------------------------' . "\n"
						. $filled_in
						. "\n" );
			}
		} ## end foreach my $instance (@instances)
	} else {
		if ( defined( $self->{opts}{i} ) ) {
			$self->status_add( status => '-i may not be used in single instance mode, .suricata.multi_instance=0' );
		}

		my $filled_in;
		eval {
			my $config = $self->{config}{suricata}{config};

			if ($use_threading) {
				$config->{threading} = $threading;
			}

			$config->{'sensor-name'} = $self->sensor_name;

			$filled_in = '%YAML 1.1' . "\n" . Dump($config);

			if ( $self->{opts}{w} ) {
				write_file( $config_base . '/include.yaml', $filled_in );
			}
		};
		if ($@) {
			$self->status_add( status => '# ' . $@, error => 1, );
		} else {
			$self->status_add( status => $filled_in );
		}
	} ## end else [ if ( $self->{config}{suricata}{multi_instance...})]

	return undef;
} ## end sub action_extra

sub short {
	return 'Generates the instance specific include for a suricata instance.';
}

sub opts_data {
	return 'i=s
w
d=s
E=s
t=s
sna=s
sno
snl=s
';
} ## end sub opts_data

sub threading {
	my $self = $_[0];

	my $enable = $self->{config}{suricata}{auto_threading}{enable};
	if ( defined( $self->{opts}{t} ) ) {
		$enable = $self->{opts}{t};
	}

	if ( !$enable ) {
		return undef;
		$self->status_add( status => 'auto threading disabled' );
	}

	$self->status_add( status => 'auto threading enabled' );

	if ( $^O eq 'linux' ) {
		$self->status_add( status => 'Linux detected, checking for pinning options' );

		my @proc_cmdline;
		eval { @proc_cmdline = read_file('/proc/cmdline'); };
		if ($@) {
			my $status = 'Failed to read /proc/cmdline ... ' . $@;
			$self->status_add( error => 1, status => $status );
			die($status);
		}

		my @line_test = grep( /rcu_nocbs\=/, @proc_cmdline );
		if ( defined( $line_test[0] ) ) {
			my $status = '/proc/cmdline matches /rcu_nocbs=/ meaning CPU pinning is in use';
			$self->status_add( error => 1, status => $status );
			die($status);
		}

		@line_test = grep( /nohz_full\=/, @proc_cmdline );
		if ( defined( $line_test[0] ) ) {
			my $status = '/proc/cmdline matches /nohz_full=/ meaning CPU pinning is in use';
			$self->status_add( error => 1, status => $status );
			die($status);
		}

		@line_test = grep( /isolcpus\=/, @proc_cmdline );
		if ( defined( $line_test[0] ) ) {
			my $status = '/proc/cmdline matches /isolcpus=/ meaning CPU pinning is in use';
			$self->status_add( error => 1, status => $status );
			die($status);
		}
	} ## end if ( $^O eq 'linux' )

	my @lstopo_split = split( /\n/, `lstopo --no-io --no-caches` );
	my @numa_check   = grep( /NUMANode\ +L\#[0-9]/, @lstopo_split );
	my @cores;
	my $node       = 0;
	my $proc_count = 0;
	foreach my $line (@lstopo_split) {
		if ( $line =~ /[\ \t]+NUMANode\ +L\#[0-9]+/ ) {
			$line =~ s/^.*NUMANode\ +L\#([0-9]+).*$/$1/;
			$node = $line;
		} elsif ( $line =~ /[\ \t]+PU\ +L\#[0-9]+/ ) {
			if ( $line =~ /[\ \t]+\(P\#[0-9]+\)/ ) {
				$line =~ s/^.*[\ \t]+\(P\#([0-9]+)\).*$/$1/;
				if ( !defined( $cores[$node] ) ) {
					$cores[$node] = [];
				}
				push( @{ $cores[$node] }, $line );
				$proc_count++;
			} else {
				$line =~ s/^.*[\ \t]+PU\ +L\#([0-9]+).*$/$1/;
				if ( !defined( $cores[$node] ) ) {
					$cores[$node] = [];
				}
				push( @{ $cores[$node] }, $line );
				$proc_count++;
			}
		} ## end elsif ( $line =~ /[\ \t]+PU\ +L\#[0-9]+/ )
	} ## end foreach my $line (@lstopo_split)

	my $status = 'Processor Ordering: ';
	if ( defined( $cores[1] ) ) {
		$status = $status . ' [';
	}
	my $node_int = 0;
	foreach my $item (@cores) {
		my $core_order = join( ',', @{$item} );
		$status = $status . '[' . $core_order . ']';
		$node_int++;
		if ( defined( $cores[$node_int] ) ) {
			$status = $status . ',';
		}
	}
	if ( defined( $cores[1] ) ) {
		$status = $status . ']';
	}
	$self->status_add( status => $status );

	my $exclude = 2;
	if ( !defined( $self->{opts}{E} ) ) {
		if ( !defined( $self->{config}{suricata}{auto_threading}{exclude} ) && $proc_count > 16 ) {
			$exclude = 3;
		} elsif (defined( $self->{config}{suricata}{auto_threading}{exclude} )) {
			$exclude = $self->{config}{suricata}{auto_threading}{exclude};
			if ( ref($exclude) ne '' && ref($exclude) ne 'SCALAR' ) {
				die( '.suricata.auto_threading.execlude is of ref type "' . ref($exclude) . '" and not a scalar' );
			}
			if ( $exclude < 1 ) {
				die('.suricata.auto_threading.execlude is less than 1');
			}
		}
	} else {
		$exclude = $self->{opts}{E};
		if ( $exclude < 1 ) {
			die('-E is less than 1');
		}
	}

	# Make sure $exclude is not greater than half the proc count
	my $exclude_test = $proc_count / 2;
	if ( $exclude > $exclude_test ) {
		die(      'Exclude, '
				. $exclude
				. ', is greater than half, '
				. $exclude_test
				. ', the proc count,'
				. $proc_count
				. ',' );
	}

	# drop by one so it is zero indexed
	$exclude--;

	my @non_workers;
	my @workers;
	foreach my $current_node (@cores) {
		my $exclude_int = 0;
		foreach my $item ( @{$current_node} ) {
			if ( $exclude_int <= $exclude ) {
				push( @non_workers, $item );
			} else {
				push( @workers, $item );
			}
			$exclude_int++;
		}
	} ## end foreach my $current_node (@cores)

	$self->status_add( status => 'Non Workers: [' . join( ',', @non_workers ) . ']' );
	$self->status_add( status => 'Workers: [' . join( ',', @workers ) . ']' );

	my $threading = {
		'set-cpu-affinity' => 'yes',
		'cpu-affinity'     => [
			{
				'management-cpu-set' => {
					mode => 'balanced',
					cpu  => [],
				}
			},
			{
				'receive-cpu-set' => {
					mode => 'balanced',
					cpu  => [],
				}
			},
			{
				'worker-cpu-set' => {
					mode => 'exclusive',
					cpu  => [],
				}
			},
		],
	};

	push( @{ $threading->{'cpu-affinity'}[0]{'management-cpu-set'}{cpu} }, @non_workers );
	push( @{ $threading->{'cpu-affinity'}[1]{'receive-cpu-set'}{cpu} },    @non_workers );
	push( @{ $threading->{'cpu-affinity'}[2]{'worker-cpu-set'}{cpu} },     @workers );

	return $threading;
} ## end sub threading

sub sensor_name {
	my $self     = $_[0];
	my $instance = $_[1];

	if ( !$self->{opts}{sno} ) {
		if (   defined($instance)
			&& defined( $self->{config}{suricata}{instances} )
			&& defined( $self->{config}{suricata}{instances}{$instance} )
			&& defined( $self->{config}{suricata}{instances}{$instance}{'sensor-name'} ) )
		{
			if (   ref( $self->{config}{suricata}{instances}{$instance}{'sensor-name'} ) ne ''
				&& ref( $self->{config}{suricata}{instances}{$instance}{'sensor-name'} ) ne 'SCALAR' )
			{
				die(      '.suricata.instance.'
						. $instance
						. 'sensor-name is a ref type of '
						. ref( $self->{config}{suricata}{instances}{$instance}{'sensor-name'} )
						. ' and not ""/SCALAR' );
			}
			return $self->{config}{suricata}{instances}{$instance}{'sensor-name'};
		} elsif ( defined( $self->{config}{suricata}{config}{'sensor-name'} ) ) {
			if (   ref( $self->{config}{suricata}{config}{'sensor-name'} ) ne ''
				&& ref( $self->{config}{suricata}{config}{'sensor-name'} ) ne 'SCALAR' )
			{
				die(      '.suricata.sensor-name is of ref type '
						. ref( $self->{config}{suricata}{config}{'sensor-name'} )
						. ' and not ""/SCALAR' );
			}
			return $self->{config}{suricata}{config}{'sensor-name'};
		} ## end elsif ( defined( $self->{config}{suricata}{config...}))
	} else {
		$self->{opts}{sna} = 1;
	}

	my $auto_sensor_name = $self->{config}{suricata}{auto_sensor_name}{enable};
	if ( defined( $self->{opts}{sna} ) ) {
		$auto_sensor_name = $self->{opts}{sna};
	}

	if ( !$auto_sensor_name ) {
		my $status
			= 'Either --sno(or --sno --sna 0) specified or .suricata.auto_sensor_name=0 and nothing is specified by ';
		if ( defined($instance) ) {
			$status = $status . '.suricata.instances.' . $instance . '.sensor-name or ';
		}
		$status = $status . '.suricata.config.sensor-name';
		die($status);
	}

	my $sensor_name = hostname;

	my $snl = $self->{config}{suricata}{auto_sensor_name}{full};
	if ( defined( $self->{opts}{snl} ) ) {
		$snl = $self->{opts}{snl};
	}
	if ( !$snl ) {
		$sensor_name =~ s/\..*$//;
	}

	if ( defined($instance) ) {
		$sensor_name = $sensor_name . '-' . $instance;
	} else {
		$sensor_name = $sensor_name . '-pie';
	}

	return $sensor_name;
} ## end sub sensor_name

1;
