compose and authority fixes. finish sprints.
This commit is contained in:
@@ -59,6 +59,11 @@ public sealed record BundleData
|
||||
/// </summary>
|
||||
public IReadOnlyList<BundleArtifact> ScanResults { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Runtime witness triplet artifacts (trace, DSSE, Sigstore bundle).
|
||||
/// </summary>
|
||||
public IReadOnlyList<BundleArtifact> RuntimeWitnesses { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Public keys for verification.
|
||||
/// </summary>
|
||||
@@ -94,6 +99,26 @@ public sealed record BundleArtifact
|
||||
/// Subject of the artifact.
|
||||
/// </summary>
|
||||
public string? Subject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime witness identity this artifact belongs to.
|
||||
/// </summary>
|
||||
public string? WitnessId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime witness artifact role (trace, dsse, sigstore_bundle).
|
||||
/// </summary>
|
||||
public string? WitnessRole { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic runtime witness lookup keys.
|
||||
/// </summary>
|
||||
public RuntimeWitnessIndexKey? WitnessIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Related artifact paths for witness-level linkage.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? LinkedArtifacts { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -79,18 +79,25 @@ public sealed record BundleManifest
|
||||
[JsonPropertyOrder(8)]
|
||||
public ImmutableArray<ArtifactEntry> ScanResults { get; init; } = ImmutableArray<ArtifactEntry>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime witness artifacts (trace.json/trace.dsse.json/trace.sigstore.json) included in the bundle.
|
||||
/// </summary>
|
||||
[JsonPropertyName("runtimeWitnesses")]
|
||||
[JsonPropertyOrder(9)]
|
||||
public ImmutableArray<ArtifactEntry> RuntimeWitnesses { get; init; } = ImmutableArray<ArtifactEntry>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Public keys for verification.
|
||||
/// </summary>
|
||||
[JsonPropertyName("publicKeys")]
|
||||
[JsonPropertyOrder(9)]
|
||||
[JsonPropertyOrder(10)]
|
||||
public ImmutableArray<KeyEntry> PublicKeys { get; init; } = ImmutableArray<KeyEntry>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Merkle root hash of all artifacts for integrity verification.
|
||||
/// </summary>
|
||||
[JsonPropertyName("merkleRoot")]
|
||||
[JsonPropertyOrder(10)]
|
||||
[JsonPropertyOrder(11)]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? MerkleRoot { get; init; }
|
||||
|
||||
@@ -99,15 +106,20 @@ public sealed record BundleManifest
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IEnumerable<ArtifactEntry> AllArtifacts =>
|
||||
Sboms.Concat(VexStatements).Concat(Attestations).Concat(PolicyVerdicts).Concat(ScanResults);
|
||||
Sboms
|
||||
.Concat(VexStatements)
|
||||
.Concat(Attestations)
|
||||
.Concat(PolicyVerdicts)
|
||||
.Concat(ScanResults)
|
||||
.Concat(RuntimeWitnesses);
|
||||
|
||||
/// <summary>
|
||||
/// Total count of artifacts in the bundle.
|
||||
/// </summary>
|
||||
[JsonPropertyName("totalArtifacts")]
|
||||
[JsonPropertyOrder(11)]
|
||||
[JsonPropertyOrder(12)]
|
||||
public int TotalArtifacts => Sboms.Length + VexStatements.Length + Attestations.Length +
|
||||
PolicyVerdicts.Length + ScanResults.Length;
|
||||
PolicyVerdicts.Length + ScanResults.Length + RuntimeWitnesses.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -165,6 +177,82 @@ public sealed record ArtifactEntry
|
||||
[JsonPropertyOrder(6)]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Subject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime witness identity this artifact belongs to.
|
||||
/// </summary>
|
||||
[JsonPropertyName("witnessId")]
|
||||
[JsonPropertyOrder(7)]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? WitnessId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime witness artifact role (trace, dsse, sigstore_bundle).
|
||||
/// </summary>
|
||||
[JsonPropertyName("witnessRole")]
|
||||
[JsonPropertyOrder(8)]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? WitnessRole { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime witness lookup keys for deterministic replay.
|
||||
/// </summary>
|
||||
[JsonPropertyName("witnessIndex")]
|
||||
[JsonPropertyOrder(9)]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public RuntimeWitnessIndexKey? WitnessIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Related artifact paths for this witness artifact.
|
||||
/// </summary>
|
||||
[JsonPropertyName("linkedArtifacts")]
|
||||
[JsonPropertyOrder(10)]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public ImmutableArray<string>? LinkedArtifacts { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic lookup keys for runtime witness artifacts.
|
||||
/// </summary>
|
||||
public sealed record RuntimeWitnessIndexKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Build ID of the observed userspace binary.
|
||||
/// </summary>
|
||||
[JsonPropertyName("buildId")]
|
||||
[JsonPropertyOrder(0)]
|
||||
public required string BuildId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Kernel release used during runtime collection.
|
||||
/// </summary>
|
||||
[JsonPropertyName("kernelRelease")]
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string KernelRelease { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Probe identifier that produced this runtime witness.
|
||||
/// </summary>
|
||||
[JsonPropertyName("probeId")]
|
||||
[JsonPropertyOrder(2)]
|
||||
public required string ProbeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy run identifier associated with the runtime evidence.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policyRunId")]
|
||||
[JsonPropertyOrder(3)]
|
||||
public required string PolicyRunId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime witness artifact role values.
|
||||
/// </summary>
|
||||
public static class RuntimeWitnessArtifactRoles
|
||||
{
|
||||
public const string Trace = "trace";
|
||||
public const string Dsse = "dsse";
|
||||
public const string SigstoreBundle = "sigstore_bundle";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -234,6 +322,7 @@ public static class BundlePaths
|
||||
public const string AttestationsDirectory = "attestations";
|
||||
public const string PolicyDirectory = "policy";
|
||||
public const string ScansDirectory = "scans";
|
||||
public const string RuntimeWitnessesDirectory = "runtime-witnesses";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -249,4 +338,6 @@ public static class BundleMediaTypes
|
||||
public const string PolicyVerdict = "application/json";
|
||||
public const string ScanResult = "application/json";
|
||||
public const string PublicKeyPem = "application/x-pem-file";
|
||||
public const string RuntimeWitnessTrace = "application/vnd.stellaops.witness.v1+json";
|
||||
public const string SigstoreBundleV03 = "application/vnd.dev.sigstore.bundle.v0.3+json";
|
||||
}
|
||||
|
||||
@@ -329,32 +329,39 @@ public sealed record ExportConfiguration
|
||||
[JsonPropertyOrder(4)]
|
||||
public bool IncludeScanResults { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Include runtime witness triplets (trace, DSSE, Sigstore bundle) in export.
|
||||
/// </summary>
|
||||
[JsonPropertyName("includeRuntimeWitnesses")]
|
||||
[JsonPropertyOrder(5)]
|
||||
public bool IncludeRuntimeWitnesses { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Include public keys for offline verification.
|
||||
/// </summary>
|
||||
[JsonPropertyName("includeKeys")]
|
||||
[JsonPropertyOrder(5)]
|
||||
[JsonPropertyOrder(6)]
|
||||
public bool IncludeKeys { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Include verification scripts.
|
||||
/// </summary>
|
||||
[JsonPropertyName("includeVerifyScripts")]
|
||||
[JsonPropertyOrder(6)]
|
||||
[JsonPropertyOrder(7)]
|
||||
public bool IncludeVerifyScripts { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Compression algorithm (gzip, brotli, none).
|
||||
/// </summary>
|
||||
[JsonPropertyName("compression")]
|
||||
[JsonPropertyOrder(7)]
|
||||
[JsonPropertyOrder(8)]
|
||||
public string Compression { get; init; } = "gzip";
|
||||
|
||||
/// <summary>
|
||||
/// Compression level (1-9).
|
||||
/// </summary>
|
||||
[JsonPropertyName("compressionLevel")]
|
||||
[JsonPropertyOrder(8)]
|
||||
[JsonPropertyOrder(9)]
|
||||
public int CompressionLevel { get; init; } = 6;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
using StellaOps.EvidenceLocker.Export.Models;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.EvidenceLocker.Export;
|
||||
|
||||
/// <summary>
|
||||
/// Validates runtime witness triplets for offline replay verification.
|
||||
/// </summary>
|
||||
public sealed class RuntimeWitnessOfflineVerifier
|
||||
{
|
||||
private static readonly HashSet<string> RequiredRoles = new(StringComparer.Ordinal)
|
||||
{
|
||||
RuntimeWitnessArtifactRoles.Trace,
|
||||
RuntimeWitnessArtifactRoles.Dsse,
|
||||
RuntimeWitnessArtifactRoles.SigstoreBundle
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Verifies runtime witness triplets using only bundle-contained artifacts.
|
||||
/// </summary>
|
||||
public RuntimeWitnessOfflineVerificationResult Verify(
|
||||
BundleManifest manifest,
|
||||
IReadOnlyDictionary<string, byte[]> artifactsByPath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifest);
|
||||
ArgumentNullException.ThrowIfNull(artifactsByPath);
|
||||
|
||||
var errors = new List<string>();
|
||||
|
||||
var witnessArtifacts = manifest.RuntimeWitnesses
|
||||
.OrderBy(static artifact => artifact.WitnessId, StringComparer.Ordinal)
|
||||
.ThenBy(static artifact => artifact.Path, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
foreach (var artifact in witnessArtifacts)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(artifact.WitnessId))
|
||||
{
|
||||
errors.Add($"runtime witness artifact '{artifact.Path}' is missing witnessId.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(artifact.WitnessRole))
|
||||
{
|
||||
errors.Add($"runtime witness artifact '{artifact.Path}' is missing witnessRole.");
|
||||
}
|
||||
else if (!RequiredRoles.Contains(artifact.WitnessRole))
|
||||
{
|
||||
errors.Add($"runtime witness artifact '{artifact.Path}' has unsupported witnessRole '{artifact.WitnessRole}'.");
|
||||
}
|
||||
|
||||
if (artifact.WitnessIndex is null)
|
||||
{
|
||||
errors.Add($"runtime witness artifact '{artifact.Path}' is missing witnessIndex.");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var group in witnessArtifacts.GroupBy(static artifact => artifact.WitnessId, StringComparer.Ordinal))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(group.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
VerifyWitnessTriplet(group.Key!, group.ToList(), artifactsByPath, errors);
|
||||
}
|
||||
|
||||
return errors.Count == 0
|
||||
? RuntimeWitnessOfflineVerificationResult.Passed()
|
||||
: RuntimeWitnessOfflineVerificationResult.Failure(errors);
|
||||
}
|
||||
|
||||
private static void VerifyWitnessTriplet(
|
||||
string witnessId,
|
||||
IReadOnlyList<ArtifactEntry> artifacts,
|
||||
IReadOnlyDictionary<string, byte[]> artifactsByPath,
|
||||
ICollection<string> errors)
|
||||
{
|
||||
var errorCountBefore = errors.Count;
|
||||
|
||||
var roleMap = artifacts
|
||||
.Where(static artifact => !string.IsNullOrWhiteSpace(artifact.WitnessRole))
|
||||
.GroupBy(static artifact => artifact.WitnessRole!, StringComparer.Ordinal)
|
||||
.ToDictionary(static group => group.Key, static group => group.First(), StringComparer.Ordinal);
|
||||
|
||||
foreach (var requiredRole in RequiredRoles)
|
||||
{
|
||||
if (!roleMap.ContainsKey(requiredRole))
|
||||
{
|
||||
errors.Add($"runtime witness '{witnessId}' is missing '{requiredRole}' artifact.");
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count > errorCountBefore && !roleMap.ContainsKey(RuntimeWitnessArtifactRoles.Trace))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!roleMap.TryGetValue(RuntimeWitnessArtifactRoles.Trace, out var traceArtifact)
|
||||
|| !roleMap.TryGetValue(RuntimeWitnessArtifactRoles.Dsse, out var dsseArtifact)
|
||||
|| !roleMap.TryGetValue(RuntimeWitnessArtifactRoles.SigstoreBundle, out var sigstoreArtifact))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryGetArtifactBytes(traceArtifact, artifactsByPath, errors, out var traceBytes)
|
||||
|| !TryGetArtifactBytes(dsseArtifact, artifactsByPath, errors, out var dsseBytes)
|
||||
|| !TryGetArtifactBytes(sigstoreArtifact, artifactsByPath, errors, out var sigstoreBytes))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryGetDssePayload(dsseBytes, dsseArtifact.Path, errors, out var dssePayloadType, out var dssePayloadBase64))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] dssePayloadBytes;
|
||||
try
|
||||
{
|
||||
dssePayloadBytes = Convert.FromBase64String(dssePayloadBase64!);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
errors.Add($"runtime witness '{witnessId}' DSSE payload is not valid base64 in '{dsseArtifact.Path}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!traceBytes.SequenceEqual(dssePayloadBytes))
|
||||
{
|
||||
errors.Add($"runtime witness '{witnessId}' trace payload bytes do not match DSSE payload.");
|
||||
}
|
||||
|
||||
VerifySigstoreBundle(sigstoreBytes, sigstoreArtifact.Path, witnessId, dssePayloadType!, dssePayloadBase64!, errors);
|
||||
}
|
||||
|
||||
private static bool TryGetArtifactBytes(
|
||||
ArtifactEntry artifact,
|
||||
IReadOnlyDictionary<string, byte[]> artifactsByPath,
|
||||
ICollection<string> errors,
|
||||
out byte[] bytes)
|
||||
{
|
||||
if (!artifactsByPath.TryGetValue(artifact.Path, out bytes!))
|
||||
{
|
||||
errors.Add($"runtime witness artifact '{artifact.Path}' is missing from offline artifact set.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var computedDigest = ComputeSha256Hex(bytes);
|
||||
var expectedDigest = NormalizeDigest(artifact.Digest);
|
||||
if (!string.Equals(expectedDigest, computedDigest, StringComparison.Ordinal))
|
||||
{
|
||||
errors.Add($"runtime witness artifact '{artifact.Path}' digest mismatch.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryGetDssePayload(
|
||||
byte[] dsseBytes,
|
||||
string path,
|
||||
ICollection<string> errors,
|
||||
out string? payloadType,
|
||||
out string? payloadBase64)
|
||||
{
|
||||
payloadType = null;
|
||||
payloadBase64 = null;
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(dsseBytes);
|
||||
var root = document.RootElement;
|
||||
|
||||
if (!root.TryGetProperty("payloadType", out var payloadTypeElement)
|
||||
|| string.IsNullOrWhiteSpace(payloadTypeElement.GetString()))
|
||||
{
|
||||
errors.Add($"DSSE envelope '{path}' is missing payloadType.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!root.TryGetProperty("payload", out var payloadElement)
|
||||
|| string.IsNullOrWhiteSpace(payloadElement.GetString()))
|
||||
{
|
||||
errors.Add($"DSSE envelope '{path}' is missing payload.");
|
||||
return false;
|
||||
}
|
||||
|
||||
payloadType = payloadTypeElement.GetString();
|
||||
payloadBase64 = payloadElement.GetString();
|
||||
return true;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
errors.Add($"DSSE envelope '{path}' is not valid JSON.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void VerifySigstoreBundle(
|
||||
byte[] sigstoreBytes,
|
||||
string path,
|
||||
string witnessId,
|
||||
string expectedPayloadType,
|
||||
string expectedPayloadBase64,
|
||||
ICollection<string> errors)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(sigstoreBytes);
|
||||
var root = document.RootElement;
|
||||
|
||||
var mediaType = root.TryGetProperty("mediaType", out var mediaTypeElement)
|
||||
? mediaTypeElement.GetString()
|
||||
: null;
|
||||
if (!string.Equals(mediaType, BundleMediaTypes.SigstoreBundleV03, StringComparison.Ordinal))
|
||||
{
|
||||
errors.Add($"runtime witness '{witnessId}' sigstore bundle '{path}' has unsupported mediaType '{mediaType ?? "<missing>"}'.");
|
||||
}
|
||||
|
||||
if (!root.TryGetProperty("dsseEnvelope", out var dsseEnvelope))
|
||||
{
|
||||
errors.Add($"runtime witness '{witnessId}' sigstore bundle '{path}' is missing dsseEnvelope.");
|
||||
return;
|
||||
}
|
||||
|
||||
var bundlePayloadType = dsseEnvelope.TryGetProperty("payloadType", out var payloadTypeElement)
|
||||
? payloadTypeElement.GetString()
|
||||
: null;
|
||||
var bundlePayload = dsseEnvelope.TryGetProperty("payload", out var payloadElement)
|
||||
? payloadElement.GetString()
|
||||
: null;
|
||||
|
||||
if (!string.Equals(bundlePayloadType, expectedPayloadType, StringComparison.Ordinal))
|
||||
{
|
||||
errors.Add($"runtime witness '{witnessId}' sigstore bundle payloadType does not match trace DSSE envelope.");
|
||||
}
|
||||
|
||||
if (!string.Equals(bundlePayload, expectedPayloadBase64, StringComparison.Ordinal))
|
||||
{
|
||||
errors.Add($"runtime witness '{witnessId}' sigstore bundle payload does not match trace DSSE envelope.");
|
||||
}
|
||||
|
||||
if (!dsseEnvelope.TryGetProperty("signatures", out var signatures)
|
||||
|| signatures.ValueKind != JsonValueKind.Array
|
||||
|| signatures.GetArrayLength() == 0)
|
||||
{
|
||||
errors.Add($"runtime witness '{witnessId}' sigstore bundle '{path}' has no DSSE signatures.");
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
errors.Add($"runtime witness '{witnessId}' sigstore bundle '{path}' is not valid JSON.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeSha256Hex(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
Span<byte> hash = stackalloc byte[32];
|
||||
SHA256.HashData(bytes, hash);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string NormalizeDigest(string digest)
|
||||
{
|
||||
const string prefix = "sha256:";
|
||||
return digest.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
||||
? digest[prefix.Length..].ToLowerInvariant()
|
||||
: digest.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime witness offline verification outcome.
|
||||
/// </summary>
|
||||
public sealed record RuntimeWitnessOfflineVerificationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether verification succeeded.
|
||||
/// </summary>
|
||||
public required bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Verification errors, if any.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Errors { get; init; } = [];
|
||||
|
||||
public static RuntimeWitnessOfflineVerificationResult Passed()
|
||||
=> new()
|
||||
{
|
||||
Success = true
|
||||
};
|
||||
|
||||
public static RuntimeWitnessOfflineVerificationResult Failure(IReadOnlyList<string> errors)
|
||||
=> new()
|
||||
{
|
||||
Success = false,
|
||||
Errors = errors
|
||||
};
|
||||
}
|
||||
@@ -165,6 +165,22 @@ public sealed class TarGzBundleExporter : IEvidenceBundleExporter
|
||||
}
|
||||
}
|
||||
|
||||
// Add runtime witness artifacts (trace / trace.dsse / trace.sigstore)
|
||||
if (config.IncludeRuntimeWitnesses)
|
||||
{
|
||||
foreach (var runtimeWitnessArtifact in bundleData.RuntimeWitnesses)
|
||||
{
|
||||
var entry = await AddArtifactAsync(
|
||||
tarWriter,
|
||||
runtimeWitnessArtifact,
|
||||
BundlePaths.RuntimeWitnessesDirectory,
|
||||
"runtime_witness",
|
||||
cancellationToken);
|
||||
manifestBuilder.AddRuntimeWitness(entry);
|
||||
checksumEntries.Add((entry.Path, entry.Digest));
|
||||
}
|
||||
}
|
||||
|
||||
// Add public keys
|
||||
if (config.IncludeKeys)
|
||||
{
|
||||
@@ -261,7 +277,13 @@ public sealed class TarGzBundleExporter : IEvidenceBundleExporter
|
||||
Size = content.Length,
|
||||
Type = type,
|
||||
Format = artifact.Format,
|
||||
Subject = artifact.Subject
|
||||
Subject = artifact.Subject,
|
||||
WitnessId = artifact.WitnessId,
|
||||
WitnessRole = artifact.WitnessRole,
|
||||
WitnessIndex = artifact.WitnessIndex,
|
||||
LinkedArtifacts = artifact.LinkedArtifacts is null
|
||||
? null
|
||||
: [.. artifact.LinkedArtifacts.Order(StringComparer.Ordinal)]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -450,6 +472,7 @@ public sealed class TarGzBundleExporter : IEvidenceBundleExporter
|
||||
- Attestations: {manifest.Attestations.Length}
|
||||
- Policy Verdicts: {manifest.PolicyVerdicts.Length}
|
||||
- Scan Results: {manifest.ScanResults.Length}
|
||||
- Runtime Witness Artifacts: {manifest.RuntimeWitnesses.Length}
|
||||
- Public Keys: {manifest.PublicKeys.Length}
|
||||
|
||||
Total Artifacts: {manifest.TotalArtifacts}
|
||||
@@ -469,6 +492,7 @@ public sealed class TarGzBundleExporter : IEvidenceBundleExporter
|
||||
+-- attestations/ # DSSE attestation envelopes
|
||||
+-- policy/ # Policy verdicts
|
||||
+-- scans/ # Scan results
|
||||
+-- runtime-witnesses/ # Runtime witness triplets and index metadata
|
||||
+-- keys/ # Public keys for verification
|
||||
```
|
||||
|
||||
@@ -515,6 +539,7 @@ internal sealed class BundleManifestBuilder
|
||||
private readonly List<ArtifactEntry> _attestations = [];
|
||||
private readonly List<ArtifactEntry> _policyVerdicts = [];
|
||||
private readonly List<ArtifactEntry> _scanResults = [];
|
||||
private readonly List<ArtifactEntry> _runtimeWitnesses = [];
|
||||
private readonly List<KeyEntry> _publicKeys = [];
|
||||
|
||||
public BundleManifestBuilder(string bundleId, DateTimeOffset createdAt)
|
||||
@@ -529,6 +554,7 @@ internal sealed class BundleManifestBuilder
|
||||
public void AddAttestation(ArtifactEntry entry) => _attestations.Add(entry);
|
||||
public void AddPolicyVerdict(ArtifactEntry entry) => _policyVerdicts.Add(entry);
|
||||
public void AddScanResult(ArtifactEntry entry) => _scanResults.Add(entry);
|
||||
public void AddRuntimeWitness(ArtifactEntry entry) => _runtimeWitnesses.Add(entry);
|
||||
public void AddPublicKey(KeyEntry entry) => _publicKeys.Add(entry);
|
||||
|
||||
public BundleManifest Build() => new()
|
||||
@@ -541,6 +567,7 @@ internal sealed class BundleManifestBuilder
|
||||
Attestations = [.. _attestations],
|
||||
PolicyVerdicts = [.. _policyVerdicts],
|
||||
ScanResults = [.. _scanResults],
|
||||
RuntimeWitnesses = [.. _runtimeWitnesses],
|
||||
PublicKeys = [.. _publicKeys]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -343,6 +343,7 @@ if __name__ == ""__main__"":
|
||||
| Attestations | {manifest.Attestations.Length} |
|
||||
| Policy Verdicts | {manifest.PolicyVerdicts.Length} |
|
||||
| Scan Results | {manifest.ScanResults.Length} |
|
||||
| Runtime Witness Artifacts | {manifest.RuntimeWitnesses.Length} |
|
||||
| Public Keys | {manifest.PublicKeys.Length} |
|
||||
| **Total Artifacts** | **{manifest.TotalArtifacts}** |
|
||||
|
||||
@@ -362,6 +363,7 @@ if __name__ == ""__main__"":
|
||||
+-- attestations/ # DSSE attestation envelopes
|
||||
+-- policy/ # Policy verdicts
|
||||
+-- scans/ # Scan results
|
||||
+-- runtime-witnesses/ # Runtime witness triplets (trace + DSSE + Sigstore bundle)
|
||||
+-- keys/ # Public keys for verification
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user