From 43b338ad1db46eb9be81eefbd0022de91f403a18 Mon Sep 17 00:00:00 2001 From: sfarouq-ext <116093375+sfarouq-ext@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:42:19 +0200 Subject: [PATCH] enh(stormshield): new mode vpn-tunnels and list-vpn-tunnels) (#5554) Co-authored-by: garnier-quentin --- .../stormshield/api/mode/listvpntunnels.pm | 111 +++++++ .../stormshield/api/mode/vpntunnels.pm | 273 ++++++++++++++++++ src/network/stormshield/api/plugin.pm | 18 +- tests/network/stormshield/api/Mockoon.json | 169 +++++++++++ .../stormshield/api/list_vpn_tunnels.robot | 40 +++ .../network/stormshield/api/vpn-tunnels.robot | 41 +++ tests/resources/spellcheck/stopwords.txt | 1 + 7 files changed, 645 insertions(+), 8 deletions(-) create mode 100644 src/network/stormshield/api/mode/listvpntunnels.pm create mode 100644 src/network/stormshield/api/mode/vpntunnels.pm create mode 100644 tests/network/stormshield/api/Mockoon.json create mode 100644 tests/network/stormshield/api/list_vpn_tunnels.robot create mode 100644 tests/network/stormshield/api/vpn-tunnels.robot diff --git a/src/network/stormshield/api/mode/listvpntunnels.pm b/src/network/stormshield/api/mode/listvpntunnels.pm new file mode 100644 index 000000000..721d9ea50 --- /dev/null +++ b/src/network/stormshield/api/mode/listvpntunnels.pm @@ -0,0 +1,111 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package network::stormshield::api::mode::listvpntunnels; + +use base qw(centreon::plugins::mode); + +use strict; +use warnings; + +my @labels = ( + 'ikeStatus', + 'name' +); + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options); + bless $self, $class; + + $options{options}->add_options(arguments => {}); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::init(%options); +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->request(command => 'monitor getikesa'); + + my $results = {}; + foreach my $entry (@{$result->{Result}}) { + $results->{ $entry->{id} } = { + name => $entry->{rulename}, + ikeStatus => $entry->{state} + }; + } + + return $results; +} + +sub run { + my ($self, %options) = @_; + + my $results = $self->manage_selection(custom => $options{custom}); + foreach my $instance (sort keys %$results) { + $self->{output}->output_add(long_msg => + join('', map("[$_: " . $results->{$instance}->{$_} . ']', @labels)) + ); + } + + $self->{output}->output_add( + severity => 'OK', + short_msg => 'List VPN tunnels:' + ); + $self->{output}->display(nolabel => 1, force_ignore_perfdata => 1, force_long_output => 1); + $self->{output}->exit(); +} + +sub disco_format { + my ($self, %options) = @_; + + $self->{output}->add_disco_format(elements => [@labels]); +} + +sub disco_show { + my ($self, %options) = @_; + + my $results = $self->manage_selection(custom => $options{custom}); + foreach (sort keys %$results) { + $self->{output}->add_disco_entry( + %{$results->{$_}} + ); + } +} + +1; + +__END__ + +=head1 MODE + +List VPN tunnels. + +=over 8 + +=back + +=cut \ No newline at end of file diff --git a/src/network/stormshield/api/mode/vpntunnels.pm b/src/network/stormshield/api/mode/vpntunnels.pm new file mode 100644 index 000000000..5920df353 --- /dev/null +++ b/src/network/stormshield/api/mode/vpntunnels.pm @@ -0,0 +1,273 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package network::stormshield::api::mode::vpntunnels; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; +use Digest::MD5 qw(md5_hex); +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +sub custom_status_output { + my ($self, %options) = @_; + + return 'ike status: ' . $self->{result_values}->{ikeStatus}; +} + +sub custom_packets_output { + my ($self, %options) = @_; + + return sprintf( + 'packets %s: %s', + $self->{result_values}->{label}, + $self->{result_values}->{value_absolute} + ); +} + +sub custom_traffic_output { + my ($self, %options) = @_; + + my ($traffic_value, $traffic_unit) = $self->{perfdata}->change_bytes(value => $self->{result_values}->{value_per_second}, network => 1); + return sprintf( + 'traffic %s: %s/s', + $self->{result_values}->{label}, + $traffic_value . $traffic_unit + ); +} + +sub custom_metric_calc { + my ($self, %options) = @_; + + my ($checked, $total) = (0, 0); + foreach (keys %{$options{new_datas}}) { + if (/$self->{instance}_$options{extra_options}->{instance_ref}_(.+)/) { + $checked |= 1; + my $new = $options{new_datas}->{$_}; + next if (!defined($options{old_datas}->{$_})); + my $old = $options{old_datas}->{$_}; + + $checked |= 2; + my $diff = $new - $old; + if ($diff < 0) { + $total += $new; + } else { + $total += $diff; + } + } + } + + if ($checked == 0) { + $self->{error_msg} = 'skipped (no value)'; + return -10; + } + if ($checked == 1) { + $self->{error_msg} = 'buffer creation'; + return -1; + } + + $self->{result_values}->{name} = $options{new_datas}->{$self->{instance} . '_name'}; + $self->{result_values}->{value_per_second} = $total / $options{delta_time}; + $self->{result_values}->{value_absolute} = $total; + $self->{result_values}->{label} = $options{extra_options}->{label_ref}; + return 0; +} + +sub prefix_tunnel_output { + my ($self, %options) = @_; + + return "VPN tunnel '" . $options{instance_value}->{name} . "' "; +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { name => 'global', type => 0 }, + { name => 'tunnels', type => 1, cb_prefix_output => 'prefix_tunnel_output', message_multiple => 'All VPN tunnels are ok', skipped_code => { -10 => 1 } } + ]; + + $self->{maps_counters}->{global} = [ + { label => 'tunnels-total', nlabel => 'vpn.tunnels.total.count', set => { + key_values => [ { name => 'total' } ], + output_template => 'Total VPN tunnels: %s', + perfdatas => [ + { template => '%s', min => 0 } + ] + } + } + ]; + + $self->{maps_counters}->{tunnels} = [ + { + label => 'status', + type => 2, + warning_default => '%{ikeStatus} =~ /connecting/', + set => { + key_values => [ { name => 'ikeStatus' }, { name => 'name' } ], + closure_custom_output => $self->can('custom_status_output'), + closure_custom_perfdata => sub { return 0; }, + closure_custom_threshold_check => \&catalog_status_threshold_ng + } + }, + { label => 'tunnel-traffic-in', nlabel => 'vpn.tunnel.traffic.in.bitspersecond', set => { + key_values => [], + manual_keys => 1, + closure_custom_calc => $self->can('custom_metric_calc'), + closure_custom_calc_extra_options => { instance_ref => 'traffic_in', label_ref => 'in' }, + closure_custom_output => $self->can('custom_traffic_output'), + threshold_use => 'value_per_second', + perfdatas => [ + { template => '%s', value => 'value_per_second', min => 0, unit => 'b/s', label_extra_instance => 1 } + ] + } + }, + { label => 'tunnel-traffic-out', nlabel => 'vpn.tunnel.traffic.out.bitspersecond', set => { + key_values => [], + manual_keys => 1, + closure_custom_calc => $self->can('custom_metric_calc'), + closure_custom_calc_extra_options => { instance_ref => 'traffic_out', label_ref => 'traffic_out' }, + closure_custom_output => $self->can('custom_traffic_output'), + threshold_use => 'value_per_second', + perfdatas => [ + { template => '%s', value => 'value_per_second', min => 0, unit => 'b/s', label_extra_instance => 1 } + ] + } + }, + { label => 'tunnel-packets-in', nlabel => 'vpn.tunnel.packets.in.count', set => { + key_values => [], + manual_keys => 1, + closure_custom_calc => $self->can('custom_metric_calc'), + closure_custom_calc_extra_options => { instance_ref => 'packets_in', label_ref => 'in' }, + closure_custom_output => $self->can('custom_packets_output'), + threshold_use => 'value_absolute', + perfdatas => [ + { template => '%s', value => 'value_absolute', min => 0, label_extra_instance => 1 } + ] + } + }, + { label => 'tunnel-packets-out', nlabel => 'vpn.tunnel.packets.out.count', set => { + key_values => [], + manual_keys => 1, + closure_custom_calc => $self->can('custom_metric_calc'), + closure_custom_calc_extra_options => { instance_ref => 'packets_out', label_ref => 'out' }, + closure_custom_output => $self->can('custom_packets_output'), + threshold_use => 'value_absolute', + perfdatas => [ + { template => '%s', value => 'value_absolute', min => 0, label_extra_instance => 1 } + ] + } + } + ]; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, statefile => 1, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options(arguments => { + 'filter-name:s' => { name => 'filter_name' } + }); + + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->request(command => 'monitor getikesa'); + + foreach my $entry (@{$result->{Result}}) { + next if (defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '' && + $entry->{rulename} !~ /$self->{option_results}->{filter_name}/); + + $self->{tunnels}->{ $entry->{rulename} } = { + name => $entry->{rulename}, + ikeStatus => $entry->{state} + }; + } + + $result = $options{custom}->request(command => 'monitor getsa'); + + foreach my $entry (@{$result->{Result}}) { + next if (!defined($self->{tunnels}->{ $entry->{ikerulename} })); + + my $instance = $entry->{rulename}; + + $self->{tunnels}->{ $entry->{ikerulename} }->{'traffic_in_' . $instance} = $entry->{bytesin} * 8; + $self->{tunnels}->{ $entry->{ikerulename} }->{'traffic_out_' . $instance} = $entry->{bytesout} * 8; + $self->{tunnels}->{ $entry->{ikerulename} }->{'packets_in_' . $instance} = $entry->{packetsin}; + $self->{tunnels}->{ $entry->{ikerulename} }->{'packets_out_' . $instance} = $entry->{packetsout}; + } + + $self->{cache_name} = 'stormshield_' . $options{custom}->get_connection_info() . '_' . $self->{mode} . '_' . + md5_hex( + (defined($self->{option_results}->{filter_counters}) ? $self->{option_results}->{filter_counters} : '') . '_' . + (defined($self->{option_results}->{filter_name}) ? $self->{option_results}->{filter_name} : '') + ); + + $self->{global} = { total => scalar(keys %{$self->{tunnels}}) }; +} + +1; + +__END__ + +=head1 MODE + +Check VPN tunnels. + +=over 8 + +=item B<--filter-name> + +Filter name (can be a regexp). + +=item B<--filter-counters> + +Only display some counters (regexp can be used). +Example: --filter-counters='tunnels-total' + +=item B<--unknown-status> + +Define the conditions to match for the status to be UNKNOWN. +You can use the following variables: %{ikeStatus}, %{name} + +=item B<--warning-status> + +Define the conditions to match for the status to be WARNING (default: '%{ikeStatus} =~ /connecting/'. +You can use the following variables: %{ikeStatus}, %{name} + +=item B<--critical-status> + +Define the conditions to match for the status to be CRITICAL. +You can use the following variables: %{ikeStatus}, %{name} + +=item B<--warning-*> B<--critical-*> + +Thresholds. +Can be: 'tunnels-total', 'tunnel-traffic-in', 'tunnel-traffic-out', +'tunnel-packets-in', 'tunnel-packets-out'. + +=back + +=cut diff --git a/src/network/stormshield/api/plugin.pm b/src/network/stormshield/api/plugin.pm index 1ac9313ab..c075ac3d2 100644 --- a/src/network/stormshield/api/plugin.pm +++ b/src/network/stormshield/api/plugin.pm @@ -30,14 +30,16 @@ sub new { bless $self, $class; $self->{modes} = { - 'cpu' => 'network::stormshield::api::mode::cpu', - 'ha' => 'network::stormshield::api::mode::ha', - 'hardware' => 'network::stormshield::api::mode::hardware', - 'health' => 'network::stormshield::api::mode::health', - 'interfaces' => 'network::stormshield::api::mode::interfaces', - 'list-interfaces' => 'network::stormshield::api::mode::listinterfaces', - 'memory' => 'network::stormshield::api::mode::memory', - 'uptime' => 'network::stormshield::api::mode::uptime' + 'cpu' => 'network::stormshield::api::mode::cpu', + 'ha' => 'network::stormshield::api::mode::ha', + 'hardware' => 'network::stormshield::api::mode::hardware', + 'health' => 'network::stormshield::api::mode::health', + 'interfaces' => 'network::stormshield::api::mode::interfaces', + 'list-interfaces' => 'network::stormshield::api::mode::listinterfaces', + 'list-vpn-tunnels' => 'network::stormshield::api::mode::listvpntunnels', + 'memory' => 'network::stormshield::api::mode::memory', + 'uptime' => 'network::stormshield::api::mode::uptime', + 'vpn-tunnels' => 'network::stormshield::api::mode::vpntunnels' }; $self->{custom_modes}->{api} = 'network::stormshield::api::custom::api'; diff --git a/tests/network/stormshield/api/Mockoon.json b/tests/network/stormshield/api/Mockoon.json new file mode 100644 index 000000000..56f9c8fb0 --- /dev/null +++ b/tests/network/stormshield/api/Mockoon.json @@ -0,0 +1,169 @@ +{ + "uuid": "c0a1a49e-58d9-49e8-bd8e-9eca9b7fe197", + "lastMigration": 32, + "name": "New environment", + "endpointPrefix": "", + "latency": 0, + "port": 3000, + "hostname": "", + "rootChildren": [ + { + "type": "route", + "uuid": "8cd81000-5c93-4799-97a3-e61978797b22" + }, + { + "type": "route", + "uuid": "cfd8d8b8-2473-405c-a6b8-4e15dbd89b3e" + }, + { + "type": "route", + "uuid": "b6f05284-391c-4a75-9525-03162ebb5856" + } + ], + "folders": [], + "routes": [ + { + "uuid": "8cd81000-5c93-4799-97a3-e61978797b22", + "type": "http", + "documentation": "", + "method": "post", + "endpoint": "auth/admin.html", + "responses": [ + { + "uuid": "02882e1f-30f7-4edc-aa92-88b12897fa92", + "body": "", + "latency": 0, + "statusCode": 200, + "label": "", + "headers": [ + { + "key": "Set-Cookie", + "value": "NETASQ_sslclient=abcdefg123; Path=/api/; Secure; HttpOnly; SameSite=Strict" + } + ], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [ + { + "target": "body", + "modifier": "", + "value": "app=sslclient&pswd=azerty123&uid=qwerty123", + "invert": false, + "operator": "equals" + } + ], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": true, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null + }, + { + "uuid": "cfd8d8b8-2473-405c-a6b8-4e15dbd89b3e", + "type": "http", + "documentation": "", + "method": "post", + "endpoint": "api/auth/login", + "responses": [ + { + "uuid": "c7548a37-47c4-4df0-9ecc-9ba3613eb05d", + "body": "\r\nAZEQERSFSRSTSTSTSTSTSTTadminSFAZERTY15HH18SF25754A7514modify,mon_write,base,contentfilter,log,filter,vpn,log_read,pki,object,user,admin,network,route,maintenance,asq,pvm,vpn_read,filter_read,report,report_read,globalobject,globalfilter,guest_admin,privacy,privacy_read,tpm,consolebase,contentfilter,log,filter,vpn,log_read,pki,object,user,admin,network,route,maintenance,asq,pvm,vpn_read,filter_read,report,report_read,globalobject,globalfilter,guest_admin,privacy,privacy_read,tpm,consoledirect127.0.0.1vlan600
", + "latency": 0, + "statusCode": 200, + "label": "", + "headers": [ + { + "key": "Accept", + "value": "*/*" + }, + { + "key": "Content-Type", + "value": "application/x-www-form-urlencoded" + }, + { + "key": "Set-Cookie", + "value": "azerty123" + }, + { + "key": "X-Content-Type-Options", + "value": "nosniff" + }, + { + "key": "X-Frame-Options", + "value": "deny" + }, + { + "key": "Content-Security-Policy", + "value": "frame-ancestors" + } + ], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": true, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null + }, + { + "uuid": "b6f05284-391c-4a75-9525-03162ebb5856", + "type": "http", + "documentation": "", + "method": "get", + "endpoint": "api/command", + "responses": [ + { + "uuid": "4cc9b593-ea33-4c15-bb5e-c388c17885e8", + "body": "\r\n
", + "latency": 0, + "statusCode": 200, + "label": "", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": true, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null + } + ], + "proxyMode": false, + "proxyHost": "", + "proxyRemovePrefix": false, + "tlsOptions": { + "enabled": false, + "type": "CERT", + "pfxPath": "", + "certPath": "", + "keyPath": "", + "caPath": "", + "passphrase": "" + }, + "cors": true, + "headers": [], + "proxyReqHeaders": [], + "proxyResHeaders": [], + "data": [], + "callbacks": [] +} \ No newline at end of file diff --git a/tests/network/stormshield/api/list_vpn_tunnels.robot b/tests/network/stormshield/api/list_vpn_tunnels.robot new file mode 100644 index 000000000..59c856e55 --- /dev/null +++ b/tests/network/stormshield/api/list_vpn_tunnels.robot @@ -0,0 +1,40 @@ +*** Settings *** + +Resource ${CURDIR}${/}..${/}..${/}..${/}resources/import.resource + +Suite Setup Start Mockoon ${MOCKOON_JSON} +Suite Teardown Stop Mockoon +Test Timeout 120s + + +*** Variables *** +${MOCKOON_JSON} ${CURDIR}${/}Mockoon.json + +${cmd} ${CENTREON_PLUGINS} +... --plugin=network::stormshield::api::plugin +... --mode=list-vpn-tunnels +... --custommode=api +... --hostname=${HOSTNAME} +... --api-username=username +... --api-password=password +... --proto=http +... --port=${APIPORT} +... --timeout=5 + + +*** Test Cases *** +list-vpn-tunnels ${tc} + [Tags] network api + ${output} Run ${CMD} ${extraoptions} | wc -l + + ${output} Strip String ${output} + Should Be Equal As Strings + ... ${output} + ... ${expected_result} + ... Wrong output result for command:\n${CMD} ${extraoptions}\n\nObtained:\n${output}\n\nExpected:\n${expected_result}\n + ... values=False + ... collapse_spaces=True + + + Examples: tc extraoptions expected_result -- + ... 1 ${EMPTY} 24 diff --git a/tests/network/stormshield/api/vpn-tunnels.robot b/tests/network/stormshield/api/vpn-tunnels.robot new file mode 100644 index 000000000..c00df3405 --- /dev/null +++ b/tests/network/stormshield/api/vpn-tunnels.robot @@ -0,0 +1,41 @@ +*** Settings *** + +Resource ${CURDIR}${/}..${/}..${/}..${/}resources/import.resource + +Suite Setup Start Mockoon ${MOCKOON_JSON} +Suite Teardown Stop Mockoon +Test Timeout 120s + + +*** Variables *** +${MOCKOON_JSON} ${CURDIR}${/}Mockoon.json + +${cmd} ${CENTREON_PLUGINS} +... --plugin=network::stormshield::api::plugin +... --custommode=api +... --hostname=${HOSTNAME} +... --api-username=username +... --api-password=password +... --proto=http +... --port=${APIPORT} +... --timeout=5 + + +*** Test Cases *** +vpn-tunnels ${tc} + [Tags] network api + ${command} Catenate + ... ${cmd} + ... --mode=vpn-tunnels + ... ${extraoptions} + + Ctn Run Command And Check Result As Strings ${command} ${expected_result} + + Examples: tc extraoptions expected_result -- + ... 1 ${EMPTY} OK: Total VPN tunnels: 22 - All VPN tunnels are ok | 'vpn.tunnels.total.count'=22;;;0; + ... 2 --filter-name='7ddb9fbb1faab23401c19beb20e31b62' OK: Total VPN tunnels: 1 - VPN tunnel '7ddb9fbb1faab23401c19beb20e31b62' ike status: installed | 'vpn.tunnels.total.count'=1;;;0; + ... 3 --filter-counters='tunnels-total' OK: Total VPN tunnels: 22 | 'vpn.tunnels.total.count'=22;;;0; + ... 4 --unknown-status='\\\%{ikeStatus} =~ /installed/' --filter-name='3061933d03c01595f6a426cfb50c5e09' UNKNOWN: VPN tunnel '3061933d03c01595f6a426cfb50c5e09' ike status: installed | 'vpn.tunnels.total.count'=1;;;0; + ... 5 --warning-status='\\\%{ikeStatus} =~ /installed/' --filter-name='2975a3940ace7eb1a13a006a51c66991' WARNING: VPN tunnel '2975a3940ace7eb1a13a006a51c66991' ike status: installed | 'vpn.tunnels.total.count'=1;;;0; + ... 6 --critical-status='\\\%{ikeStatus} =~ /installed/' --filter-name='4793e3b444d2342a46df35dd0338f2cc' CRITICAL: VPN tunnel '4793e3b444d2342a46df35dd0338f2cc' ike status: installed | 'vpn.tunnels.total.count'=1;;;0; + ... 7 --warning-tunnels-total=20 --critical-tunnels-total=25 WARNING: Total VPN tunnels: 22 | 'vpn.tunnels.total.count'=22;0:20;0:25;0; diff --git a/tests/resources/spellcheck/stopwords.txt b/tests/resources/spellcheck/stopwords.txt index 03f6f9549..ccdbcb553 100644 --- a/tests/resources/spellcheck/stopwords.txt +++ b/tests/resources/spellcheck/stopwords.txt @@ -247,6 +247,7 @@ SSH standAlone statefile --statefile-concat-cwd +Stormshield SureBackup systemd SysVol