using System.Security.Cryptography; using System.Text; using System.Text.Json; namespace AVEVA.Historian.ReverseEngineering.Capture; /// One captured operation: the (optional) request buffer and the response buffer, raw. public sealed record ProtocolCapture(string Op, byte[]? Request, byte[]? Response, string? Notes = null); /// /// CW-1 fixture writer: takes a live , redacts it with /// , and writes a committable JSON fixture under /// fixtures/protocol/<op>/. The fixture records sanitized hex, lengths, SHA-256 of the /// sanitized bytes, and the scrub report — never the original identity-bearing bytes. /// /// Timestamps are passed in (never generated here) so the writer stays deterministic and testable. /// public static class ProtocolFixtureWriter { public static string BuildFixtureJson( ProtocolCapture capture, IReadOnlyList secrets, string capturedUtcIso) { ArgumentNullException.ThrowIfNull(capture); BufferSection? request = BuildSection(capture.Request, secrets); BufferSection? response = BuildSection(capture.Response, secrets); var document = new { op = capture.Op, capturedUtc = capturedUtcIso, notes = capture.Notes, request, response, }; return JsonSerializer.Serialize(document, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }); } /// Serializes the fixture and writes it to /<op>/<name>.json. Returns the path. public static string Write( string fixtureRoot, string name, ProtocolCapture capture, IReadOnlyList secrets, string capturedUtcIso) { ArgumentException.ThrowIfNullOrWhiteSpace(fixtureRoot); ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentNullException.ThrowIfNull(capture); string json = BuildFixtureJson(capture, secrets, capturedUtcIso); string directory = Path.Combine(fixtureRoot, capture.Op); Directory.CreateDirectory(directory); string path = Path.Combine(directory, name + ".json"); File.WriteAllText(path, json, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); return path; } private static BufferSection? BuildSection(byte[]? raw, IReadOnlyList secrets) { if (raw is null) { return null; } SanitizeResult result = ProtocolCaptureSanitizer.Sanitize(raw, secrets); ProtocolCaptureSanitizer.AssertNoSecretsRemain(result.Sanitized, secrets); return new BufferSection( Length: raw.Length, Sha256: Convert.ToHexString(SHA256.HashData(result.Sanitized)).ToLowerInvariant(), Hex: Convert.ToHexString(result.Sanitized).ToLowerInvariant(), Redactions: result.Report .Where(r => r.Total > 0) .Select(r => new RedactionEntry(r.Name, r.AsciiMatches, r.Utf16Matches)) .ToArray()); } private sealed record BufferSection(int Length, string Sha256, string Hex, IReadOnlyList Redactions); private sealed record RedactionEntry(string Secret, int AsciiMatches, int Utf16Matches); }