Refactor code structure for improved readability and maintainability; optimize performance in key functions.

This commit is contained in:
master
2025-12-22 19:06:31 +02:00
parent dfaa2079aa
commit 4602ccc3a3
1444 changed files with 109919 additions and 8058 deletions

View File

@@ -0,0 +1,143 @@
namespace StellaOps.AuditPack.Models;
using System.Collections.Immutable;
/// <summary>
/// A sealed, self-contained audit pack for verification and compliance.
/// Contains all inputs and outputs required to reproduce and verify a scan.
/// </summary>
public sealed record AuditPack
{
/// <summary>
/// Unique identifier for this audit pack.
/// </summary>
public required string PackId { get; init; }
/// <summary>
/// Schema version for forward compatibility.
/// </summary>
public string SchemaVersion { get; init; } = "1.0.0";
/// <summary>
/// Human-readable name for this pack.
/// </summary>
public required string Name { get; init; }
/// <summary>
/// UTC timestamp when pack was created.
/// </summary>
public required DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// Run manifest for replay.
/// </summary>
public required RunManifest RunManifest { get; init; }
/// <summary>
/// Evidence index linking verdict to all evidence.
/// </summary>
public required EvidenceIndex EvidenceIndex { get; init; }
/// <summary>
/// The verdict from the scan.
/// </summary>
public required Verdict Verdict { get; init; }
/// <summary>
/// Offline bundle manifest (contents stored separately).
/// </summary>
public required BundleManifest OfflineBundle { get; init; }
/// <summary>
/// All attestations in the evidence chain.
/// </summary>
public ImmutableArray<Attestation> Attestations { get; init; } = [];
/// <summary>
/// SBOM documents (CycloneDX and SPDX).
/// </summary>
public ImmutableArray<SbomDocument> Sboms { get; init; } = [];
/// <summary>
/// VEX documents applied.
/// </summary>
public ImmutableArray<VexDocument> VexDocuments { get; init; } = [];
/// <summary>
/// Trust roots for signature verification.
/// </summary>
public ImmutableArray<TrustRoot> TrustRoots { get; init; } = [];
/// <summary>
/// Pack contents inventory with paths and digests.
/// </summary>
public required PackContents Contents { get; init; }
/// <summary>
/// SHA-256 digest of this pack manifest (excluding signature).
/// </summary>
public string? PackDigest { get; init; }
/// <summary>
/// DSSE signature over the pack.
/// </summary>
public string? Signature { get; init; }
}
public sealed record PackContents
{
public ImmutableArray<PackFile> Files { get; init; } = [];
public long TotalSizeBytes { get; init; }
public int FileCount { get; init; }
}
public sealed record PackFile(
string RelativePath,
string Digest,
long SizeBytes,
PackFileType Type);
public enum PackFileType
{
Manifest,
RunManifest,
EvidenceIndex,
Verdict,
Sbom,
Vex,
Attestation,
Feed,
Policy,
TrustRoot,
Other
}
public sealed record SbomDocument(
string Id,
string Format,
string Content,
string Digest);
public sealed record VexDocument(
string Id,
string Format,
string Content,
string Digest);
public sealed record TrustRoot(
string Id,
string Type, // fulcio, rekor, custom
string Content,
string Digest);
public sealed record Attestation(
string Id,
string Type,
string Envelope, // DSSE envelope
string Digest);
// Placeholder types - these would reference actual domain models
public sealed record RunManifest(string ScanId, DateTimeOffset Timestamp);
public sealed record EvidenceIndex(ImmutableArray<string> EvidenceIds);
public sealed record Verdict(string VerdictId, string Status);
public sealed record BundleManifest(string BundleId, string Version);

View 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);

View File

@@ -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);

View File

@@ -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; }
}

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>