feat(cli): scadalink audit query subcommand (#23 M8)

This commit is contained in:
Joseph Doherty
2026-05-20 21:55:38 -04:00
parent 3263b39477
commit 2fa46ed400
8 changed files with 864 additions and 3 deletions
+145 -3
View File
@@ -24,22 +24,164 @@ public static class AuditCommands
private static Command BuildQuery(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var sinceOption = new Option<string?>("--since") { Description = "Start time: relative (1h, 24h, 7d) or ISO-8601" };
var untilOption = new Option<string?>("--until") { Description = "End time: relative (1h, 24h, 7d) or ISO-8601" };
var channelOption = new Option<string?>("--channel") { Description = "Filter by channel (OutboundApi, OutboundDb, Notification, InboundApi)" };
var kindOption = new Option<string?>("--kind") { Description = "Filter by event kind" };
var statusOption = new Option<string?>("--status") { Description = "Filter by status (single value)" };
var siteOption = new Option<string?>("--site") { Description = "Filter by source site ID" };
var instanceOption = new Option<string?>("--instance") { Description = "Filter by instance" };
var targetOption = new Option<string?>("--target") { Description = "Filter by target (external system, DB connection, notification list)" };
var actorOption = new Option<string?>("--actor") { Description = "Filter by actor" };
var correlationIdOption = new Option<string?>("--correlation-id") { Description = "Filter by correlation ID" };
var errorsOnlyOption = new Option<bool>("--errors-only") { Description = "Show only failed events (status=Failed; overrides --status)" };
var pageSizeOption = new Option<int>("--page-size") { Description = "Events per page (1-1000)" };
pageSizeOption.DefaultValueFactory = _ => 100;
var allOption = new Option<bool>("--all") { Description = "Fetch every page, following the keyset cursor" };
var cmd = new Command("query") { Description = "Query audit log events" };
cmd.SetAction((ParseResult result) => 0);
cmd.Add(sinceOption);
cmd.Add(untilOption);
cmd.Add(channelOption);
cmd.Add(kindOption);
cmd.Add(statusOption);
cmd.Add(siteOption);
cmd.Add(instanceOption);
cmd.Add(targetOption);
cmd.Add(actorOption);
cmd.Add(correlationIdOption);
cmd.Add(errorsOnlyOption);
cmd.Add(pageSizeOption);
cmd.Add(allOption);
cmd.SetAction(async (ParseResult result) =>
{
var connection = AuditCommandHelpers.ResolveConnection(result, urlOption, usernameOption, passwordOption);
if (connection.Error != null)
{
OutputFormatter.WriteError(connection.Error, connection.ErrorCode!);
return 1;
}
var format = AuditCommandHelpers.ResolveFormat(result, formatOption);
var formatter = AuditFormatterFactory.Create(format, Console.Error);
var args = new AuditQueryArgs
{
Since = result.GetValue(sinceOption),
Until = result.GetValue(untilOption),
Channel = result.GetValue(channelOption),
Kind = result.GetValue(kindOption),
Status = result.GetValue(statusOption),
Site = result.GetValue(siteOption),
Instance = result.GetValue(instanceOption),
Target = result.GetValue(targetOption),
Actor = result.GetValue(actorOption),
CorrelationId = result.GetValue(correlationIdOption),
ErrorsOnly = result.GetValue(errorsOnlyOption),
PageSize = result.GetValue(pageSizeOption),
};
var fetchAll = result.GetValue(allOption);
try
{
using var client = new ManagementHttpClient(connection.Url!, connection.Username!, connection.Password!);
return await AuditQueryHelpers.RunQueryAsync(
client, args, fetchAll, formatter, Console.Out, DateTimeOffset.UtcNow);
}
catch (FormatException ex)
{
OutputFormatter.WriteError(ex.Message, "INVALID_ARGUMENT");
return 1;
}
});
return cmd;
}
private static Command BuildExport(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var sinceOption = new Option<string>("--since") { Description = "Start time: relative (1h, 24h, 7d) or ISO-8601", Required = true };
var untilOption = new Option<string>("--until") { Description = "End time: relative (1h, 24h, 7d) or ISO-8601", Required = true };
var formatExportOption = new Option<string>("--format") { Description = "Export format", Required = true };
formatExportOption.AcceptOnlyFromAmong("csv", "jsonl", "parquet");
var outputOption = new Option<string>("--output") { Description = "Destination file path", Required = true };
var channelOption = new Option<string?>("--channel") { Description = "Filter by channel" };
var kindOption = new Option<string?>("--kind") { Description = "Filter by event kind" };
var statusOption = new Option<string?>("--status") { Description = "Filter by status" };
var siteOption = new Option<string?>("--site") { Description = "Filter by source site ID" };
var targetOption = new Option<string?>("--target") { Description = "Filter by target" };
var actorOption = new Option<string?>("--actor") { Description = "Filter by actor" };
var cmd = new Command("export") { Description = "Export audit log events to a file" };
cmd.SetAction((ParseResult result) => 0);
cmd.Add(sinceOption);
cmd.Add(untilOption);
cmd.Add(formatExportOption);
cmd.Add(outputOption);
cmd.Add(channelOption);
cmd.Add(kindOption);
cmd.Add(statusOption);
cmd.Add(siteOption);
cmd.Add(targetOption);
cmd.Add(actorOption);
cmd.SetAction(async (ParseResult result) =>
{
var connection = AuditCommandHelpers.ResolveConnection(result, urlOption, usernameOption, passwordOption);
if (connection.Error != null)
{
OutputFormatter.WriteError(connection.Error, connection.ErrorCode!);
return 1;
}
var args = new AuditExportArgs
{
Since = result.GetValue(sinceOption)!,
Until = result.GetValue(untilOption)!,
Format = result.GetValue(formatExportOption)!,
Output = result.GetValue(outputOption)!,
Channel = result.GetValue(channelOption),
Kind = result.GetValue(kindOption),
Status = result.GetValue(statusOption),
Site = result.GetValue(siteOption),
Target = result.GetValue(targetOption),
Actor = result.GetValue(actorOption),
};
try
{
using var client = new ManagementHttpClient(connection.Url!, connection.Username!, connection.Password!);
return await AuditExportHelpers.RunExportAsync(client, args, Console.Out, DateTimeOffset.UtcNow);
}
catch (FormatException ex)
{
OutputFormatter.WriteError(ex.Message, "INVALID_ARGUMENT");
return 1;
}
});
return cmd;
}
private static Command BuildVerifyChain(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var monthOption = new Option<string>("--month") { Description = "Month to verify (YYYY-MM)", Required = true };
var cmd = new Command("verify-chain") { Description = "Verify the audit log hash chain for a month" };
cmd.SetAction((ParseResult result) => 0);
cmd.Add(monthOption);
cmd.SetAction((ParseResult result) =>
{
var month = result.GetValue(monthOption)!;
if (!AuditVerifyChainHelpers.IsValidMonth(month))
{
OutputFormatter.WriteError(
$"Invalid month '{month}'. Expected YYYY-MM (e.g. 2026-05).", "INVALID_ARGUMENT");
return 1;
}
Console.WriteLine(
"Hash-chain tamper-evidence is not enabled in this release. "
+ "See Component-AuditLog.md (Security & Tamper-Evidence) for the v1.x roadmap.");
return 0;
});
return cmd;
}
}