save progress

This commit is contained in:
StellaOps Bot
2026-01-02 21:06:27 +02:00
parent f46bde5575
commit 3f197814c5
441 changed files with 21545 additions and 4306 deletions

View File

@@ -5,9 +5,6 @@
// Description: Writes self-contained audit bundles for offline replay.
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Formats.Tar;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -20,12 +17,21 @@ namespace StellaOps.AuditPack.Services;
/// </summary>
public sealed class AuditBundleWriter : IAuditBundleWriter
{
private readonly TimeProvider _timeProvider;
private readonly IAuditPackIdGenerator _idGenerator;
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public AuditBundleWriter(TimeProvider? timeProvider = null, IAuditPackIdGenerator? idGenerator = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
_idGenerator = idGenerator ?? new GuidAuditPackIdGenerator();
}
/// <summary>
/// Creates an audit bundle from the specified inputs.
/// </summary>
@@ -36,20 +42,16 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
ArgumentNullException.ThrowIfNull(request);
ArgumentException.ThrowIfNullOrWhiteSpace(request.OutputPath);
var tempDir = Path.Combine(Path.GetTempPath(), $"audit-bundle-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
try
{
var entries = new List<BundleEntry>();
var files = new List<BundleFileEntry>();
var archiveEntries = new List<ArchiveEntry>();
// Write SBOM
string sbomDigest;
if (request.Sbom is not null)
{
var sbomPath = Path.Combine(tempDir, "sbom.json");
await File.WriteAllBytesAsync(sbomPath, request.Sbom, cancellationToken);
sbomDigest = ComputeSha256(request.Sbom);
entries.Add(new BundleEntry("sbom.json", sbomDigest, request.Sbom.Length));
files.Add(new BundleFileEntry
@@ -59,6 +61,7 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
SizeBytes = request.Sbom.Length,
ContentType = BundleContentType.Sbom
});
archiveEntries.Add(new ArchiveEntry("sbom.json", request.Sbom));
}
else
{
@@ -69,10 +72,6 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
string feedsDigest;
if (request.FeedsSnapshot is not null)
{
var feedsDir = Path.Combine(tempDir, "feeds");
Directory.CreateDirectory(feedsDir);
var feedsPath = Path.Combine(feedsDir, "feeds-snapshot.ndjson");
await File.WriteAllBytesAsync(feedsPath, request.FeedsSnapshot, cancellationToken);
feedsDigest = ComputeSha256(request.FeedsSnapshot);
entries.Add(new BundleEntry("feeds/feeds-snapshot.ndjson", feedsDigest, request.FeedsSnapshot.Length));
files.Add(new BundleFileEntry
@@ -82,6 +81,7 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
SizeBytes = request.FeedsSnapshot.Length,
ContentType = BundleContentType.Feeds
});
archiveEntries.Add(new ArchiveEntry("feeds/feeds-snapshot.ndjson", request.FeedsSnapshot));
}
else
{
@@ -92,10 +92,6 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
string policyDigest;
if (request.PolicyBundle is not null)
{
var policyDir = Path.Combine(tempDir, "policy");
Directory.CreateDirectory(policyDir);
var policyPath = Path.Combine(policyDir, "policy-bundle.tar.gz");
await File.WriteAllBytesAsync(policyPath, request.PolicyBundle, cancellationToken);
policyDigest = ComputeSha256(request.PolicyBundle);
entries.Add(new BundleEntry("policy/policy-bundle.tar.gz", policyDigest, request.PolicyBundle.Length));
files.Add(new BundleFileEntry
@@ -105,6 +101,7 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
SizeBytes = request.PolicyBundle.Length,
ContentType = BundleContentType.Policy
});
archiveEntries.Add(new ArchiveEntry("policy/policy-bundle.tar.gz", request.PolicyBundle));
}
else
{
@@ -115,10 +112,6 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
string? vexDigest = null;
if (request.VexStatements is not null)
{
var vexDir = Path.Combine(tempDir, "vex");
Directory.CreateDirectory(vexDir);
var vexPath = Path.Combine(vexDir, "vex-statements.json");
await File.WriteAllBytesAsync(vexPath, request.VexStatements, cancellationToken);
vexDigest = ComputeSha256(request.VexStatements);
entries.Add(new BundleEntry("vex/vex-statements.json", vexDigest, request.VexStatements.Length));
files.Add(new BundleFileEntry
@@ -128,14 +121,13 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
SizeBytes = request.VexStatements.Length,
ContentType = BundleContentType.Vex
});
archiveEntries.Add(new ArchiveEntry("vex/vex-statements.json", request.VexStatements));
}
// Write verdict
string verdictDigest;
if (request.Verdict is not null)
{
var verdictPath = Path.Combine(tempDir, "verdict.json");
await File.WriteAllBytesAsync(verdictPath, request.Verdict, cancellationToken);
verdictDigest = ComputeSha256(request.Verdict);
entries.Add(new BundleEntry("verdict.json", verdictDigest, request.Verdict.Length));
files.Add(new BundleFileEntry
@@ -145,6 +137,7 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
SizeBytes = request.Verdict.Length,
ContentType = BundleContentType.Verdict
});
archiveEntries.Add(new ArchiveEntry("verdict.json", request.Verdict));
}
else
{
@@ -154,10 +147,6 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
// Write proof bundle (optional)
if (request.ProofBundle is not null)
{
var proofDir = Path.Combine(tempDir, "proof");
Directory.CreateDirectory(proofDir);
var proofPath = Path.Combine(proofDir, "proof-bundle.json");
await File.WriteAllBytesAsync(proofPath, request.ProofBundle, cancellationToken);
var proofDigest = ComputeSha256(request.ProofBundle);
entries.Add(new BundleEntry("proof/proof-bundle.json", proofDigest, request.ProofBundle.Length));
files.Add(new BundleFileEntry
@@ -167,16 +156,13 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
SizeBytes = request.ProofBundle.Length,
ContentType = BundleContentType.ProofBundle
});
archiveEntries.Add(new ArchiveEntry("proof/proof-bundle.json", request.ProofBundle));
}
// Write trust roots (optional)
string? trustRootsDigest = null;
if (request.TrustRoots is not null)
{
var trustDir = Path.Combine(tempDir, "trust");
Directory.CreateDirectory(trustDir);
var trustPath = Path.Combine(trustDir, "trust-roots.json");
await File.WriteAllBytesAsync(trustPath, request.TrustRoots, cancellationToken);
trustRootsDigest = ComputeSha256(request.TrustRoots);
entries.Add(new BundleEntry("trust/trust-roots.json", trustRootsDigest, request.TrustRoots.Length));
files.Add(new BundleFileEntry
@@ -186,14 +172,13 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
SizeBytes = request.TrustRoots.Length,
ContentType = BundleContentType.TrustRoot
});
archiveEntries.Add(new ArchiveEntry("trust/trust-roots.json", request.TrustRoots));
}
// Write scoring rules (optional)
string? scoringDigest = null;
if (request.ScoringRules is not null)
{
var scoringPath = Path.Combine(tempDir, "scoring-rules.json");
await File.WriteAllBytesAsync(scoringPath, request.ScoringRules, cancellationToken);
scoringDigest = ComputeSha256(request.ScoringRules);
entries.Add(new BundleEntry("scoring-rules.json", scoringDigest, request.ScoringRules.Length));
files.Add(new BundleFileEntry
@@ -203,15 +188,14 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
SizeBytes = request.ScoringRules.Length,
ContentType = BundleContentType.Other
});
archiveEntries.Add(new ArchiveEntry("scoring-rules.json", request.ScoringRules));
}
// Write time anchor (optional)
TimeAnchor? timeAnchor = null;
if (request.TimeAnchor is not null)
{
var timeAnchorPath = Path.Combine(tempDir, "time-anchor.json");
var timeAnchorBytes = JsonSerializer.SerializeToUtf8Bytes(request.TimeAnchor, JsonOptions);
await File.WriteAllBytesAsync(timeAnchorPath, timeAnchorBytes, cancellationToken);
var timeAnchorBytes = CanonicalJson.Serialize(request.TimeAnchor, JsonOptions);
var timeAnchorDigest = ComputeSha256(timeAnchorBytes);
entries.Add(new BundleEntry("time-anchor.json", timeAnchorDigest, timeAnchorBytes.Length));
files.Add(new BundleFileEntry
@@ -221,6 +205,7 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
SizeBytes = timeAnchorBytes.Length,
ContentType = BundleContentType.TimeAnchor
});
archiveEntries.Add(new ArchiveEntry("time-anchor.json", timeAnchorBytes));
timeAnchor = new TimeAnchor
{
Timestamp = request.TimeAnchor.Timestamp,
@@ -235,9 +220,9 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
// Build manifest
var manifest = new AuditBundleManifest
{
BundleId = request.BundleId ?? Guid.NewGuid().ToString("N"),
BundleId = request.BundleId ?? _idGenerator.NewBundleId(),
Name = request.Name ?? $"audit-{request.ScanId}",
CreatedAt = DateTimeOffset.UtcNow,
CreatedAt = _timeProvider.GetUtcNow(),
ScanId = request.ScanId,
ImageRef = request.ImageRef,
ImageDigest = request.ImageDigest,
@@ -259,9 +244,8 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
};
// Write manifest
var manifestBytes = JsonSerializer.SerializeToUtf8Bytes(manifest, JsonOptions);
var manifestPath = Path.Combine(tempDir, "manifest.json");
await File.WriteAllBytesAsync(manifestPath, manifestBytes, cancellationToken);
var manifestBytes = CanonicalJson.Serialize(manifest, JsonOptions);
archiveEntries.Add(new ArchiveEntry("manifest.json", manifestBytes));
// Sign manifest if requested
string? signingKeyId = null;
@@ -282,8 +266,7 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
if (signResult.Success && signResult.Envelope is not null)
{
var signaturePath = Path.Combine(tempDir, "manifest.sig");
await File.WriteAllBytesAsync(signaturePath, signResult.Envelope, cancellationToken);
archiveEntries.Add(new ArchiveEntry("manifest.sig", signResult.Envelope));
signingKeyId = signResult.KeyId;
signingAlgorithm = signResult.Algorithm;
signed = true;
@@ -297,7 +280,7 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
outputPath = $"{outputPath}.tar.gz";
}
await CreateTarGzAsync(tempDir, outputPath, cancellationToken);
await ArchiveUtilities.WriteTarGzAsync(outputPath, archiveEntries, cancellationToken);
var bundleDigest = await ComputeFileDigestAsync(outputPath, cancellationToken);
@@ -320,21 +303,6 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
{
return AuditBundleWriteResult.Failed($"Failed to write audit bundle: {ex.Message}");
}
finally
{
// Clean up temp directory
try
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, recursive: true);
}
}
catch
{
// Ignore cleanup errors
}
}
}
private static string ComputeSha256(byte[] content)
@@ -395,19 +363,6 @@ public sealed class AuditBundleWriter : IAuditBundleWriter
}
}
private static async Task CreateTarGzAsync(string sourceDir, string outputPath, CancellationToken ct)
{
var outputDir = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
{
Directory.CreateDirectory(outputDir);
}
await using var fileStream = File.Create(outputPath);
await using var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal);
await TarFile.CreateFromDirectoryAsync(sourceDir, gzipStream, includeBaseDirectory: false, ct);
}
private sealed record BundleEntry(string Path, string Digest, long SizeBytes);
}