2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-20 14:45:41 +00:00

Merge pull request #7779 from VersatusHPC/audit/cmdlog-test-boundary

refactor(xcatd): expose command log response handling
This commit is contained in:
Vinícius Ferrão
2026-08-29 20:49:57 -03:00
committed by GitHub
3 changed files with 378 additions and 323 deletions
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env perl
# IBM(c) 2007 EPL license http://www.eclipse.org/legal/epl-v10.html
package xCAT::CmdLog;
use strict;
use warnings;
use xCAT::xcatd;
sub new {
my ($class) = @_;
return bless {
buffer => '',
sensitive => 0,
}, $class;
}
sub start_response {
my ($self, $req, $req_redacted) = @_;
$self->{sensitive} = response_is_sensitive($req, $req_redacted);
return;
}
sub collect {
my ($self, $response, $local_server) = @_;
$self->{buffer} .= format_response($response, $local_server);
return 0;
}
sub finalize {
my ($self) = @_;
my $response = finalize_response($self->{buffer}, $self->{sensitive});
$self->{buffer} = '';
$self->{sensitive} = 0;
return $response;
}
sub response_is_sensitive {
my ($req, $req_redacted) = @_;
return 1 if defined $req->{command}->[0]
and ($req->{command}->[0] eq 'getcredentials' or $req->{command}->[0] eq 'lsvm');
return 1 if join(' ', @{ $req->{arg} || [] }) =~ /passw/i;
return 1 if xCAT::xcatd->secret_in_request($req->{command}->[0], $req->{arg});
return 1 if $req_redacted;
return 0;
}
sub finalize_response {
my ($buffer, $sensitive) = @_;
return "*REDACTED*\n"
if $sensitive or xCAT::xcatd->secret_in_response($buffer);
return $buffer;
}
sub format_response {
my ($response, $local_server) = @_;
my $response_log = "";
if (exists($response->{xcatresponse}->[0]->{serverdone})
&& !exists($response->{xcatresponse}->[0]->{error})) {
return $response_log;
}
my $responses;
if (exists($response->{xcatresponse})) {
$responses = $response->{xcatresponse};
} else {
push @{$responses}, $response;
}
return $response_log if ref($responses) ne 'ARRAY' or scalar(@$responses) == 0;
foreach my $item (@{$responses}) {
my $rsp = $item;
my $msgsource = "";
$msgsource = $rsp->{xcatdsource}->[0] if $rsp->{xcatdsource};
$msgsource = "" if $local_server eq $msgsource;
if ($rsp->{error}) {
if (ref($rsp->{error}) eq 'ARRAY') {
foreach my $text (@{ $rsp->{error} }) {
my $desc = "$text";
$desc = "[$msgsource]: $desc" if $desc && $msgsource;
$desc = "Error: $desc" unless $rsp->{NoErrorPrefix};
$response_log .= "$desc\n";
}
} elsif (defined($rsp->{error})) {
my $desc = $rsp->{error};
$desc = "[$msgsource]: $desc" if $desc && $msgsource;
$desc = "Error: $desc" unless $rsp->{NoErrorPrefix};
$response_log .= "$desc\n";
}
}
if ($rsp->{warning}) {
if (ref($rsp->{warning}) eq 'ARRAY') {
foreach my $text (@{ $rsp->{warning} }) {
my $desc = "$text";
$desc = "[$msgsource]: $desc" if $desc && $msgsource;
$desc = "Warning: $desc" unless $rsp->{NoWarnPrefix};
$response_log .= "$desc\n";
}
} elsif (defined($rsp->{warning})) {
my $desc = $rsp->{warning};
$desc = "[$msgsource]: $desc" if $desc && $msgsource;
$desc = "Warning: $desc" unless $rsp->{NoWarnPrefix};
$response_log .= "$desc\n";
}
}
if ($rsp->{info}) {
if (ref($rsp->{info}) eq 'ARRAY') {
foreach my $text (@{ $rsp->{info} }) {
my $desc = "$text";
$desc = "[$msgsource]: $desc" if $desc && $msgsource;
$response_log .= "$desc\n";
}
} else {
my $desc = $rsp->{info};
$desc = "[$msgsource]: $desc" if $desc && $msgsource;
$response_log .= "$desc\n";
}
}
if ($rsp->{sinfo}) {
if (ref($rsp->{sinfo}) eq 'ARRAY') {
foreach my $text (@{ $rsp->{sinfo} }) {
$response_log .= "$text " if defined $text;
}
} elsif (defined($rsp->{sinfo})) {
$response_log .= $rsp->{sinfo} . " ";
}
}
my $nodes = $rsp->{node};
$nodes = [$nodes] unless ref $nodes eq 'ARRAY';
if (scalar @{$nodes}) {
foreach my $node (@$nodes) {
my $desc;
if (ref($node->{name}) eq 'ARRAY') {
$desc = $node->{name}->[0];
} else {
$desc = $node->{name};
}
if ($node->{error} && defined($node->{error}->[0])) {
if ($desc) {
$desc = "$desc: [$msgsource]" if $msgsource;
} else {
$desc = "[$msgsource]" if $msgsource;
}
$desc .= ": Error: " . $node->{error}->[0];
}
if ($node->{warning} && defined($node->{warning}->[0])) {
if ($desc) {
$desc = "$desc: [$msgsource]" if $msgsource;
} else {
$desc = "[$msgsource]" if $msgsource;
}
$desc .= ": Warning: " . $node->{warning}->[0];
}
if ($node->{data}) {
if ($desc) {
$desc = "$desc: [$msgsource]" if $msgsource;
} else {
$desc = "[$msgsource]" if $msgsource;
}
if (ref(\($node->{data})) eq 'SCALAR') {
$desc = $desc . ": " . $node->{data} if defined $node->{data};
} elsif (ref($node->{data}) eq 'HASH') {
if ($node->{data}->{desc}) {
if (ref($node->{data}->{desc}) eq 'ARRAY') {
$desc = $desc . ": " . $node->{data}->{desc}->[0]
if defined $node->{data}->{desc}->[0];
} else {
$desc = $desc . ": " . $node->{data}->{desc}
if defined $node->{data}->{desc};
}
}
if ($node->{data}->{contents}) {
if (ref($node->{data}->{contents}) eq 'ARRAY') {
$desc = "$desc: " . $node->{data}->{contents}->[0]
if defined $node->{data}->{contents}->[0];
} else {
$desc = "$desc: " . $node->{data}->{contents}
if defined $node->{data}->{contents};
}
}
} elsif (ref(\($node->{data}->[0])) eq 'SCALAR') {
$desc = $desc . ": " . $node->{data}->[0]
if defined $node->{data}->[0];
} else {
if ($node->{data}->[0]->{desc}
&& defined($node->{data}->[0]->{desc}->[0])) {
$desc = $desc . ": " . $node->{data}->[0]->{desc}->[0];
}
if ($node->{data}->[0]->{contents}
&& defined($node->{data}->[0]->{contents}->[0])) {
$desc = "$desc: " . $node->{data}->[0]->{contents}->[0];
}
}
}
$response_log .= "$desc\n" if $desc;
}
}
foreach my $key (keys %{$rsp}) {
next if $key ne 'data';
if ($rsp->{data}) {
if (ref($rsp->{data}) eq 'ARRAY') {
foreach my $data_entry (@{ $rsp->{data} }) {
my $desc;
if (ref(\($data_entry)) eq 'SCALAR') {
$desc = $data_entry;
} else {
$desc = $data_entry->{desc}->[0] if $data_entry->{desc};
if ($data_entry->{contents}) {
if ($desc) {
$desc = "$desc: " . $data_entry->{contents}->[0]
if defined $data_entry->{contents}->[0];
} else {
$desc = $data_entry->{contents}->[0];
}
}
}
$response_log .= "$desc\n" if $desc;
}
} else {
$response_log .= $rsp->{data} . "\n";
}
}
}
}
return $response_log;
}
1;
+8 -241
View File
@@ -51,6 +51,7 @@ use xCAT::TableUtils;
use xCAT::NetworkUtils;
use xCAT::MsgUtils;
use xCAT::xcatd;
use xCAT::CmdLog;
use xCAT::State;
my $os = xCAT::Utils->osver();
my $arch = `uname -p`;
@@ -224,8 +225,7 @@ if ($tmp) {
}
my $cmdlog_alllog = "====================================================\n";
my $cmdlog_starttime=undef;
my $cmdlog_response_buffer = "";
my $cmdlog_response_sensitive = 0;
my $cmdlog_response = xCAT::CmdLog->new();
# ----used for command log end---------
my $enable_perf = $sitetab->getAttribs({'key'=>'enableperf'},'value');
@@ -2892,7 +2892,7 @@ sub service_connection {
$cmdlog_request = xCAT::xcatd->redact_password($cmdlog_request);
$cmdlog_req_redacted = 1 if $cmdlog_request ne $cmdlog_before_redact;
$cmdlog_alllog .= $cmdlog_request . "\n[Response]\n";
$cmdlog_response_sensitive = cmdlog_response_is_sensitive($req, $cmdlog_req_redacted);
$cmdlog_response->start_response($req, $cmdlog_req_redacted);
# ----used for command log end----------
@@ -3400,254 +3400,21 @@ sub disable_callingtrace {
=head3 cmdlog_collectlog
Used by recording command output feature.
collecting each output for one specific command
The most part of this subroutine logic comes from handle_response subroutine in Client.pm
Collect one response fragment for the current command.
Response formatting and buffering are handled by xCAT::CmdLog.
Returns:
0 -> successful
1 -> failed
=cut
# --------------------------------------------------------------------------------
sub cmdlog_collectlog() {
my $rsponse = shift;
my $rsp_log = "";
if ((exists($rsponse->{xcatresponse}->[0]->{serverdone})) && (!exists($rsponse->{xcatresponse}->[0]->{error}))) { return 0; }
my $rsp;
if (exists($rsponse->{xcatresponse})) {
$rsp = $rsponse->{xcatresponse};
} else {
push @{$rsp}, $rsponse;
}
if (ref($rsp) ne 'ARRAY') { return 0; }
if (scalar(@$rsp) == 0) { return 0; }
foreach my $tmprsp (@{$rsp}) {
$rsp = $tmprsp;
# handle response
my $msgsource = "";
$msgsource = $rsp->{xcatdsource}->[0] if ($rsp->{xcatdsource});
#Only show response source when it is from different service node
$msgsource = "" if ($MYXCATSERVER eq $msgsource);
# Handle errors
if ($rsp->{error}) {
if (ref($rsp->{error}) eq 'ARRAY') {
foreach my $text (@{ $rsp->{error} }) {
my $desc = "$text";
$desc = "[$msgsource]: $desc" if ($desc && $msgsource);
$desc = "Error: $desc" unless ($rsp->{NoErrorPrefix});
$rsp_log .= "$desc\n";
}
}
else {
if (defined($rsp->{error})) {
my $desc = $rsp->{error};
$desc = "[$msgsource]: $desc" if ($desc && $msgsource);
$desc = "Error: $desc" unless ($rsp->{NoErrorPrefix});
$rsp_log .= "$desc\n";
}
}
}
if ($rsp->{warning}) {
if (ref($rsp->{warning}) eq 'ARRAY') {
foreach my $text (@{ $rsp->{warning} }) {
my $desc = "$text";
$desc = "[$msgsource]: $desc" if ($desc && $msgsource);
$desc = "Warning: $desc" unless ($rsp->{NoWarnPrefix});
$rsp_log .= "$desc\n";
}
}
else {
if (defined($rsp->{warning})) {
my $desc = $rsp->{warning};
$desc = "[$msgsource]: $desc" if ($desc && $msgsource);
$desc = "Warning: $desc" unless ($rsp->{NoWarnPrefix});
$rsp_log .= "$desc\n";
}
}
}
if ($rsp->{info}) {
if (ref($rsp->{info}) eq 'ARRAY') {
foreach my $text (@{ $rsp->{info} }) {
my $desc = "$text";
$desc = "[$msgsource]: $desc" if ($desc && $msgsource);
$rsp_log .= "$desc\n";
}
} else {
my $desc = $rsp->{info};
$desc = "[$msgsource]: $desc" if ($desc && $msgsource);
$rsp_log .= "$desc\n";
}
}
if ($rsp->{sinfo}) {
if (ref($rsp->{sinfo}) eq 'ARRAY') {
foreach my $text (@{ $rsp->{sinfo} }) {
if (defined($text)) {
$rsp_log .= "$text ";
}
}
} else {
if (defined($rsp->{sinfo})) {
$rsp_log .= $rsp->{sinfo} . " ";
}
}
}
# Handle {node} structure
my $errflg = 0;
my $nodes = ($rsp->{node});
unless (ref $nodes eq 'ARRAY') {
$nodes = [$nodes];
}
if (scalar @{$nodes}) {
my $node;
foreach $node (@$nodes) {
my $desc;
if (ref($node->{name}) eq 'ARRAY') {
$desc = $node->{name}->[0];
} else {
$desc = $node->{name};
}
if (($node->{error}) && defined($node->{error}->[0])) {
if ($desc) {
$desc = "$desc: [$msgsource]" if ($msgsource);
} else {
$desc = "[$msgsource]" if ($msgsource);
}
$desc .= ": Error: " . $node->{error}->[0];
$errflg = 1;
}
if (($node->{warning}) && defined($node->{warning}->[0])){
if ($desc) {
$desc = "$desc: [$msgsource]" if ($msgsource);
} else {
$desc = "[$msgsource]" if ($msgsource);
}
$desc .= ": Warning: " . $node->{warning}->[0];
$errflg = 1;
}
if ($node->{data}) {
if ($desc) {
$desc = "$desc: [$msgsource]" if ($msgsource);
} else {
$desc = "[$msgsource]" if ($msgsource);
}
if (ref(\($node->{data})) eq 'SCALAR') {
if (defined($node->{data})) {
$desc = $desc . ": " . $node->{data};
}
} elsif (ref($node->{data}) eq 'HASH') {
if ($node->{data}->{desc}) {
if (ref($node->{data}->{desc}) eq 'ARRAY') {
if (defined($node->{data}->{desc}->[0])) {
$desc = $desc . ": " . $node->{data}->{desc}->[0];
}
} else {
if (defined($node->{data}->{desc})) {
$desc = $desc . ": " . $node->{data}->{desc};
}
}
}
if ($node->{data}->{contents}) {
if (ref($node->{data}->{contents}) eq 'ARRAY') {
if (defined($node->{data}->{contents}->[0])) {
$desc = "$desc: " . $node->{data}->{contents}->[0];
}
} else {
if (defined($node->{data}->{contents})) {
$desc = "$desc: " . $node->{data}->{contents};
}
}
}
} elsif (ref(\($node->{data}->[0])) eq 'SCALAR') {
if (defined($node->{data}->[0])) {
$desc = $desc . ": " . $node->{data}->[0];
}
} else {
if ($node->{data}->[0]->{desc}) {
if (defined($node->{data}->[0]->{desc}->[0])) {
$desc = $desc . ": " . $node->{data}->[0]->{desc}->[0];
}
}
if ($node->{data}->[0]->{contents}) {
if (defined($node->{data}->[0]->{contents}->[0])) {
$desc = "$desc: " . $node->{data}->[0]->{contents}->[0];
}
}
}
}
if ($desc) {
if ($errflg == 1) {
$rsp_log .= "$desc\n";
} else {
$rsp_log .= "$desc\n";
}
}
}
}
# Handle {data} structure with no nodes
foreach my $mykey (keys %{$rsp}) {
if ($mykey ne "data") { next; }
if ($rsp->{data}) {
if (ref($rsp->{data}) eq 'ARRAY') {
my $data = ($rsp->{data});
my $data_entry;
foreach $data_entry (@$data) {
my $desc;
if (ref(\($data_entry)) eq 'SCALAR') {
$desc = $data_entry;
} else {
if ($data_entry->{desc}) {
$desc = $data_entry->{desc}->[0];
}
if ($data_entry->{contents}) {
if ($desc) {
if (defined($data_entry->{contents}->[0])) {
$desc = "$desc: " . $data_entry->{contents}->[0];
}
} else {
$desc = $data_entry->{contents}->[0];
}
}
}
if ($desc) {
$rsp_log .= "$desc\n";
}
}
} else {
$rsp_log .= $rsp->{data} . "\n";
}
}
}
}
$cmdlog_response_buffer .= $rsp_log;
return 0;
}
sub cmdlog_response_is_sensitive {
my ($req, $req_redacted) = @_;
return 1 if defined $req->{command}->[0] and ($req->{command}->[0] eq 'getcredentials' or $req->{command}->[0] eq 'lsvm');
return 1 if join(' ', @{ $req->{arg} || [] }) =~ /passw/i;
return 1 if xCAT::xcatd->secret_in_request($req->{command}->[0], $req->{arg});
return 1 if $req_redacted;
return 0;
my $response = shift;
return $cmdlog_response->collect($response, $MYXCATSERVER);
}
sub cmdlog_finalize_response {
if ($cmdlog_response_sensitive or xCAT::xcatd->secret_in_response($cmdlog_response_buffer)) {
$cmdlog_response_buffer = "*REDACTED*\n";
}
$cmdlog_alllog .= $cmdlog_response_buffer;
$cmdlog_response_buffer = "";
$cmdlog_response_sensitive = 0;
$cmdlog_alllog .= $cmdlog_response->finalize();
}
# --------------------------------------------------------------------------------
+134 -82
View File
@@ -3,116 +3,100 @@ use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../lib";
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use Test::More;
use xCAT::CmdLog;
use XCAT::Test::File qw(repo_path slurp_repo_file);
use xCAT::xcatd;
my $xcatd = repo_path('xCAT-server/sbin/xcatd');
plan skip_all => 'xcatd not found' unless -r $xcatd;
my $src = slurp_repo_file('xCAT-server/sbin/xcatd');
# Extract the three command-log response subs and load them. xcatd is present,
# so a sub that cannot be extracted is a hard failure, not a skip.
my %sub;
for my $name (qw(cmdlog_response_is_sensitive cmdlog_finalize_response cmdlog_collectlog)) {
my ($body) = $src =~ /(^sub \Q$name\E\b.*?^\})/ms;
BAIL_OUT("could not extract $name from xcatd") unless $body;
$body =~ s/^sub \Q$name\E\(\)/sub $name/m;
$sub{$name} = $body;
}
our $cmdlog_alllog;
our $cmdlog_response_buffer;
our $cmdlog_response_sensitive;
our $MYXCATSERVER = "";
{
no strict;
no warnings;
eval "$sub{cmdlog_response_is_sensitive}\n$sub{cmdlog_finalize_response}\n$sub{cmdlog_collectlog}";
}
die "eval of command-log subs failed: $@" if $@;
# Classification from the request.
ok(cmdlog_response_is_sensitive({ command => ['getcredentials'], arg => [] }, ''),
ok(xCAT::CmdLog::response_is_sensitive(
{ command => ['getcredentials'], arg => [] }, ''),
'getcredentials is a sensitive-response command');
ok(cmdlog_response_is_sensitive({ command => ['lsvm'], arg => [] }, 0),
ok(xCAT::CmdLog::response_is_sensitive(
{ command => ['lsvm'], arg => [] }, 0),
'lsvm returns the directory entry with its passwords, so it is sensitive');
ok(cmdlog_response_is_sensitive({ command => ['gettab'], arg => ['key=xcat', 'passwd.password'] }, ''),
ok(xCAT::CmdLog::response_is_sensitive(
{ command => ['gettab'], arg => ['key=xcat', 'passwd.password'] }, ''),
'gettab of a passwd column is sensitive');
ok(cmdlog_response_is_sensitive({ command => ['tabdump'], arg => ['passwd'] }, ''),
ok(xCAT::CmdLog::response_is_sensitive(
{ command => ['tabdump'], arg => ['passwd'] }, ''),
'tabdump passwd is sensitive');
ok(cmdlog_response_is_sensitive({ command => ['rspconfig'], arg => [] }, 1),
ok(xCAT::CmdLog::response_is_sensitive(
{ command => ['rspconfig'], arg => [] }, 1),
'a redacted request is sensitive');
ok(!cmdlog_response_is_sensitive({ command => ['rpower'], arg => ['n1', 'stat'] }, 0),
ok(!xCAT::CmdLog::response_is_sensitive(
{ command => ['rpower'], arg => ['n1', 'stat'] }, 0),
'a benign request is not sensitive');
# A secret attribute with no "passw" in its name must classify through the
# shared secret set, not the text heuristic.
ok(cmdlog_response_is_sensitive({ command => ['gettab'], arg => ['node=pdu01', 'pdu.authkey'] }, 0),
ok(xCAT::CmdLog::response_is_sensitive(
{ command => ['gettab'], arg => ['node=pdu01', 'pdu.authkey'] }, 0),
'gettab of an authentication key is sensitive');
ok(cmdlog_response_is_sensitive({ command => ['gettab'], arg => ['node=pdu01', 'pdu.privkey'] }, 0),
ok(xCAT::CmdLog::response_is_sensitive(
{ command => ['gettab'], arg => ['node=pdu01', 'pdu.privkey'] }, 0),
'gettab of a privacy key is sensitive');
ok(cmdlog_response_is_sensitive({ command => ['gettab'], arg => ['key=snmpc', 'site.value'] }, 0),
ok(xCAT::CmdLog::response_is_sensitive(
{ command => ['gettab'], arg => ['key=snmpc', 'site.value'] }, 0),
'gettab of the snmpc site value is sensitive');
ok(!cmdlog_response_is_sensitive({ command => ['gettab'], arg => ['key=domain', 'site.value'] }, 0),
ok(!xCAT::CmdLog::response_is_sensitive(
{ command => ['gettab'], arg => ['key=domain', 'site.value'] }, 0),
'gettab of a plain site value is not sensitive');
# A dump of a whole table that owns a secret column returns the bare values.
ok(cmdlog_response_is_sensitive({ command => ['tabdump'], arg => ['token'] }, 0),
ok(xCAT::CmdLog::response_is_sensitive(
{ command => ['tabdump'], arg => ['token'] }, 0),
'tabdump of the token table is sensitive');
ok(cmdlog_response_is_sensitive({ command => ['tabdump'], arg => ['prodkey'] }, 0),
ok(xCAT::CmdLog::response_is_sensitive(
{ command => ['tabdump'], arg => ['prodkey'] }, 0),
'tabdump of the prodkey table is sensitive');
ok(cmdlog_response_is_sensitive({ command => ['tabdump'], arg => ['site'] }, 0),
ok(xCAT::CmdLog::response_is_sensitive(
{ command => ['tabdump'], arg => ['site'] }, 0),
'tabdump of the site table is sensitive');
ok(cmdlog_response_is_sensitive({ command => ['tabdump'], arg => ['-w', 'key==snmpc', 'site'] }, 0),
ok(xCAT::CmdLog::response_is_sensitive(
{ command => ['tabdump'], arg => ['-w', 'key==snmpc', 'site'] }, 0),
'a filtered site dump is sensitive');
ok(!cmdlog_response_is_sensitive({ command => ['tabdump'], arg => ['networks'] }, 0),
ok(!xCAT::CmdLog::response_is_sensitive(
{ command => ['tabdump'], arg => ['networks'] }, 0),
'tabdump of the networks table is not sensitive');
ok(cmdlog_response_is_sensitive({ command => ['nodels'], noderange => ['node01'], arg => ['prodkey'] }, 0),
ok(xCAT::CmdLog::response_is_sensitive(
{ command => ['nodels'], noderange => ['node01'], arg => ['prodkey'] }, 0),
'nodels of a whole secret table is sensitive');
ok(!cmdlog_response_is_sensitive({ command => ['nodels'], noderange => ['switches'], arg => [] }, 0),
ok(!xCAT::CmdLog::response_is_sensitive(
{ command => ['nodels'], noderange => ['switches'], arg => [] }, 0),
'a group named like a secret table is not sensitive');
ok(!cmdlog_response_is_sensitive({ command => ['nodels'], noderange => ['node01'], arg => ['nodetype'] }, 0),
ok(!xCAT::CmdLog::response_is_sensitive(
{ command => ['nodels'], noderange => ['node01'], arg => ['nodetype'] }, 0),
'nodels of a benign table is not sensitive');
# Drive collect(s) then finalize, returning what was appended to the log.
sub run {
my ($sensitive, @responses) = @_;
$cmdlog_alllog = "";
$cmdlog_response_buffer = "";
$cmdlog_response_sensitive = $sensitive;
cmdlog_collectlog({ xcatresponse => [ { data => [$_] } ] }) for @responses;
cmdlog_finalize_response();
return $cmdlog_alllog;
my $cmdlog = xCAT::CmdLog->new();
$cmdlog->start_response(
{ command => ['rpower'], arg => ['node01', 'stat'] },
$sensitive,
);
$cmdlog->collect(
{ xcatresponse => [ { data => [$_] } ] },
'',
) for @responses;
return $cmdlog->finalize();
}
# A bare passwd value (gettab passwd.password) has no "passw" in the response,
# so only the request classification catches it.
my $bare = run(1, "S3cr3tPW");
my $bare = run(1, 'S3cr3tPW');
unlike($bare, qr/S3cr3tPW/, 'a bare passwd value is not logged');
like($bare, qr/\*REDACTED\*/, 'a sensitive response is redacted');
# A secret split across callbacks is redacted regardless of order.
unlike(run(0, "password", "S3cr3tPW"), qr/S3cr3tPW/,
unlike(run(0, 'password', 'S3cr3tPW'), qr/S3cr3tPW/,
'multi-callback, passw first: the secret fragment is not logged');
unlike(run(0, "S3cr3tPW", "password"), qr/S3cr3tPW/,
unlike(run(0, 'S3cr3tPW', 'password'), qr/S3cr3tPW/,
'multi-callback, secret first: the earlier fragment is redacted too');
# Fallback: a response mentioning a password is redacted even without request signal.
like(run(0, "invalid password for node"), qr/\*REDACTED\*/,
like(run(0, 'invalid password for node'), qr/\*REDACTED\*/,
'a response mentioning a password is redacted by the fallback');
# A benign response is logged verbatim.
my $benign = run(0, "node01: on");
my $benign = run(0, 'node01: on');
like($benign, qr/node01: on/, 'a benign response is logged verbatim');
unlike($benign, qr/\*REDACTED\*/, 'a benign response is not redacted');
# A detailed object listing expands attributes the request never named.
my $lsdef = run(0, 'Object name: pdu01', ' authkey=AUTH_SECRET', ' privkey=PRIV_SECRET');
unlike($lsdef, qr/AUTH_SECRET|PRIV_SECRET/, 'implicit lsdef secrets are not logged');
like($lsdef, qr/\*REDACTED\*/, 'lsdef response containing secret attributes is redacted');
@@ -121,19 +105,87 @@ unlike($colon, qr/AAAAA/, 'a colon separated secret column is redacted');
my $plain = run(0, 'Object name: node01', ' groups=compute', ' mgt=ipmi');
unlike($plain, qr/\*REDACTED\*/, 'a benign object listing is not redacted');
# Two requests on one connection: the finalizer runs at the next command's
# start. The earlier response must be preserved (redacted), the flag reset, and
# the next benign response neither lost nor over-redacted.
$cmdlog_alllog = "";
$cmdlog_response_buffer = "";
$cmdlog_response_sensitive = 1;
cmdlog_collectlog({ xcatresponse => [ { data => ["SECRET_N"] } ] });
cmdlog_finalize_response();
is($cmdlog_response_sensitive, 0, 'finalize clears the sensitive flag');
cmdlog_collectlog({ xcatresponse => [ { data => ["benign_np1"] } ] });
cmdlog_finalize_response();
like($cmdlog_alllog, qr/\*REDACTED\*/, 'the earlier sensitive response is preserved as redacted, not lost');
like($cmdlog_alllog, qr/benign_np1/, 'the next benign response is logged, not over-redacted');
unlike($cmdlog_alllog, qr/SECRET_N/, 'the earlier secret is not leaked');
my $connection = xCAT::CmdLog->new();
$connection->start_response(
{ command => ['getcredentials'], arg => [] },
0,
);
is(
$connection->collect(
{ xcatresponse => [ { data => ['SECRET_N'] } ] },
'',
),
0,
'collecting a command response succeeds',
);
my $first = $connection->finalize();
is($connection->{sensitive}, 0, 'finalization resets response sensitivity');
$connection->start_response(
{ command => ['rpower'], arg => ['node01', 'stat'] },
0,
);
$connection->collect(
{ xcatresponse => [ { data => ['benign_np1'] } ] },
'',
);
my $second = $connection->finalize();
like($first, qr/\*REDACTED\*/, 'a sensitive response is preserved as redacted');
unlike($first, qr/SECRET_N/, 'the sensitive response does not leak');
like($second, qr/benign_np1/, 'a benign response after a sensitive request is preserved');
unlike($second, qr/\*REDACTED\*/, 'the benign response is not over-redacted');
unlike($second, qr/SECRET_N/, 'the finalized secret is not replayed in the next response');
my $local_connection = xCAT::CmdLog->new();
$local_connection->start_response(
{ command => ['rpower'], arg => ['node01', 'stat'] },
0,
);
$local_connection->collect(
{
xcatresponse => [ {
xcatdsource => ['management'],
info => ['working'],
} ],
},
'management',
);
is(
$local_connection->finalize(),
"working\n",
'collect forwards the local server identity to response formatting',
);
is(
xCAT::CmdLog::format_response(
{ xcatresponse => [ { serverdone => [1] } ] },
'management',
),
'',
'a terminal serverdone response does not add log text',
);
is(
xCAT::CmdLog::format_response(
{
xcatresponse => [ {
xcatdsource => ['service'],
error => ['failed'],
warning => ['careful'],
info => ['working'],
sinfo => ['progress'],
node => [ {
name => ['node01'],
data => ['on'],
} ],
} ],
},
'management',
),
"Error: [service]: failed\n"
. "Warning: [service]: careful\n"
. "[service]: working\n"
. "progress node01: [service]: on\n",
'response formatting preserves source, severity, progress, and node data',
);
done_testing();