File Coverage

blib/lib/Ixchel/Actions/sagan_merged.pm
Criterion Covered Total %
statement 38 119 31.9
branch 0 22 0.0
condition n/a
subroutine 13 17 76.4
pod 2 4 50.0
total 53 162 32.7


line stmt bran cond sub pod time code
1             package Ixchel::Actions::sagan_merged;
2              
3 1     1   132472 use 5.006;
  1         4  
4 1     1   5 use strict;
  1         2  
  1         36  
5 1     1   7 use warnings;
  1         2  
  1         71  
6 1     1   648 use File::Slurp;
  1         50271  
  1         95  
7 1     1   554 use YAML::XS qw(Dump Load);
  1         4131  
  1         76  
8 1     1   589 use Ixchel::functions::file_get;
  1         4  
  1         80  
9 1     1   550 use utf8;
  1         344  
  1         14  
10 1     1   52 use File::Temp qw/ tempfile tempdir /;
  1         3  
  1         71  
11 1     1   6 use File::Spec;
  1         18  
  1         29  
12 1     1   681 use YAML::yq::Helper;
  1         18952  
  1         50  
13 1     1   585 use Hash::Merge;
  1         8956  
  1         61  
14 1     1   7 use File::Copy;
  1         2  
  1         68  
15 1     1   6 use base 'Ixchel::Actions::base';
  1         3  
  1         643  
16              
17             =head1 NAME
18              
19             Ixchel::Actions::sagan_merged - Generated a merged base/include for Sagan.
20              
21             =head1 VERSION
22              
23             Version 0.2.0
24              
25             =cut
26              
27             our $VERSION = '0.2.0';
28              
29             =head1 CLI SYNOPSIS
30              
31             ixchel -a sagan_merged [B<--np>] [B<-w>] [B<-i> ]
32              
33             =head1 CODE SYNOPSIS
34              
35             use Data::Dumper;
36              
37             my $results=$ixchel->action(action=>'sagan_base', opts=>{np=>1, w=>1, });
38              
39             print Dumper($results);
40              
41             =head1 DESCRIPTION
42              
43             .sagan.base_config is used as the URL for the config to use and needs to be something
44             understood by L. By default
45             https://raw.githubusercontent.com/quadrantsec/sagan/main/etc/sagan.yaml is used.
46              
47             The following arrays are blanked in the file.
48              
49             .rules-files
50             .processors
51             .outputs
52              
53             These are removed as they are array based, making it very awkward to deal with with
54             having them previously defined.
55              
56             A include is then generated using .sagan.config. If .sagan.multi_instance is set to 1,
57             then .sagan.instances.$instance is merged on top of it using L
58             with RIGHT_PRECEDENT as below with arrays being replaced. This is then generated and
59             merged into the base file file using yq.
60              
61             .include is set to .sagan.config_base.'/sagan-rules.yaml' in the case of single
62             instance setups if .sagan.multi_instance is set to 1 then
63             .sagan.config_base."/sagan-$instance-rules.yaml"
64              
65             =head1 FLAGS
66              
67             =head2 -w
68              
69             Write out the configs.
70              
71             =head2 -i instance
72              
73             A instance to operate on.
74              
75             =head1 RESULT HASH REF
76              
77             .errors :: A array of errors encountered.
78             .status_text :: A string description of what was done and teh results.
79             .ok :: Set to zero if any of the above errored.
80              
81             =cut
82              
83       0 0   sub new_extra { }
84              
85             sub action_extra {
86 0     0 0   my $self = $_[0];
87              
88 0           my $config_base = $self->{config}{sagan}{config_base};
89              
90 0           my $have_config = 0;
91 0           my $tmpdir = tempdir( CLEANUP => 1 );
92 0           my $tmp_base = $tmpdir . '/base.yaml';
93 0           eval {
94 0           my $fetched_raw_yaml;
95             my $parsed_yaml;
96 0           $fetched_raw_yaml = file_get( url => $self->{config}{sagan}{base_config} );
97 0 0         if ( !defined($fetched_raw_yaml) ) {
98 0           die('file_get returned undef');
99             }
100 0           utf8::encode($fetched_raw_yaml);
101 0           $parsed_yaml = Load($fetched_raw_yaml);
102 0 0         if ( !defined($parsed_yaml) ) {
103 0           die('Attempting to parse the returned data as YAML failed');
104             }
105              
106 0           write_file( $tmp_base, $fetched_raw_yaml );
107              
108 0           my $yq = YAML::yq::Helper->new( file => $tmp_base );
109 0           $yq->delete( var => '.rules-files' );
110 0           $yq->delete( var => '.include' );
111 0           $yq->clear_array( var => '.outputs' );
112 0           $yq->clear_array( var => '.processors' );
113              
114 0           $have_config = 1;
115             };
116 0 0         if ($@) {
117             $self->status_add(
118 0           status => 'Fetching and proccessing ' . $self->{config}{sagan}{base_config} . ' failed... ' . $@,
119             error => 1,
120             );
121 0           return undef;
122             }
123              
124 0 0         if ($have_config) {
125 0 0         if ( $self->{config}{sagan}{multi_instance} ) {
126 0           my @instances;
127              
128 0 0         if ( defined( $self->{opts}{i} ) ) {
129 0           @instances = ( $self->{opts}{i} );
130             } else {
131 0           @instances = keys( %{ $self->{config}{sagan}{instances} } );
  0            
132             }
133 0           foreach my $instance (@instances) {
134             # clean it up so there is less likely of a chance of some one deciding to do that by hand and borking the file
135             my $include_path = File::Spec->canonpath(
136 0           $self->{config}{sagan}{config_base} . '/sagan-' . $instance . '-rules.yaml' );
137              
138 0           my $instance_base = $tmpdir . '/base-' . $instance . '.yaml';
139              
140 0           my $config_file = $self->{config}{sagan}{config_base} . '/sagan-' . $instance . '.yaml';
141              
142 0           my $to_be_merged = $tmpdir . '/to_merge.yaml';
143              
144 0           eval {
145 0           copy( $tmp_base, $config_file );
146              
147 0           my $config = $self->{config}{sagan}{instances}{$instance};
148 0           my $base_config = $self->{config}{sagan}{config};
149 0           my $merger = Hash::Merge->new('RIGHT_PRECEDENT');
150 0           my %tmp_config = %{$config};
  0            
151 0           my %tmp_base_config = %{$base_config};
  0            
152 0           my $merged = $merger->merge( \%tmp_base_config, \%tmp_config );
153 0           $merged->{include} = $include_path;
154              
155 0           my $filled_in = '%YAML 1.1' . "\n" . Dump($merged);
156 0           write_file( $to_be_merged, $filled_in );
157              
158 0           my $yq = YAML::yq::Helper->new( file => $instance_base );
159 0           $yq->merge_yaml( yaml => $to_be_merged );
160              
161 0           $filled_in = read_file($instance_base);
162              
163 0 0         if ( $self->{opts}{w} ) {
164 0           write_file( $config_file, $filled_in );
165             }
166              
167 0           $self->status_add( status => '-----[ Instance '
168             . $instance
169             . ' ]-------------------------------------' . "\n"
170             . $filled_in
171             . "\n" );
172             };
173 0 0         if ($@) {
174 0           $self->status_add(
175             status => '-----[ Error: Instance '
176             . $instance
177             . " ]-------------------------------------\nCreating merged base/include failed... "
178             . $@,
179             error => 1,
180             );
181             }
182             } ## end foreach my $instance (@instances)
183             } else {
184             # clean it up so there is less likely of a chance of some one deciding to do that by hand and borking the file
185 0           my $include_path = File::Spec->canonpath( $self->{config}{sagan}{config_base} . '/sagan-rules.yaml' );
186              
187 0           my $config_file = $self->{config}{sagan}{config_base} . '/sagan.yaml';
188              
189 0           my $to_merge = $self->{config}{sagan}{config};
190 0           $to_merge->{include} = $include_path;
191 0           my $to_include = '%YAML 1.1' . "\n" . Dump($to_merge);
192              
193 0           my $to_be_merged = $tmpdir . '/to_merge.yaml';
194 0           write_file( $to_be_merged, $to_include );
195              
196 0           my $yq = YAML::yq::Helper->new( file => $tmp_base );
197 0           $yq->merge_yaml( yaml => $to_be_merged );
198              
199 0           my $raw_yaml;
200 0           eval {
201 0           $raw_yaml = read_file($tmp_base);
202              
203 0           $self->status_add( status => "Base... \n" . $raw_yaml );
204              
205 0 0         if ( $self->{opts}{w} ) {
206 0           write_file( $config_file, $raw_yaml );
207             }
208             };
209 0 0         if ($@) {
210 0           $self->status_add(
211             status => 'Died... ' . $@,
212             error => 1,
213             );
214              
215             }
216             } ## end else [ if ( $self->{config}{sagan}{multi_instance...})]
217             } ## end if ($have_config)
218              
219 0           eval { unlink($tmp_base); };
  0            
220 0 0         if ($@) {
221 0           $self->status_add(
222             error => 1,
223             status => 'Unlinking "' . $tmp_base . '" failed ... ' . $@
224             );
225             }
226              
227 0           return undef;
228             } ## end sub action_extra
229              
230             sub short {
231 0     0 1   return 'Generated a merged base/include for Sagan.';
232             }
233              
234             sub opts_data {
235 0     0 1   return 'i=s
236             w
237             ';
238             }
239              
240             1;