From 651095a4eaf03d311d62a1f318b1414ef5f9451f Mon Sep 17 00:00:00 2001 From: omercier <32134301+omercier@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:23:12 +0100 Subject: [PATCH] enh(passwordmgr-centreonvault): adapt vault module for plugins to the new specs (#5302) Refs: CTOR-1151 --- .../packaging/centreon-plugin.yaml.template | 2 + .../src/centreon/script/centreonvault.pm | 6 +- .../plugins/passwordmgr/centreonvault.pm | 462 +++++++++++++++--- .../passwordmgr/centreonvault.mockoon.json | 239 +++++++++ tests/centreon/plugins/passwordmgr/env | 1 + .../passwordmgr/vault_config_encrypted.json | 10 + .../passwordmgr/vault_config_incomplete.json | 10 + .../passwordmgr/vault_config_plain.json | 11 + .../os/linux/snmp/memory-centreonvault.robot | 37 ++ tests/resources/spellcheck/stopwords.txt | 4 + 10 files changed, 712 insertions(+), 70 deletions(-) create mode 100644 tests/centreon/plugins/passwordmgr/centreonvault.mockoon.json create mode 100644 tests/centreon/plugins/passwordmgr/env create mode 100644 tests/centreon/plugins/passwordmgr/vault_config_encrypted.json create mode 100644 tests/centreon/plugins/passwordmgr/vault_config_incomplete.json create mode 100644 tests/centreon/plugins/passwordmgr/vault_config_plain.json create mode 100644 tests/os/linux/snmp/memory-centreonvault.robot diff --git a/.github/packaging/centreon-plugin.yaml.template b/.github/packaging/centreon-plugin.yaml.template index 9a159b0b54..a0cc5cfec4 100644 --- a/.github/packaging/centreon-plugin.yaml.template +++ b/.github/packaging/centreon-plugin.yaml.template @@ -30,6 +30,7 @@ provides: overrides: rpm: depends: [ + perl(Crypt::OpenSSL::AES), perl(Digest::MD5), perl(Pod::Find), perl-Net-Curl, @@ -62,6 +63,7 @@ overrides: [@RPM_PROVIDES@] deb: depends: [ + libcrypt-openssl-aes-perl, libpod-parser-perl, libnet-curl-perl, liburi-encode-perl, diff --git a/connectors/vmware/src/centreon/script/centreonvault.pm b/connectors/vmware/src/centreon/script/centreonvault.pm index 47164af79e..dc255cc1c2 100644 --- a/connectors/vmware/src/centreon/script/centreonvault.pm +++ b/connectors/vmware/src/centreon/script/centreonvault.pm @@ -335,7 +335,7 @@ Centreon Vault password manager =head1 SYNOPSIS -Allows to retrieve secrets (usually username and password) from a Hashicorp vault compatible api given a config file as constructor. +Allows to retrieve secrets (usually username and password) from a Hashicorp vault compatible API. use centreon::vmware::logger; use centreon::script::centreonvault; @@ -364,8 +364,8 @@ The expected file format for Centreon Vault is: { "name": "hashicorp_vault", - "url": "vault-server.mydomain.com", - "salt": "", + "url": "vault-server.my-domain.com", + "salt": "", "port": 443, "root_path": "vmware_daemon", "role_id": ")", diff --git a/src/centreon/plugins/passwordmgr/centreonvault.pm b/src/centreon/plugins/passwordmgr/centreonvault.pm index 555cc6cc66..c42a2a8032 100644 --- a/src/centreon/plugins/passwordmgr/centreonvault.pm +++ b/src/centreon/plugins/passwordmgr/centreonvault.pm @@ -25,6 +25,11 @@ use warnings; use Data::Dumper; use centreon::plugins::http; use JSON::XS; +use MIME::Base64; +use Crypt::OpenSSL::AES; +use centreon::plugins::statefile; + +my $VAULT_PATH_REGEX = qr/^secret::hashicorp_vault::([^:]+)::(.+)$/; sub new { my ($class, %options) = @_; @@ -32,21 +37,29 @@ sub new { bless $self, $class; if (!defined($options{output})) { - print "Class PasswordMgr: Need to specify 'output' argument.\n"; + print "Class PasswordMgr needs an 'output' argument that must be of type centreon::plugins::output.\n"; exit 3; } if (!defined($options{options})) { - $options{output}->add_option_msg(short_msg => "Class PasswordMgr: Need to specify 'options' argument."); + print "Class PasswordMgr needs an 'options' argument that must be of type centreon::plugins::options.\n"; $options{output}->option_exit(); } $options{options}->add_options(arguments => { - 'vault-config:s' => { name => 'vault_config', default => '/etc/centreon-engine/centreonvault.json'}, + 'vault-config:s' => { name => 'vault_config', default => '/etc/centreon-engine/centreonvault.json'}, + 'vault-cache:s' => { name => 'vault_cache', default => '/var/lib/centreon/centplugins/centreonvault_session'}, + 'vault-env-file:s' => { name => 'vault_env_file', default => '/usr/share/centreon/.env'}, }); + $options{options}->add_help(package => __PACKAGE__, sections => 'VAULT OPTIONS'); $self->{output} = $options{output}; - $self->{http} = centreon::plugins::http->new(%options, noptions => 1, default_backend => 'curl'); + + # to access the vault, http protocol management is needed + $self->{http} = centreon::plugins::http->new(%options, noptions => 1, default_backend => 'curl', insecure => 1); + + # to store the token and its expiration date, a statefile is needed + $self->{cache} = centreon::plugins::statefile->new(); return $self; } @@ -56,20 +69,21 @@ sub extract_map_options { $self->{map_option} = []; - # Parse all options to find '/\{.*\:\:secret\:\:(.*)\}/' dedicated patern in value and add entries in map_option + # Parse all options to find '/# .*\:\:secret\:\:(.*)/' pattern in the options values and add entries in map_option foreach my $option (keys %{$options{option_results}}) { if (defined($options{option_results}{$option})) { next if ($option eq 'map_option'); if (ref($options{option_results}{$option}) eq 'ARRAY') { foreach (@{$options{option_results}{$option}}) { - if ($_ =~ /\{.*\:\:secret\:\:(.*)\:\:(.*)\}/i) { - push (@{$self->{request_endpoint}}, "$1::/v1/".$2); + if ($_ =~ $VAULT_PATH_REGEX) { + push (@{$self->{request_endpoint}}, "/v1/$1::$2"); push (@{$self->{map_option}}, $option."=%".$_); } } } else { - if ($options{option_results}{$option} =~ /\{.*\:\:secret\:\:(.*)\:\:(.*)\}/i) { - push (@{$self->{request_endpoint}}, "$1::/v1/".$2); + + if (my ($path, $secret) = $options{option_results}{$option} =~ $VAULT_PATH_REGEX) { + push (@{$self->{request_endpoint}}, "/v1/" . $path . "::" . $secret); push (@{$self->{map_option}}, $option."=%".$options{option_results}{$option}); } } @@ -80,95 +94,280 @@ sub extract_map_options { sub vault_settings { my ($self, %options) = @_; - if (!defined($options{option_results}->{vault_config}) - || $options{option_results}->{vault_config} eq '') { - $self->{output}->add_option_msg(short_msg => "Please set --vault-config option"); + if (centreon::plugins::misc::is_empty($options{option_results}->{vault_config})) { + $self->{output}->add_option_msg(short_msg => "Please provide a Centreon Vault configuration file path with --vault-config option"); $self->{output}->option_exit(); } if (! -f $options{option_results}->{vault_config}) { - $self->{output}->add_option_msg(short_msg => "Cannot find file '$options{option_results}->{vault_config}'"); + $self->{output}->add_option_msg(short_msg => "File '$options{option_results}->{vault_config}' could not be found."); $self->{output}->option_exit(); } - + $self->{vault_cache} = $options{option_results}->{vault_cache}; + $self->{vault_env_file} = $options{option_results}->{vault_env_file}; + $self->{vault_config} = $options{option_results}->{vault_config}; + + my $file_content = do { local $/ = undef; if (!open my $fh, "<", $options{option_results}->{vault_config}) { - $self->{output}->add_option_msg(short_msg => "Could not open file $options{option_results}->{vault_config}: $!"); + $self->{output}->add_option_msg(short_msg => "Could not read file $options{option_results}->{vault_config}: $!"); $self->{output}->option_exit(); } <$fh>; }; - my $json; + # decode the JSON content of the file + my $json = centreon::plugins::misc::json_decode($file_content); + if (!defined($json)) { + $self->{output}->add_option_msg(short_msg => "Cannot decode JSON : $file_content\n"); + $self->{output}->option_exit(); + } + + # set the default values + $self->{vault}->{protocol} = 'https'; + $self->{vault}->{url} = '127.0.0.1'; + $self->{vault}->{port} = '8100'; + + # define the list of expected attributes in the JSON file + my @valid_json_options = ( + 'protocol', + 'url', + 'port', + 'root_path', + 'token', + 'secret_id', + 'role_id' + ); + + # set the object fields when the json fields are not empty + foreach my $valid_option (@valid_json_options) { + $self->{vault}->{$valid_option} = $json->{$valid_option} + if ( !centreon::plugins::misc::is_empty( $json->{ $valid_option } ) ); + } + + return 1; +} + +sub get_decryption_key { + my ($self, %options) = @_; + + # try getting APP_SECRET from the environment variables + if ( !centreon::plugins::misc::is_empty($ENV{'APP_SECRET'}) ) { + return $ENV{'APP_SECRET'}; + } + + # try getting APP_SECRET defined in the env file (default: /usr/share/centreon/.env) file + my $fh; + return undef if (!open $fh, "<", $self->{vault_env_file}); + for my $line (<$fh>) { + if ($line =~ /^APP_SECRET=(.*)$/) { + return $1; + } + } + + return undef; +} + +sub extract_and_decrypt { + my ($self, %options) = @_; + + my $input = decode_base64($options{data}); + my $key = $options{key}; + + # with AES-256, the IV length must 16 bytes + my $iv_length = 16; + # extract the IV, the hashed data, the encrypted data + my $iv = substr($input, 0, $iv_length); # initialization vector + my $hashed_data = substr($input, $iv_length, 64); # hmac of the original data, for integrity control + my $encrypted_data = substr($input, $iv_length + 64); # data to decrypt + + # Creating the AES decryption object + my $cipher; eval { - $json = JSON::XS->new->utf8->decode($file_content); + $cipher = Crypt::OpenSSL::AES->new( + $key, + { + 'cipher' => 'AES-256-CBC', + 'iv' => $iv + } + ); }; if ($@) { - $self->{output}->add_option_msg(short_msg => "Cannot decode json file"); + $self->{output}->add_option_msg(short_msg => "There was an error while creating the AES object: " . $@); $self->{output}->option_exit(); } - foreach my $vault_name (keys %$json) { - $self->{$vault_name}->{vault_protocol} = 'https'; - $self->{$vault_name}->{vault_address} = '127.0.0.1'; - $self->{$vault_name}->{vault_port} = '8100'; + # Decrypting the data + my $decrypted_data; + eval {$decrypted_data = $cipher->decrypt($encrypted_data);}; + if ($@) { + $self->{output}->add_option_msg(short_msg => "There was an error while decrypting an AES-encrypted data: " . $@); + $self->{output}->option_exit(); + } - $self->{$vault_name}->{vault_protocol} = $json->{$vault_name}->{'vault-protocol'} - if ($json->{$vault_name}->{'vault-protocol'} && $json->{$vault_name}->{'vault-protocol'} ne ''); - $self->{$vault_name}->{vault_address} = $json->{$vault_name}->{'vault-address'} - if ($json->{$vault_name}->{'vault-address'} && $json->{$vault_name}->{'vault-address'} ne ''); - $self->{$vault_name}->{vault_port} = $json->{$vault_name}->{'vault-port'} - if ($json->{$vault_name}->{'vault-port'} && $json->{$vault_name}->{'vault-port'} ne ''); - $self->{$vault_name}->{vault_token} = $json->{$vault_name}->{'vault-token'} - if ($json->{$vault_name}->{'vault-token'} && $json->{$vault_name}->{'vault-token'} ne ''); + return $decrypted_data; +} + +sub is_token_still_valid { + my ($self) = @_; + if ( + !defined($self->{auth}) + || centreon::plugins::misc::is_empty($self->{auth}->{token}) + || centreon::plugins::misc::is_empty($self->{auth}->{expiration_epoch}) + || $self->{auth}->{expiration_epoch} !~ /\d+/ + || $self->{auth}->{expiration_epoch} <= time() + ) { + $self->{output}->output_add(long_msg => "The token is missing or has expired or is invalid.", debug => 1); + return undef; } + $self->{output}->output_add(long_msg => "The cached token is still valid.", debug => 1); + # Possible enhancement: check the token validity by calling this endpoint: /v1/auth/token/lookup-self + # {"request_id":"XXXXX","lease_id":"","renewable":false,"lease_duration":0,"data":{"accessor":"XXXXXXX","creation_time":1732294406,"creation_ttl":2764800,"display_name":"approle","entity_id":"XXX","expire_time":"2024-12-24T16:53:26.932122122Z","explicit_max_ttl":0,"id":"hvs.secretToken","issue_time":"2024-11-22T16:53:26.932129132Z","meta":{"role_name":"myvault"},"num_uses":0,"orphan":true,"path":"auth/approle/login","policies":["default","myvault"],"renewable":true,"ttl":2764724,"type":"service"},"wrap_info":null,"warnings":null,"auth":null,"mount_type":"token"} + + return 1; } +sub check_authentication { + my ($self, %options) = @_; + + # prepare the cache (aka statefile) + $self->{cache}->check_options(option_results => $options{option_results}); + my ($dir, $file, $suffix) = $options{option_results}->{vault_cache} =~ /^(.*\/)([^\/]+)(_.*)?$/; + + # Try reading the cache file + if ($self->{cache}->read( + statefile => $file, + statefile_suffix => defined($suffix) ? $suffix : '', + statefile_dir => $dir, + statefile_format => 'json' + )) { + # if the cache file could be read, get the token and its expiration date + $self->{auth} = { + token => $self->{cache}->get(name => 'token'), + expiration_epoch => $self->{cache}->get(name => 'expiration_epoch') + }; + } + + # if it is not valid, authenticate to get a new token + if ( !$self->is_token_still_valid() ) { + return $self->authenticate(); + } + + return 1; +} + +sub authenticate { + my ($self) = @_; + + # initial value: assuming the role and secret id might not be encrypted + my $role_id = $self->{vault}->{role_id}; + my $secret_id = $self->{vault}->{secret_id}; + if (centreon::plugins::misc::is_empty($role_id) || centreon::plugins::misc::is_empty($secret_id)) { + $self->{output}->add_option_msg(short_msg => "Unable to authenticate to the vault: role_id or secret_id is empty."); + $self->{output}->option_exit(); + } + my $decryption_key = $self->get_decryption_key(); + + # Decrypt the role_id and the secret_id if we have a decryption key + if ( !centreon::plugins::misc::is_empty($decryption_key) ) { + $role_id = $self->extract_and_decrypt( + data => $role_id, + key => $decryption_key + ); + $secret_id = $self->extract_and_decrypt( + data => $secret_id, + key => $decryption_key + ); + } + + # Authenticate to get the token + my ($auth_result_json) = $self->{http}->request( + hostname => $self->{vault}->{url}, + port => $self->{vault}->{port}, + proto => $self->{vault}->{protocol}, + method => 'POST', + url_path => "/v1/auth/approle/login", + query_form_post => "role_id=$role_id&secret_id=$secret_id", + header => [ + 'Content-Type: application/x-www-form-urlencoded', + 'Accept: */*', + 'X-Vault-Request: true', + 'User-Agent: Centreon-Plugins' + ] + ); + + # Convert the response into a JSON object + my $auth_result_obj = centreon::plugins::misc::json_decode($auth_result_json); + if (!defined($auth_result_obj)) { + # exit with UNKNOWN status + $self->{output}->add_option_msg(short_msg => "Cannot decode JSON response from the vault server: $auth_result_json."); + $self->{output}->option_exit(); + } + # Authentication to the vault has passed + # store the token (.auth.client_token) and its expiration date (current date + .lease_duration) + my $expiration_epoch = -1; + my $lease_duration = $auth_result_obj->{auth}->{lease_duration}; + if ( defined($lease_duration) + && $lease_duration =~ /\d+/ + && $lease_duration > 0 ) { + $expiration_epoch = time() + $lease_duration; + } + $self->{auth} = { + 'token' => $auth_result_obj->{auth}->{client_token}, + 'expiration_epoch' => $expiration_epoch + }; + $self->{cache}->write(data => $self->{auth}, name => 'auth'); + + $self->{output}->output_add(long_msg => "Authenticating worked. Token valid until " + . localtime($self->{auth}->{expiration_epoch}), debug => 1); + + return 1; +} + + + sub request_api { my ($self, %options) = @_; $self->vault_settings(%options); + # check the authentication + if (!$self->check_authentication(%options)) { + $self->{output}->add_option_msg(short_msg => "Unable to authenticate to the vault."); + $self->{output}->option_exit(); + } + $self->{lookup_values} = {}; + foreach my $item (@{$self->{request_endpoint}}) { # Extract vault name configuration from endpoint # 'vault::/v1//monitoring/hosts/7ad55afc-fa9e-4851-85b7-e26f47e421d7' - my ($vault_name, $endpoint); - if ($item =~ /(.*)\:\:(.*)/i) { - $vault_name = $1; - $endpoint = $2; - } + my ($endpoint, $secret) = $item =~ /^(.*)\:\:(.*)$/; - if (!defined($self->{$vault_name})) { - $self->{output}->add_option_msg(short_msg => "Cannot get vault access for: $vault_name"); - $self->{output}->option_exit(); - } - - my $headers = ['Accept: application/json']; - if (defined($self->{$vault_name}->{vault_token})) { - push @$headers, 'X-Vault-Token: ' . $self->{$vault_name}->{vault_token}; - } my ($response) = $self->{http}->request( - hostname => $self->{$vault_name}->{vault_address}, - port => $self->{$vault_name}->{vault_port}, - proto => $self->{$vault_name}->{vault_protocol}, + hostname => $self->{vault}->{url}, + port => $self->{vault}->{port}, + proto => $self->{vault}->{protocol}, method => 'GET', url_path => $endpoint, - header => $headers + header => [ + 'Accept: application/json', + 'User-Agent: Centreon-Plugins', + 'X-Vault-Request: true', + 'X-Vault-Token: ' . $self->{auth}->{token} + ] ); - - my $json; - eval { - $json = JSON::XS->new->utf8->decode($response); - }; - if ($@) { - $self->{output}->add_option_msg(short_msg => "Cannot decode Vault JSON response: $@"); + + my $json = centreon::plugins::misc::json_decode($response); + if (!defined($json->{data})) { + $self->{output}->add_option_msg(short_msg => "Cannot decode Vault JSON response: $response"); $self->{output}->option_exit(); }; - foreach (keys %{$json->{data}}) { - $self->{lookup_values}->{'{' . $_ . '::secret::' . $vault_name . '::' . substr($endpoint, index($endpoint, '/', 1) + 1) . '}'} = $json->{data}->{$_}; + foreach my $secret_name (keys %{$json->{data}->{data}}) { + # e.g. secret::hashicorp_vault::myspace/data/snmp::PubCommunity + $self->{lookup_values}->{'secret::hashicorp_vault::' . substr($endpoint, index($endpoint, '/', 1) + 1) . '::' . $secret_name} = $json->{data}->{data}->{$secret_name}; } } } @@ -176,16 +375,11 @@ sub request_api { sub do_map { my ($self, %options) = @_; - foreach (@{$self->{map_option}}) { - next if (! /^(.+?)=%(.+)$/); - - my ($option, $map) = ($1, $2); - - $map = $self->{lookup_values}->{$2} if (defined($self->{lookup_values}->{$2})); - $option =~ s/-/_/g; - $options{option_results}->{$option} = $map; + foreach my $mapping (@{$self->{map_option}}) { + my ($opt_name, $opt_value) = $mapping =~ /^(.+?)=%(.+)$/ or next; + $opt_name =~ s/-/_/g; + $options{option_results}->{$opt_name} = defined($self->{lookup_values}->{$opt_value}) ? $self->{lookup_values}->{$opt_value} : $opt_value; } - } sub manage_options { @@ -219,7 +413,15 @@ To be used with an array containing keys/values saved in a secret path by resour =item B<--vault-config> -The path to the file defining access to the Centreon vault (/etc/centreon-engine/centreonvault.json by default) +Path to the file defining access to the Centreon vault (default: C). + +=item B<--vault-cache> + +Path to the file where the token to access the Centreon vault will be stored (default: C). + +=item B<--vault-env-file> + +Path to the file containing the APP_SECRET variable (default: C). =back @@ -228,3 +430,129 @@ The path to the file defining access to the Centreon vault (/etc/centreon-engine B. =cut + +=head1 NAME + +centreon::plugins::passwordmgr::centreonvault - Module for getting secrets from Centreon Vault. + +=head1 SYNOPSIS + + use centreon::plugins::passwordmgr::centreonvault; + + my $vault = centreon::plugins::passwordmgr::centreonvault->new(output => $output, options => $options); + $vault->manage_options(option_results => \%option_results); + +=head1 DESCRIPTION + +This module provides methods to retrieve secrets (passwords, SNMP communities, ...) from Centreon Vault (adequately +configured HashiCorp Vault). +It extracts and decrypt the information required to login to the vault from the vault configuration file, authenticates +to the vault, retrieves secrets, and maps them to the corresponding options for the centreon-plugins to work with. + +=head1 METHODS + +=head2 new + + my $vault = centreon::plugins::passwordmgr::centreonvault->new(%options); + +Creates a new `centreon::plugins::passwordmgr::centreonvault` object. The `%options` hash can include: + +=over 4 + +=item * output + +The output object for displaying debug and error messages. + +=item * options + +The options object for handling command-line options. + +=back + +=head2 extract_map_options + + $vault->extract_map_options(option_results => \%option_results); + +Extracts and maps options that match the Vault path regex pattern (C). The +`%option_results` hash should include the command-line options. + +=head2 vault_settings + + $vault->vault_settings(option_results => \%option_results); + +Loads and validates the Vault configuration from the specified file. +The `%option_results` hash should include the command-line options. + +=head2 get_decryption_key + + my $key = $vault->get_decryption_key(); + +Retrieves the decryption key from C environment variable. It will look for it in the the specified +environment file if it is not available in the environment variables. + +=head2 extract_and_decrypt + + my $decrypted_data = $vault->extract_and_decrypt(data => $data, key => $key); + +Decrypts the given data using the specified key. The options must include: + +=over 4 + +=item * data + +The base64-encoded data to decrypt. + +=item * key + +The base64-encoded decryption key. + +=back + +=head2 is_token_still_valid + + my $is_valid = $vault->is_token_still_valid(); + +Checks if there is a token in the cache and if it is still valid based on its expiration date. Returns 1 if valid, otherwise undef. + +=head2 check_authentication + + $vault->check_authentication(option_results => \%option_results); + +Checks the authentication status and retrieves a new token if necessary. The `%option_results` hash should include the command-line options. + +=head2 authenticate + + $vault->authenticate(); + +Authenticates to the Vault, retrieves a new token and stores it in the dedicated cache file. + +=head2 request_api + + $vault->request_api(option_results => \%option_results); + +Sends requests to the Vault API to retrieve secrets. The `%option_results` hash should include the command-line options. + +=head2 do_map + + $vault->do_map(option_results => \%option_results); + +Maps the retrieved secrets to the corresponding options. The `%option_results` hash should include the command-line options. +Calling this method will update the `%option_results` hash replacing vault paths with the retrieved secrets. + +=head2 manage_options + + $vault->manage_options(option_results => \%option_results); + +Manages the options by extracting, requesting, and mapping secrets. The `%option_results` hash should include the command-line options. + +NB: This is the main method to be called from outside the module. All other methods are intended to be used internally. + +=head1 AUTHOR + +Centreon + +=head1 LICENSE + +Licensed under the Apache License, Version 2.0. + +=cut diff --git a/tests/centreon/plugins/passwordmgr/centreonvault.mockoon.json b/tests/centreon/plugins/passwordmgr/centreonvault.mockoon.json new file mode 100644 index 0000000000..0161765966 --- /dev/null +++ b/tests/centreon/plugins/passwordmgr/centreonvault.mockoon.json @@ -0,0 +1,239 @@ +{ + "uuid": "5beab5e3-1b32-4ec1-a6d9-18c546c2d894", + "lastMigration": 33, + "name": "Centreonvault", + "endpointPrefix": "", + "latency": 0, + "port": 3000, + "hostname": "", + "folders": [], + "routes": [ + { + "uuid": "9623237f-f204-4972-ac46-8cfaadfa975a", + "type": "http", + "documentation": "", + "method": "post", + "endpoint": "v1/auth/approle/login", + "responses": [ + { + "uuid": "3bb67f80-c60a-41a9-b071-f5a559f19613", + "body": "{\n \"request_id\": \"r2p2c3po-b013-723a-24c7-ad80aa1fbddb\",\n \"lease_id\": \"\",\n \"renewable\": false,\n \"lease_duration\": 0,\n \"data\": null,\n \"wrap_info\": null,\n \"warnings\": null,\n \"auth\": {\n \"client_token\": \"hvs.thistokenisafakeonebutwillworkwiththetests\",\n \"accessor\": \"7PjTD&rpX53oqLRNa4C5t\",\n \"policies\": [\n \"default\",\n \"centreon-plugins\"\n ],\n \"token_policies\": [\n \"default\",\n \"omercier\"\n ],\n \"metadata\": {\n \"role_name\": \"centreon-plugins\"\n },\n \"lease_duration\": 2764800,\n \"renewable\": true,\n \"entity_id\": \"bbdov2-0dd9-97e8-66d6-3db885ccffd8\",\n \"token_type\": \"service\",\n \"orphan\": true,\n \"mfa_requirement\": null,\n \"num_uses\": 0\n },\n \"mount_type\": \"\"\n}", + "latency": 0, + "statusCode": 200, + "label": "", + "headers": [ + { + "key": "access-control-allow-headers", + "value": "Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With" + }, + { + "key": "access-control-allow-methods", + "value": "GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS" + }, + { + "key": "access-control-allow-origin", + "value": "*" + }, + { + "key": "content-security-policy", + "value": "default-src 'none'" + }, + { + "key": "content-type", + "value": "text/html; charset=utf-8" + }, + { + "key": "x-content-type-options", + "value": "nosniff" + } + ], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [ + { + "target": "query", + "modifier": "role_id", + "value": "thisroleidisinplaintext", + "invert": false, + "operator": "equals" + }, + { + "target": "query", + "modifier": "secret_id", + "value": "thissecretidisinplaintext", + "invert": false, + "operator": "equals" + } + ], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + }, + { + "uuid": "bba1ccb5-9415-4630-a82e-ff192b1f5680", + "body": "{\"errors\":[\"invalid role or secret ID\"]}", + "latency": 0, + "statusCode": 400, + "label": "", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null, + "streamingMode": null, + "streamingInterval": 0 + }, + { + "uuid": "5378cdb8-7126-4b58-aa23-ef79f5b06ba4", + "type": "http", + "documentation": "", + "method": "get", + "endpoint": "v1/myvault/data/snmp", + "responses": [ + { + "uuid": "ca08be91-0f07-42e1-8119-c2510c1b31f4", + "body": "{\"request_id\":\"bbdo2cbd-e3f0-d84c-b668-65416f0b9d97\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"data\":{\"Linux\":\"os/linux/snmp/linux\"},\"metadata\":{\"created_time\":\"2024-11-21T12:34:26.606125626Z\",\"custom_metadata\":null,\"deletion_time\":\"\",\"destroyed\":false,\"version\":1}},\"wrap_info\":null,\"warnings\":null,\"auth\":null,\"mount_type\":\"kv\"}\n", + "latency": 0, + "statusCode": 200, + "label": "", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [ + { + "target": "header", + "modifier": "X-Vault-Token", + "value": "hvs.thistokenisafakeonebutwillworkwiththetests", + "invert": false, + "operator": "equals" + } + ], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + }, + { + "uuid": "9bab45de-a545-4863-a8f6-7613a1d2ad64", + "latency": 0, + "statusCode": 404, + "label": "", + "headers": [ + { + "key": "access-control-allow-headers", + "value": "Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With" + }, + { + "key": "access-control-allow-methods", + "value": "GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS" + }, + { + "key": "access-control-allow-origin", + "value": "*" + }, + { + "key": "content-security-policy", + "value": "default-src 'none'" + }, + { + "key": "content-type", + "value": "text/html; charset=utf-8" + }, + { + "key": "x-content-type-options", + "value": "nosniff" + } + ], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": true, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null, + "streamingMode": null, + "streamingInterval": 0 + } + ], + "rootChildren": [ + { + "type": "route", + "uuid": "9623237f-f204-4972-ac46-8cfaadfa975a" + }, + { + "type": "route", + "uuid": "5378cdb8-7126-4b58-aa23-ef79f5b06ba4" + } + ], + "proxyMode": false, + "proxyHost": "", + "proxyRemovePrefix": false, + "tlsOptions": { + "enabled": false, + "type": "CERT", + "pfxPath": "", + "certPath": "", + "keyPath": "", + "caPath": "", + "passphrase": "" + }, + "cors": true, + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Access-Control-Allow-Methods", + "value": "GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS" + }, + { + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With" + } + ], + "proxyReqHeaders": [ + { + "key": "", + "value": "" + } + ], + "proxyResHeaders": [ + { + "key": "", + "value": "" + } + ], + "data": [], + "callbacks": [] +} \ No newline at end of file diff --git a/tests/centreon/plugins/passwordmgr/env b/tests/centreon/plugins/passwordmgr/env new file mode 100644 index 0000000000..2e79ece4df --- /dev/null +++ b/tests/centreon/plugins/passwordmgr/env @@ -0,0 +1 @@ +APP_SECRET=thiskeyismadefortestspurpose1234 diff --git a/tests/centreon/plugins/passwordmgr/vault_config_encrypted.json b/tests/centreon/plugins/passwordmgr/vault_config_encrypted.json new file mode 100644 index 0000000000..450465f2d1 --- /dev/null +++ b/tests/centreon/plugins/passwordmgr/vault_config_encrypted.json @@ -0,0 +1,10 @@ +{ + "name": "hashicorp_vault", + "url": "127.0.0.1", + "protocol": "http", + "salt": "controlhmachashingkeyforcontrols", + "port": 3000, + "root_path": "", + "role_id": "FO7/tbXY90gg+9igMXhLI2BYrGiRxOtmsgY8GlSVO0DHTO0DYGFnExCAuqPyVLoyvvjdba7Crl7TOb73H/QGMlFkfbE4/qAeiTF9ReMS1TnsUoxfTLKqCXERXGApkxOrSIhv/z6+UBKmQwPVhLwD7w==", + "secret_id": "odQ16TYwubSSi/m4mo88D8Trupbf+3ehgmfxA7wrEtbCEDciZciBRY2cc9Yb2yD+ivR64WR8RsPVPpGbPj1AFpu1TIwL88ic7zjGZaZiIr5kZenoi6xJquxQZbNW5t2N8JaUb/Qupp2wvQ2rxv9zoQ==" +} \ No newline at end of file diff --git a/tests/centreon/plugins/passwordmgr/vault_config_incomplete.json b/tests/centreon/plugins/passwordmgr/vault_config_incomplete.json new file mode 100644 index 0000000000..43e44a2fc9 --- /dev/null +++ b/tests/centreon/plugins/passwordmgr/vault_config_incomplete.json @@ -0,0 +1,10 @@ +{ + "name": "hashicorp_vault", + "url": "127.0.0.1", + "salt": "cetteclenestpasutiliseeinthetest", + "port": 3001, + "root_path": "", + "role_id": "", + "secret_id": "" +} + diff --git a/tests/centreon/plugins/passwordmgr/vault_config_plain.json b/tests/centreon/plugins/passwordmgr/vault_config_plain.json new file mode 100644 index 0000000000..2de5c21e6b --- /dev/null +++ b/tests/centreon/plugins/passwordmgr/vault_config_plain.json @@ -0,0 +1,11 @@ +{ + "name": "hashicorp_vault", + "url": "127.0.0.1", + "protocol": "http", + "salt": "", + "port": 3000, + "root_path": "", + "role_id": "thisroleidisinplaintext", + "secret_id": "thissecretidisinplaintext" +} + diff --git a/tests/os/linux/snmp/memory-centreonvault.robot b/tests/os/linux/snmp/memory-centreonvault.robot new file mode 100644 index 0000000000..b9359e3952 --- /dev/null +++ b/tests/os/linux/snmp/memory-centreonvault.robot @@ -0,0 +1,37 @@ +*** Settings *** +Documentation Centreonvault module + +Resource ${CURDIR}${/}..${/}..${/}..${/}resources/import.resource + +Suite Setup Start Mockoon ${MOCKOON_JSON} +Suite Teardown Stop Mockoon +Test Timeout 120s + + +*** Variables *** + +${CMD} ${CENTREON_PLUGINS} --plugin=os::linux::snmp::plugin --pass-manager=centreonvault --snmp-port=${SNMPPORT} --snmp-version=${SNMPVERSION} --hostname=${HOSTNAME} +${VAULT_CACHE} /var/lib/centreon/centplugins/centreonvault_cache +${VAULT_FILES} ${CURDIR}${/}..${/}..${/}..${/}centreon${/}plugins${/}passwordmgr +${MOCKOON_JSON} ${VAULT_FILES}${/}centreonvault.mockoon.json + +*** Test Cases *** +Linux Memory with vault ${tc} + [Tags] snmp linux vault mockoon + Remove File ${VAULT_CACHE} + ${command} Catenate + ... ${CMD} + ... --mode=memory + ... --snmp-community=secret::hashicorp_vault::myvault/data/snmp::${secret} + ... --vault-config=${vault_config} + ... --vault-cache=${VAULT_CACHE} + ... ${extra_options} + + Ctn Run Command And Check Result As Regexp ${command} ${expected_regexp} + + Examples: tc secret vault_config extra_options expected_regexp -- + ... 1 Linux ${EMPTY} ${EMPTY} UNKNOWN: Please provide a Centreon Vault configuration file path with --vault-config option + ... 2 Linux ${VAULT_FILES}${/}vault.json ${EMPTY} UNKNOWN: File '.*/centreon/plugins/passwordmgr/vault.json' could not be found. + ... 3 Linux ${VAULT_FILES}${/}vault_config_incomplete.json ${EMPTY} UNKNOWN: Unable to authenticate to the vault: role_id or secret_id is empty. + ... 4 Linux ${VAULT_FILES}${/}vault_config_plain.json --debug OK: Ram Total: 1.92 GB Used + ... 5 Linux ${VAULT_FILES}${/}vault_config_encrypted.json --vault-env-file=${VAULT_FILES}${/}env OK: Ram Total: 1.92 GB Used diff --git a/tests/resources/spellcheck/stopwords.txt b/tests/resources/spellcheck/stopwords.txt index 3ce5d83fd5..f8c2e567c4 100644 --- a/tests/resources/spellcheck/stopwords.txt +++ b/tests/resources/spellcheck/stopwords.txt @@ -28,10 +28,13 @@ aws AWSCLI --aws-role-arn Backbox +base64 blocked-by-uf --cacert-file cardtemperature +centreon Centreon +centreonvault --cert-pkcs12 --cert-pwd CloudWatch @@ -66,6 +69,7 @@ dfsrevent dns-resolve-time --dyn-mode -EncodedCommand +env ESX eth --exclude-fs