using System.Globalization; using System.Text.Json; namespace ZB.MOM.WW.ScadaBridge.CLI.Commands; /// /// Helpers for the audit verify-chain subcommand (ScadaBridge#41). The command sends /// the VerifyAuditChain management command for one UTC calendar month and reports /// whether that month's AuditLog hash chain is intact, or the first break (chain partition, /// ChainSeq, EventId, kind). /// /// /// Exit codes: 0 intact (or nothing chained yet), when /// the chain is broken, 1 on a request error, 2 on an authorization failure. /// public static class AuditVerifyChainHelpers { /// Process exit code when verification found a break (tamper evidence). public const int BrokenExitCode = 3; /// /// Client deadline. The server bounds the call with the long-running management Ask /// (5 min); the client waits a little longer so the server's answer always arrives. /// public static readonly TimeSpan RequestTimeout = TimeSpan.FromMinutes(6); /// /// Returns true if is a well-formed YYYY-MM value /// with a real month (01-12). A malformed month (e.g. 2026-13) is rejected. /// /// The month string to validate in YYYY-MM format. /// true if the string is a well-formed YYYY-MM value with a real month; otherwise false. public static bool IsValidMonth(string? month) => !string.IsNullOrWhiteSpace(month) && DateTime.TryParseExact(month, "yyyy-MM", CultureInfo.InvariantCulture, DateTimeStyles.None, out _); /// /// Renders a VerifyAuditChain result and maps it to the process exit code. /// json prints the server body verbatim; table prints a short report. /// /// The success body (an AuditChainVerificationResult, camelCase). /// Resolved output format. /// Writer for the report. /// 0 when intact / not chained, when broken. public static int Render(string json, string format, TextWriter output) { using var doc = JsonDocument.Parse(json); var root = doc.RootElement; var status = root.TryGetProperty("status", out var s) ? s.GetString() ?? "" : ""; var broken = string.Equals(status, "Broken", StringComparison.OrdinalIgnoreCase); if (!string.Equals(format, "table", StringComparison.OrdinalIgnoreCase)) { output.WriteLine(json); output.Flush(); return broken ? BrokenExitCode : 0; } output.WriteLine($"Partition : {Str(root, "partition")}"); output.WriteLine($"Status : {status}"); output.WriteLine($"Rows verified : {Num(root, "rowsVerified")}"); output.WriteLine($"Tombstones applied: {Num(root, "tombstonesApplied")}"); output.WriteLine($"Purged through : {Num(root, "purgedThroughSeq", "-")}"); output.WriteLine($"Head sequence : {Num(root, "headSeq", "-")}"); output.WriteLine($"Unchained rows : {Num(root, "unchainedRows")}"); if (root.TryGetProperty("firstBreak", out var brk) && brk.ValueKind == JsonValueKind.Object) { output.WriteLine("First break:"); output.WriteLine($" partition : {Str(brk, "partition")}"); output.WriteLine($" chainSeq : {Num(brk, "chainSeq")}"); output.WriteLine($" eventId : {Str(brk, "eventId", "-")}"); output.WriteLine($" kind : {Str(brk, "kind")}"); output.WriteLine($" detail : {Str(brk, "detail")}"); } output.Flush(); return broken ? BrokenExitCode : 0; } private static string Str(JsonElement e, string name, string missing = "") => e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() ?? missing : missing; private static string Num(JsonElement e, string name, string missing = "0") => e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number ? v.GetInt64().ToString(CultureInfo.InvariantCulture) : missing; }