Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
247
src/__Libraries/StellaOps.AuditPack/Services/AuditPackBuilder.cs
Normal file
247
src/__Libraries/StellaOps.AuditPack/Services/AuditPackBuilder.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
namespace StellaOps.AuditPack.Services;
|
||||
|
||||
using StellaOps.AuditPack.Models;
|
||||
using System.Collections.Immutable;
|
||||
using System.Formats.Tar;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Builds audit packs from scan results.
|
||||
/// </summary>
|
||||
public sealed class AuditPackBuilder : IAuditPackBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds an audit pack from a scan result.
|
||||
/// </summary>
|
||||
public async Task<AuditPack> BuildAsync(
|
||||
ScanResult scanResult,
|
||||
AuditPackOptions options,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var files = new List<PackFile>();
|
||||
|
||||
// Collect all evidence
|
||||
var attestations = await CollectAttestationsAsync(scanResult, ct);
|
||||
var sboms = CollectSboms(scanResult);
|
||||
var vexDocuments = CollectVexDocuments(scanResult);
|
||||
var trustRoots = await CollectTrustRootsAsync(options, ct);
|
||||
|
||||
// Build offline bundle subset (only required feeds/policies)
|
||||
var bundleManifest = await BuildMinimalBundleAsync(scanResult, ct);
|
||||
|
||||
// Create pack structure
|
||||
var pack = new AuditPack
|
||||
{
|
||||
PackId = Guid.NewGuid().ToString(),
|
||||
SchemaVersion = "1.0.0",
|
||||
Name = options.Name ?? $"audit-pack-{scanResult.ScanId}",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
RunManifest = new RunManifest(scanResult.ScanId, DateTimeOffset.UtcNow),
|
||||
EvidenceIndex = new EvidenceIndex([]),
|
||||
Verdict = new Verdict(scanResult.ScanId, "completed"),
|
||||
OfflineBundle = bundleManifest,
|
||||
Attestations = [.. attestations],
|
||||
Sboms = [.. sboms],
|
||||
VexDocuments = [.. vexDocuments],
|
||||
TrustRoots = [.. trustRoots],
|
||||
Contents = new PackContents
|
||||
{
|
||||
Files = [.. files],
|
||||
TotalSizeBytes = files.Sum(f => f.SizeBytes),
|
||||
FileCount = files.Count
|
||||
}
|
||||
};
|
||||
|
||||
return WithDigest(pack);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports audit pack to archive file.
|
||||
/// </summary>
|
||||
public async Task ExportAsync(
|
||||
AuditPack pack,
|
||||
string outputPath,
|
||||
ExportOptions options,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"audit-pack-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
// Write pack manifest
|
||||
var manifestJson = JsonSerializer.Serialize(pack, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
await File.WriteAllTextAsync(Path.Combine(tempDir, "manifest.json"), manifestJson, ct);
|
||||
|
||||
// Write run manifest
|
||||
var runManifestJson = JsonSerializer.Serialize(pack.RunManifest);
|
||||
await File.WriteAllTextAsync(Path.Combine(tempDir, "run-manifest.json"), runManifestJson, ct);
|
||||
|
||||
// Write evidence index
|
||||
var evidenceJson = JsonSerializer.Serialize(pack.EvidenceIndex);
|
||||
await File.WriteAllTextAsync(Path.Combine(tempDir, "evidence-index.json"), evidenceJson, ct);
|
||||
|
||||
// Write verdict
|
||||
var verdictJson = JsonSerializer.Serialize(pack.Verdict);
|
||||
await File.WriteAllTextAsync(Path.Combine(tempDir, "verdict.json"), verdictJson, ct);
|
||||
|
||||
// Write SBOMs
|
||||
var sbomsDir = Path.Combine(tempDir, "sboms");
|
||||
Directory.CreateDirectory(sbomsDir);
|
||||
foreach (var sbom in pack.Sboms)
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(sbomsDir, $"{sbom.Id}.json"),
|
||||
sbom.Content,
|
||||
ct);
|
||||
}
|
||||
|
||||
// Write attestations
|
||||
var attestationsDir = Path.Combine(tempDir, "attestations");
|
||||
Directory.CreateDirectory(attestationsDir);
|
||||
foreach (var att in pack.Attestations)
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(attestationsDir, $"{att.Id}.json"),
|
||||
att.Envelope,
|
||||
ct);
|
||||
}
|
||||
|
||||
// Write VEX documents
|
||||
if (pack.VexDocuments.Length > 0)
|
||||
{
|
||||
var vexDir = Path.Combine(tempDir, "vex");
|
||||
Directory.CreateDirectory(vexDir);
|
||||
foreach (var vex in pack.VexDocuments)
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(vexDir, $"{vex.Id}.json"),
|
||||
vex.Content,
|
||||
ct);
|
||||
}
|
||||
}
|
||||
|
||||
// Write trust roots
|
||||
var certsDir = Path.Combine(tempDir, "trust-roots");
|
||||
Directory.CreateDirectory(certsDir);
|
||||
foreach (var root in pack.TrustRoots)
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(certsDir, $"{root.Id}.pem"),
|
||||
root.Content,
|
||||
ct);
|
||||
}
|
||||
|
||||
// Create tar.gz archive
|
||||
await CreateTarGzAsync(tempDir, outputPath, ct);
|
||||
|
||||
// Sign if requested
|
||||
if (options.Sign && !string.IsNullOrEmpty(options.SigningKey))
|
||||
{
|
||||
await SignPackAsync(outputPath, options.SigningKey, ct);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(tempDir))
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static AuditPack WithDigest(AuditPack pack)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(pack with { PackDigest = null, Signature = null });
|
||||
var digest = ComputeDigest(json);
|
||||
return pack with { PackDigest = digest };
|
||||
}
|
||||
|
||||
private static string ComputeDigest(string content)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static async Task CreateTarGzAsync(string sourceDir, string outputPath, CancellationToken ct)
|
||||
{
|
||||
var tarPath = outputPath.Replace(".tar.gz", ".tar");
|
||||
|
||||
// Create tar
|
||||
await TarFile.CreateFromDirectoryAsync(sourceDir, tarPath, includeBaseDirectory: false, ct);
|
||||
|
||||
// Compress to tar.gz
|
||||
using var tarStream = File.OpenRead(tarPath);
|
||||
using var gzStream = File.Create(outputPath);
|
||||
using var gzip = new GZipStream(gzStream, CompressionLevel.Optimal);
|
||||
await tarStream.CopyToAsync(gzip, ct);
|
||||
|
||||
// Clean up uncompressed tar
|
||||
File.Delete(tarPath);
|
||||
}
|
||||
|
||||
private static Task<ImmutableArray<Attestation>> CollectAttestationsAsync(ScanResult scanResult, CancellationToken ct)
|
||||
{
|
||||
// TODO: Collect attestations from storage
|
||||
return Task.FromResult(ImmutableArray<Attestation>.Empty);
|
||||
}
|
||||
|
||||
private static ImmutableArray<SbomDocument> CollectSboms(ScanResult scanResult)
|
||||
{
|
||||
// TODO: Collect SBOMs
|
||||
return [];
|
||||
}
|
||||
|
||||
private static ImmutableArray<VexDocument> CollectVexDocuments(ScanResult scanResult)
|
||||
{
|
||||
// TODO: Collect VEX documents
|
||||
return [];
|
||||
}
|
||||
|
||||
private static Task<ImmutableArray<TrustRoot>> CollectTrustRootsAsync(AuditPackOptions options, CancellationToken ct)
|
||||
{
|
||||
// TODO: Load trust roots from configuration
|
||||
return Task.FromResult(ImmutableArray<TrustRoot>.Empty);
|
||||
}
|
||||
|
||||
private static Task<BundleManifest> BuildMinimalBundleAsync(ScanResult scanResult, CancellationToken ct)
|
||||
{
|
||||
// TODO: Build minimal offline bundle
|
||||
return Task.FromResult(new BundleManifest("bundle-1", "1.0.0"));
|
||||
}
|
||||
|
||||
private static Task SignPackAsync(string packPath, string signingKey, CancellationToken ct)
|
||||
{
|
||||
// TODO: Sign pack with key
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAuditPackBuilder
|
||||
{
|
||||
Task<AuditPack> BuildAsync(ScanResult scanResult, AuditPackOptions options, CancellationToken ct = default);
|
||||
Task ExportAsync(AuditPack pack, string outputPath, ExportOptions options, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed record AuditPackOptions
|
||||
{
|
||||
public string? Name { get; init; }
|
||||
public bool IncludeFeeds { get; init; } = true;
|
||||
public bool IncludePolicies { get; init; } = true;
|
||||
public bool MinimizeSize { get; init; } = false;
|
||||
}
|
||||
|
||||
public sealed record ExportOptions
|
||||
{
|
||||
public bool Sign { get; init; } = true;
|
||||
public string? SigningKey { get; init; }
|
||||
public bool Compress { get; init; } = true;
|
||||
}
|
||||
|
||||
// Placeholder for scan result
|
||||
public sealed record ScanResult(string ScanId);
|
||||
@@ -0,0 +1,205 @@
|
||||
namespace StellaOps.AuditPack.Services;
|
||||
|
||||
using StellaOps.AuditPack.Models;
|
||||
using System.Formats.Tar;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Imports and validates audit packs.
|
||||
/// </summary>
|
||||
public sealed class AuditPackImporter : IAuditPackImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Imports an audit pack from archive.
|
||||
/// </summary>
|
||||
public async Task<ImportResult> ImportAsync(
|
||||
string archivePath,
|
||||
ImportOptions options,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var extractDir = options.ExtractDirectory ??
|
||||
Path.Combine(Path.GetTempPath(), $"audit-pack-{Guid.NewGuid():N}");
|
||||
|
||||
try
|
||||
{
|
||||
// Extract archive
|
||||
await ExtractTarGzAsync(archivePath, extractDir, ct);
|
||||
|
||||
// Load manifest
|
||||
var manifestPath = Path.Combine(extractDir, "manifest.json");
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
return ImportResult.Failed("Manifest file not found");
|
||||
}
|
||||
|
||||
var manifestJson = await File.ReadAllTextAsync(manifestPath, ct);
|
||||
var pack = JsonSerializer.Deserialize<AuditPack>(manifestJson);
|
||||
|
||||
if (pack == null)
|
||||
{
|
||||
return ImportResult.Failed("Failed to deserialize manifest");
|
||||
}
|
||||
|
||||
// Verify integrity
|
||||
var integrityResult = await VerifyIntegrityAsync(pack, extractDir, ct);
|
||||
if (!integrityResult.IsValid)
|
||||
{
|
||||
return ImportResult.Failed("Integrity verification failed", integrityResult.Errors);
|
||||
}
|
||||
|
||||
// Verify signatures if present
|
||||
SignatureResult? signatureResult = null;
|
||||
if (options.VerifySignatures)
|
||||
{
|
||||
signatureResult = await VerifySignaturesAsync(pack, extractDir, ct);
|
||||
if (!signatureResult.IsValid)
|
||||
{
|
||||
return ImportResult.Failed("Signature verification failed", signatureResult.Errors);
|
||||
}
|
||||
}
|
||||
|
||||
return new ImportResult
|
||||
{
|
||||
Success = true,
|
||||
Pack = pack,
|
||||
ExtractDirectory = extractDir,
|
||||
IntegrityResult = integrityResult,
|
||||
SignatureResult = signatureResult
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ImportResult.Failed($"Import failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ExtractTarGzAsync(string archivePath, string extractDir, CancellationToken ct)
|
||||
{
|
||||
Directory.CreateDirectory(extractDir);
|
||||
|
||||
var tarPath = archivePath.Replace(".tar.gz", ".tar");
|
||||
|
||||
// Decompress gz
|
||||
using (var gzStream = File.OpenRead(archivePath))
|
||||
using (var gzip = new GZipStream(gzStream, CompressionMode.Decompress))
|
||||
using (var tarStream = File.Create(tarPath))
|
||||
{
|
||||
await gzip.CopyToAsync(tarStream, ct);
|
||||
}
|
||||
|
||||
// Extract tar
|
||||
await TarFile.ExtractToDirectoryAsync(tarPath, extractDir, overwriteFiles: true, ct);
|
||||
|
||||
// Clean up tar
|
||||
File.Delete(tarPath);
|
||||
}
|
||||
|
||||
private static async Task<IntegrityResult> VerifyIntegrityAsync(
|
||||
AuditPack pack,
|
||||
string extractDir,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
// Verify each file digest
|
||||
foreach (var file in pack.Contents.Files)
|
||||
{
|
||||
var filePath = Path.Combine(extractDir, file.RelativePath);
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
errors.Add($"Missing file: {file.RelativePath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var content = await File.ReadAllBytesAsync(filePath, ct);
|
||||
var actualDigest = Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant();
|
||||
|
||||
if (actualDigest != file.Digest.ToLowerInvariant())
|
||||
{
|
||||
errors.Add($"Digest mismatch for {file.RelativePath}: expected {file.Digest}, got {actualDigest}");
|
||||
}
|
||||
}
|
||||
|
||||
// Verify pack digest
|
||||
if (!string.IsNullOrEmpty(pack.PackDigest))
|
||||
{
|
||||
var computed = ComputePackDigest(pack);
|
||||
if (computed != pack.PackDigest)
|
||||
{
|
||||
errors.Add($"Pack digest mismatch: expected {pack.PackDigest}, got {computed}");
|
||||
}
|
||||
}
|
||||
|
||||
return new IntegrityResult(errors.Count == 0, errors);
|
||||
}
|
||||
|
||||
private static async Task<SignatureResult> VerifySignaturesAsync(
|
||||
AuditPack pack,
|
||||
string extractDir,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
// Load signature
|
||||
var signaturePath = Path.Combine(extractDir, "signature.sig");
|
||||
if (!File.Exists(signaturePath))
|
||||
{
|
||||
return new SignatureResult(true, [], "No signature present");
|
||||
}
|
||||
|
||||
var signature = await File.ReadAllTextAsync(signaturePath, ct);
|
||||
|
||||
// Verify against trust roots
|
||||
foreach (var root in pack.TrustRoots)
|
||||
{
|
||||
// TODO: Implement actual signature verification
|
||||
// For now, just check that trust root exists
|
||||
if (!string.IsNullOrEmpty(root.Content))
|
||||
{
|
||||
return new SignatureResult(true, [], $"Verified with {root.Id}");
|
||||
}
|
||||
}
|
||||
|
||||
errors.Add("Signature does not verify against any trust root");
|
||||
return new SignatureResult(false, errors);
|
||||
}
|
||||
|
||||
private static string ComputePackDigest(AuditPack pack)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(pack with { PackDigest = null, Signature = null });
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(json);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAuditPackImporter
|
||||
{
|
||||
Task<ImportResult> ImportAsync(string archivePath, ImportOptions options, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed record ImportOptions
|
||||
{
|
||||
public string? ExtractDirectory { get; init; }
|
||||
public bool VerifySignatures { get; init; } = true;
|
||||
public bool KeepExtracted { get; init; } = false;
|
||||
}
|
||||
|
||||
public sealed record ImportResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public AuditPack? Pack { get; init; }
|
||||
public string? ExtractDirectory { get; init; }
|
||||
public IntegrityResult? IntegrityResult { get; init; }
|
||||
public SignatureResult? SignatureResult { get; init; }
|
||||
public IReadOnlyList<string>? Errors { get; init; }
|
||||
|
||||
public static ImportResult Failed(string message, IReadOnlyList<string>? errors = null) =>
|
||||
new() { Success = false, Errors = errors != null ? [message, .. errors] : [message] };
|
||||
}
|
||||
|
||||
public sealed record IntegrityResult(bool IsValid, IReadOnlyList<string> Errors);
|
||||
|
||||
public sealed record SignatureResult(bool IsValid, IReadOnlyList<string> Errors, string? Message = null);
|
||||
@@ -0,0 +1,125 @@
|
||||
namespace StellaOps.AuditPack.Services;
|
||||
|
||||
using StellaOps.AuditPack.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Replays scans from imported audit packs and compares results.
|
||||
/// </summary>
|
||||
public sealed class AuditPackReplayer : IAuditPackReplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Replays a scan from an imported audit pack.
|
||||
/// </summary>
|
||||
public async Task<ReplayComparisonResult> ReplayAsync(
|
||||
ImportResult importResult,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!importResult.Success || importResult.Pack == null)
|
||||
{
|
||||
return ReplayComparisonResult.Failed("Invalid import result");
|
||||
}
|
||||
|
||||
var pack = importResult.Pack;
|
||||
|
||||
// Load offline bundle from pack
|
||||
var bundlePath = Path.Combine(importResult.ExtractDirectory!, "bundle");
|
||||
// TODO: Load bundle using bundle loader
|
||||
// await _bundleLoader.LoadAsync(bundlePath, ct);
|
||||
|
||||
// Execute replay
|
||||
var replayResult = await ExecuteReplayAsync(pack.RunManifest, ct);
|
||||
|
||||
if (!replayResult.Success)
|
||||
{
|
||||
return ReplayComparisonResult.Failed(
|
||||
$"Replay failed: {string.Join(", ", replayResult.Errors ?? [])}");
|
||||
}
|
||||
|
||||
// Compare verdicts
|
||||
var comparison = CompareVerdicts(pack.Verdict, replayResult.Verdict);
|
||||
|
||||
return new ReplayComparisonResult
|
||||
{
|
||||
Success = true,
|
||||
IsIdentical = comparison.IsIdentical,
|
||||
OriginalVerdictDigest = pack.Verdict.VerdictId,
|
||||
ReplayedVerdictDigest = replayResult.VerdictDigest,
|
||||
Differences = comparison.Differences,
|
||||
ReplayDurationMs = replayResult.DurationMs
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<ReplayResult> ExecuteReplayAsync(
|
||||
RunManifest runManifest,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// TODO: Implement actual replay execution
|
||||
// This would call the scanner with frozen time and offline bundle
|
||||
await Task.CompletedTask;
|
||||
|
||||
return new ReplayResult
|
||||
{
|
||||
Success = true,
|
||||
Verdict = new Verdict("replayed-verdict", "completed"),
|
||||
VerdictDigest = "placeholder-digest",
|
||||
DurationMs = 1000
|
||||
};
|
||||
}
|
||||
|
||||
private static VerdictComparison CompareVerdicts(Verdict original, Verdict? replayed)
|
||||
{
|
||||
if (replayed == null)
|
||||
return new VerdictComparison(false, ["Replayed verdict is null"]);
|
||||
|
||||
var originalJson = JsonSerializer.Serialize(original);
|
||||
var replayedJson = JsonSerializer.Serialize(replayed);
|
||||
|
||||
if (originalJson == replayedJson)
|
||||
return new VerdictComparison(true, []);
|
||||
|
||||
// Find differences
|
||||
var differences = FindJsonDifferences(originalJson, replayedJson);
|
||||
return new VerdictComparison(false, differences);
|
||||
}
|
||||
|
||||
private static List<string> FindJsonDifferences(string json1, string json2)
|
||||
{
|
||||
// TODO: Implement proper JSON diff
|
||||
// For now, just report that they differ
|
||||
if (json1 == json2)
|
||||
return [];
|
||||
|
||||
return ["Verdicts differ"];
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAuditPackReplayer
|
||||
{
|
||||
Task<ReplayComparisonResult> ReplayAsync(ImportResult importResult, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed record ReplayComparisonResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public bool IsIdentical { get; init; }
|
||||
public string? OriginalVerdictDigest { get; init; }
|
||||
public string? ReplayedVerdictDigest { get; init; }
|
||||
public IReadOnlyList<string> Differences { get; init; } = [];
|
||||
public long ReplayDurationMs { get; init; }
|
||||
public string? Error { get; init; }
|
||||
|
||||
public static ReplayComparisonResult Failed(string error) =>
|
||||
new() { Success = false, Error = error };
|
||||
}
|
||||
|
||||
public sealed record VerdictComparison(bool IsIdentical, IReadOnlyList<string> Differences);
|
||||
|
||||
public sealed record ReplayResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public Verdict? Verdict { get; init; }
|
||||
public string? VerdictDigest { get; init; }
|
||||
public long DurationMs { get; init; }
|
||||
public IReadOnlyList<string>? Errors { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user