Skip to content

Commit 0fa82a2

Browse files
authored
enh(cli): enhance debugging of CLI plugins (#6100)
Enhance debugging of CLI plugins Refs: CTOR-1247
1 parent 11b8e6e commit 0fa82a2

6 files changed

Lines changed: 184 additions & 10 deletions

File tree

.gitleaksignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
tests/cloud/google/gcp/gcp_config.json:private-key:5
1+
tests/cloud/google/gcp/gcp_config.json:private-key:5
2+
tests/centreon/plugins/mask_secrets.t:generic-api-key:44
3+
tests/centreon/plugins/mask_secrets.t:curl-auth-header:17
4+
tests/centreon/plugins/mask_secrets.t:curl-auth-header:41

src/centreon/plugins/misc.pm

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ our @EXPORT_OK = qw/change_seconds
4747
slurp_file
4848
format_opt
4949
trim
50+
mask_secrets
5051
value_of/;
5152

5253
sub execute {
@@ -192,13 +193,19 @@ sub unix_execute {
192193
# On some equipment. Cannot get a pseudo terminal
193194
if (defined($options{ssh_pipe}) && $options{ssh_pipe} == 1) {
194195
$cmd = "echo '" . $sub_cmd . "' | " . $cmd . ' ' . join(' ', @$args);
196+
$options{output}->output_add(long_msg => 'execute command: ' . $cmd, show_password => $options{output}->show_password()) if $options{output}->is_debug();
197+
195198
($lerror, $stdout, $exit_code) = backtick(
196199
command => $cmd,
197200
timeout => $options{options}->{timeout},
198201
wait_exit => $wait_exit,
199202
redirect_stderr => $redirect_stderr
200203
);
201204
} else {
205+
if ($options{output}->is_debug()) {
206+
my $cmd = $cmd . join (' ', [ @$args, $sub_cmd // '']);
207+
$options{output}->output_add(long_msg => 'execute command: ' . $cmd, show_password => $options{output}->show_password())
208+
}
202209
($lerror, $stdout, $exit_code) = backtick(
203210
command => $cmd,
204211
arguments => [@$args, $sub_cmd],
@@ -213,6 +220,8 @@ sub unix_execute {
213220
$cmd .= $options{command} if (defined($options{command}));
214221
$cmd .= ' ' . $options{command_options} if (defined($options{command_options}));
215222

223+
$options{output}->output_add(long_msg => 'execute command: ' . $cmd, show_password => $options{output}->show_password()) if $options{output}->is_debug();
224+
216225
if (defined($options{no_shell_interpretation}) and $options{no_shell_interpretation} ne '') {
217226
my @args = split(' ',$cmd);
218227
($lerror, $stdout, $exit_code) = backtick(
@@ -961,6 +970,68 @@ sub format_opt($) {
961970
$_[0] =~ s/_/-/gr;
962971
}
963972

973+
# Attempts to mask secrets in a command line string by replacing them with $mask ( or *** if mask is not defined )
974+
# Be aware that this is only a try, there is no guarantee that all secrets will be masked!
975+
sub mask_secrets($;$) {
976+
my ($command, $mask) = @_;
977+
978+
return '' unless $command;
979+
980+
$mask = '***' unless defined $mask;
981+
982+
my $masked = $command;
983+
984+
# Handled cases with examples
985+
986+
# command -password P@s$W@rD -I100
987+
$masked =~ s/(\s+-+password[=\s:]+)[^\s'"]+/$1$mask/gi;
988+
989+
# command -token=P@s$W@rD -I100
990+
$masked =~ s/(\s+-+token[=\s:]+)[^\s'"]+/$1$mask/gi;
991+
992+
# curl --header "api-key: P@s$W@rD" https://test.com' OR curl -api-key P@s$W@rD
993+
$masked =~ s/(\s+-+api[_-]?key[=\s:]+)[^\s'"]+/$1$mask/gi;
994+
$masked =~ s/(api[_-]?key\s*:\s*)[^\s'"]+/$1$mask/gi;
995+
996+
# command -secret=P@s$W@rD -I100
997+
$masked =~ s/(\s+-+secret[=\s:]+)[^\s'"]+/$1$mask/gi;
998+
999+
# curl --header "secret: P@s$W@rD" https://test.com'
1000+
$masked =~ s/(secret\s*:\s*)[^\s'"]+/$1$mask/gi;
1001+
1002+
# command -authent=P@s$W@rD -I100
1003+
$masked =~ s/(\s+-+auth(ent)?[=\s:]+)[^\s'"]+/$1$mask/gi;
1004+
1005+
# command -cert-password=P@s$W@rD -I100
1006+
$masked =~ s/(\s+-+cert[_-]?password[=\s:]+)[^\s'"]+/$1$mask/gi;
1007+
# command -passphrase=P@s$W@rD -I100
1008+
$masked =~ s/(\s+-+passphrase[=\s:]+)[^\s'"]+/$1$mask/gi;
1009+
1010+
# snmpwalk -snmp-community MyCommunitString123 -v 2c localhost',
1011+
$masked =~ s/(\s+-+snmp[_-]?community[=\s:]+)[^\s'"]+/$1$mask/gi;
1012+
1013+
# snmpwalk --c MyCommunitString123 -v 2c localhost',
1014+
$masked =~ s/(\s-c\s+)[^\s'"]+/$1$mask/gi;
1015+
1016+
# mysql -u root -p PASSWORD database
1017+
$masked =~ s/(\s+-p\s+)[^\s'"]+/$1$mask/gi;
1018+
1019+
# mysql -u root -pPASSWORD database
1020+
$masked =~ s/(\s+-p)(?!assword\b)([A-Z])[^\s'"]*/$1$mask/gi;
1021+
1022+
my $common_auth = 'Bearer|Basic|Negotiate|X-API-Key|ApiKey|NTLM|AWS4-HMAC-SHA256';
1023+
1024+
# curl -H "Authorization: Bearer ABCDEF" http://test.com'
1025+
$masked =~ s/((?:$common_auth)\s+)[^\s'"]+/$1$mask/gi;
1026+
# curl -H "PVX-Authorization: ABCDEF" http://test.com'
1027+
$masked =~ s/((?:[\w-]*)Authorization\s*:\s*)(?!$common_auth)[^\s',;"]+/$1$mask/gi;
1028+
1029+
# https://admin:password@test.com:8080/api
1030+
$masked =~ s/([\w.-]+):([\w?!@#$%^&*._-]+)(@)/$1:$mask$3/g;
1031+
1032+
return $masked;
1033+
}
1034+
9641035
1;
9651036

9661037
__END__
@@ -1616,6 +1687,19 @@ Returns 1 if the string is excluded, 0 if it is included.
16161687
The string is excluded if $exclude_regexp is defined and matches the string, or if $include_regexp is defined and does
16171688
not match the string. The string will also be excluded if it is undefined.
16181689
1690+
=back
1691+
1692+
=head2 mask_secrets
1693+
1694+
my $to_print = centreon::plugins::misc::mask_secrets($unsafe_string, $mask);
1695+
1696+
Attempts to mask secrets in a command line string by replacing them with $mask ( or *** if mask is not defined )
1697+
Be aware that this is only a try, there is no guarantee that all secrets will be masked!
1698+
1699+
=over 4
1700+
1701+
=item * C<$ident> - name to convert.
1702+
16191703
=head1 AUTHOR
16201704
16211705
Centreon

src/centreon/plugins/output.pm

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright 2024 Centreon (http://www.centreon.com/)
2+
# Copyright 2026-Present Centreon (http://www.centreon.com/)
33
#
44
# Centreon is a full-fledged industry-strength solution that meets
55
# the needs in IT infrastructure and application monitoring for
@@ -22,7 +22,7 @@ package centreon::plugins::output;
2222

2323
use strict;
2424
use warnings;
25-
use centreon::plugins::misc;
25+
use centreon::plugins::misc qw/mask_secrets/;
2626

2727
sub new {
2828
my ($class, %options) = @_;
@@ -51,6 +51,7 @@ sub new {
5151
'verbose' => { name => 'verbose' },
5252
'debug' => { name => 'debug' },
5353
'debug-stream' => { name => 'debug_stream' },
54+
'show-password' => { name => 'show_password' },
5455
'opt-exit:s' => { name => 'opt_exit', default => 'unknown' },
5556
'output-xml' => { name => 'output_xml' },
5657
'output-json' => { name => 'output_json' },
@@ -226,6 +227,8 @@ sub output_add {
226227

227228
if (defined($options->{short_msg})) {
228229
chomp $options->{short_msg};
230+
$options->{short_msg} = mask_secrets($options->{short_msg}) if defined $options->{show_password} && !$options->{show_password};
231+
229232
if (defined($self->{global_short_concat_outputs}->{uc($options->{severity})})) {
230233
$self->{global_short_concat_outputs}->{uc($options->{severity})} .= $options->{separator} . $options->{short_msg};
231234
} else {
@@ -237,10 +240,13 @@ sub output_add {
237240
}
238241

239242
if (defined($options->{long_msg})) {
240-
chomp $options->{long_msg};
243+
if (($options->{debug} == 0 || defined($self->{option_results}->{debug})) || defined($self->{option_results}->{debug_stream})) {
244+
chomp $options->{long_msg};
245+
$options->{long_msg} = mask_secrets($options->{long_msg}) if defined $options->{show_password} && !$options->{show_password};
241246

242-
push @{$self->{global_long_output}}, $options->{long_msg} if ($options->{debug} == 0 || defined($self->{option_results}->{debug}));
243-
print $options->{long_msg} . "\n" if (defined($self->{option_results}->{debug_stream}));
247+
push @{$self->{global_long_output}}, $options->{long_msg} if ($options->{debug} == 0 || defined($self->{option_results}->{debug}));
248+
print $options->{long_msg} . "\n" if (defined($self->{option_results}->{debug_stream}));
249+
}
244250
}
245251
}
246252

@@ -995,6 +1001,13 @@ sub is_debug {
9951001
return 0;
9961002
}
9971003

1004+
sub show_password {
1005+
my ($self) = @_;
1006+
1007+
return $self->{option_results}->{show_password}
1008+
}
1009+
1010+
9981011
sub load_eval {
9991012
my ($self) = @_;
10001013

@@ -1571,6 +1584,11 @@ Display extended status information (long output).
15711584
15721585
Display debug messages.
15731586
1587+
=item B<--show-password>
1588+
1589+
By default, sensitive information in command lines is hidden in debug output and replaced with C<***> (however, debug logs may still display sensitive information).
1590+
Using the C<--show-password> option will display the passwords in plain text.
1591+
15741592
=item B<--filter-perfdata>
15751593
15761594
Filter perfdata that match the regexp.

tests/centreon/plugins/database_redis_parameters.t

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ my $cust = database::redis::custom::cli->new(
4747
);
4848
$options->{custom} = $cust;
4949

50-
foreach my $test ({ title => 'Test --key parameter', param => { key => 'private.key'}, expect => q(--key 'private.key') },
51-
{ title => 'Test --cert parameter', param => { cert => 'dummy.crt'}, expect => q(--cert 'dummy.crt') },
52-
{ title => 'Test --cacert parameter', param => { cacert => 'ca.crt'}, expect => q(--cacert 'ca.crt') },) {
53-
$cust->set_options(option_results => $test->{param} );
50+
foreach my $test ({ title => 'Test --key parameter', param => { cert => '', cacert => '', key => 'private.key' }, expect => q(--key 'private.key') },
51+
{ title => 'Test --cert parameter', param => { cert => 'dummy.crt', cacert => '', key => '' }, expect => q(--cert 'dummy.crt') },
52+
{ title => 'Test --cacert parameter', param => { cert => '', cacert => 'ca.crt', key => '' }, expect => q(--cacert 'ca.crt') },) {
53+
$cust->set_options(option_results => { %{$test->{param}}, ssh_hostname => '', tls => '', server => '', password => '', username => '', port => '' } );
5454
$cust->check_options();
5555

5656
$plugin->manage_selection(%$options);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use strict;
2+
use warnings;
3+
use Test2::V0;
4+
5+
use Test2::Tools::Compare qw{is like match};
6+
use FindBin;
7+
use lib "$FindBin::RealBin/../../../src";
8+
use centreon::plugins::misc qw/mask_secrets/;
9+
10+
sub mask_secrets_execute() {
11+
my @tests = ( { original => 'raidcom get system -password MySecretPass123 -I100',
12+
masked => 'raidcom get system -password *** -I100'
13+
},
14+
{ original => 'pairdisplay -password=SuperSecret123 -g GRP1',
15+
masked => 'pairdisplay -password=*** -g GRP1'
16+
},
17+
{ original => 'curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" https://test.com',
18+
masked => 'curl -H "Authorization: Bearer ***" https://test.com'
19+
},
20+
{ original => 'curl -H "Authorization: Basic ABCDEF" https://test.com',
21+
masked => 'curl -H "Authorization: Basic ***" https://test.com'
22+
},
23+
{ original => 'snmpwalk -snmp-community=tutu -v 2c localhost',
24+
masked => 'snmpwalk -snmp-community=*** -v 2c localhost'
25+
},
26+
{ original => 'snmpwalk -c MyCommunitString123 -v 2c localhost',
27+
masked => 'snmpwalk -c *** -v 2c localhost'
28+
},
29+
{ original => 'ssh -l admin user@host.com',
30+
masked => 'ssh -l admin user@host.com'
31+
},
32+
{ original => 'mysql -u root -pMyPassword123 database',
33+
masked => 'mysql -u root -p*** database'
34+
},
35+
{ original => 'mysql -u root -p MyPassword123 database',
36+
masked => 'mysql -u root -p *** database'
37+
},
38+
{ original => 'pg_dump -U postgres -W database',
39+
masked => 'pg_dump -U postgres -W database'
40+
},
41+
{ original => 'curl --header "api-key: sk-1234567890abcdef" https://test.com',
42+
masked => 'curl --header "api-key: ***" https://test.com'
43+
},
44+
{ original => 'prog get path -token=abc123def456 -I100',
45+
masked => 'prog get path -token=*** -I100'
46+
},
47+
{ original => 'PVX-Authorization: P@sSw@RdZ',
48+
masked => 'PVX-Authorization: ***'
49+
},
50+
{ original => 'Authorization: Basic ABCDEF',
51+
masked => 'Authorization: Basic ***'
52+
},
53+
{ original => 'https://admin:SecurePass@test.com:8080/api',
54+
masked => 'https://admin:***@test.com:8080/api',
55+
},
56+
{ original => '/tmp/centreon-plugins/test.pl --secret=secret',
57+
masked => '/tmp/centreon-plugins/test.pl --secret=***',
58+
},
59+
);
60+
61+
foreach my $test (@tests) {
62+
my $masked = mask_secrets($test->{original});
63+
ok($test->{masked} eq $masked, "Masks secrets in '".$test->{original} . "' => '" . $masked."'");
64+
}
65+
}
66+
67+
mask_secrets_execute();
68+
done_testing();

tests/centreon/plugins/sshcli.t

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ my $_fake_ssh = '/tmp/test_fake_ssh_'.$$;
2424
sub new { bless {}, shift }
2525
sub add_option_msg { }
2626
sub option_exit { die }
27+
sub is_debug { 0 }
2728
}
2829

2930
sub process_test {

0 commit comments

Comments
 (0)