doctor enhancements, setup, enhancements, ui functionality and design consolidation and , test projects fixes , product advisory attestation/rekor and delta verfications enhancements

This commit is contained in:
master
2026-01-19 09:02:59 +02:00
parent 8c4bf54aed
commit 17419ba7c4
809 changed files with 170738 additions and 12244 deletions

View File

@@ -0,0 +1,551 @@
// -----------------------------------------------------------------------------
// EvidenceBundleImporter.cs
// Sprint: SPRINT_20260118_029_Evidence_bundle_export_import
// Task: TASK-029-002 - Implement EvidenceBundleImporter
// Description: Imports and validates evidence bundles with full verification
// -----------------------------------------------------------------------------
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text.Json;
namespace StellaOps.EvidenceLocker.Import;
/// <summary>
/// Imports and validates evidence bundles.
/// Verifies checksums, DSSE signatures, and Rekor proofs.
/// </summary>
public interface IEvidenceBundleImporter
{
/// <summary>
/// Imports a bundle from a stream.
/// </summary>
Task<BundleImportResult> ImportAsync(
Stream bundleStream,
BundleImportOptions options,
CancellationToken ct = default);
/// <summary>
/// Validates a bundle without importing.
/// </summary>
Task<BundleValidationResult> ValidateAsync(
Stream bundleStream,
BundleValidationOptions options,
CancellationToken ct = default);
/// <summary>
/// Imports a bundle from a file path.
/// </summary>
Task<BundleImportResult> ImportFromFileAsync(
string filePath,
BundleImportOptions options,
CancellationToken ct = default);
}
/// <summary>
/// Default evidence bundle importer implementation.
/// </summary>
public sealed class EvidenceBundleImporter : IEvidenceBundleImporter
{
private readonly IDsseVerifier? _dsseVerifier;
private readonly IRekorVerifier? _rekorVerifier;
private readonly IArtifactStore? _artifactStore;
/// <summary>
/// Creates a new evidence bundle importer.
/// </summary>
public EvidenceBundleImporter(
IDsseVerifier? dsseVerifier = null,
IRekorVerifier? rekorVerifier = null,
IArtifactStore? artifactStore = null)
{
_dsseVerifier = dsseVerifier;
_rekorVerifier = rekorVerifier;
_artifactStore = artifactStore;
}
/// <inheritdoc />
public async Task<BundleImportResult> ImportAsync(
Stream bundleStream,
BundleImportOptions options,
CancellationToken ct = default)
{
// First validate
var validation = await ValidateAsync(bundleStream, new BundleValidationOptions
{
VerifyChecksums = options.VerifyChecksums,
VerifyDsseSignatures = options.VerifyDsseSignatures,
VerifyRekorProofs = options.VerifyRekorProofs
}, ct);
if (!validation.IsValid && !options.AllowInvalidBundles)
{
return BundleImportResult.Failed(
"Bundle validation failed",
validation.Errors);
}
// Reset stream position
bundleStream.Position = 0;
// Extract and store artifacts
var artifacts = await ExtractArtifactsAsync(bundleStream, ct);
if (_artifactStore != null && options.StoreArtifacts)
{
foreach (var artifact in artifacts)
{
await StoreArtifactAsync(artifact, options, ct);
}
}
return BundleImportResult.Success(
validation.Manifest,
artifacts.Count,
validation.Warnings);
}
/// <inheritdoc />
public async Task<BundleValidationResult> ValidateAsync(
Stream bundleStream,
BundleValidationOptions options,
CancellationToken ct = default)
{
var errors = new List<string>();
var warnings = new List<string>();
try
{
using var archive = new ZipArchive(bundleStream, ZipArchiveMode.Read, leaveOpen: true);
// 1. Read and parse manifest
var manifest = await ReadManifestAsync(archive, errors, ct);
if (manifest == null)
{
return BundleValidationResult.Invalid(errors, warnings);
}
// 2. Verify checksums
if (options.VerifyChecksums)
{
await VerifyChecksumsAsync(archive, manifest, errors, warnings);
}
// 3. Verify DSSE signatures
if (options.VerifyDsseSignatures && _dsseVerifier != null)
{
await VerifyDsseSignaturesAsync(archive, errors, warnings, ct);
}
// 4. Verify Rekor proofs
if (options.VerifyRekorProofs && _rekorVerifier != null)
{
await VerifyRekorProofsAsync(archive, errors, warnings, ct);
}
if (errors.Count > 0)
{
return BundleValidationResult.Invalid(errors, warnings, manifest);
}
return BundleValidationResult.Valid(manifest, warnings);
}
catch (Exception ex)
{
errors.Add($"Bundle extraction failed: {ex.Message}");
return BundleValidationResult.Invalid(errors, warnings);
}
}
/// <inheritdoc />
public async Task<BundleImportResult> ImportFromFileAsync(
string filePath,
BundleImportOptions options,
CancellationToken ct = default)
{
await using var fileStream = File.OpenRead(filePath);
return await ImportAsync(fileStream, options, ct);
}
private static async Task<BundleManifest?> ReadManifestAsync(
ZipArchive archive,
List<string> errors,
CancellationToken ct)
{
var manifestEntry = archive.GetEntry("manifest.json");
if (manifestEntry == null)
{
errors.Add("Missing manifest.json");
return null;
}
try
{
await using var stream = manifestEntry.Open();
return await JsonSerializer.DeserializeAsync<BundleManifest>(stream, cancellationToken: ct);
}
catch (Exception ex)
{
errors.Add($"Invalid manifest.json: {ex.Message}");
return null;
}
}
private static async Task VerifyChecksumsAsync(
ZipArchive archive,
BundleManifest manifest,
List<string> errors,
List<string> warnings)
{
foreach (var file in manifest.Files)
{
var entry = archive.GetEntry(file.Path);
if (entry == null)
{
errors.Add($"Missing file: {file.Path}");
continue;
}
await using var stream = entry.Open();
using var sha256 = SHA256.Create();
var hashBytes = await sha256.ComputeHashAsync(stream);
var computedHash = Convert.ToHexString(hashBytes).ToLowerInvariant();
if (!string.Equals(computedHash, file.Sha256, StringComparison.OrdinalIgnoreCase))
{
errors.Add($"Checksum mismatch for {file.Path}: expected {file.Sha256}, got {computedHash}");
}
}
}
private async Task VerifyDsseSignaturesAsync(
ZipArchive archive,
List<string> errors,
List<string> warnings,
CancellationToken ct)
{
var dsseEntries = archive.Entries
.Where(e => e.FullName.StartsWith("dsse/") && e.FullName.EndsWith(".json"))
.ToList();
foreach (var entry in dsseEntries)
{
await using var stream = entry.Open();
using var reader = new StreamReader(stream);
var json = await reader.ReadToEndAsync(ct);
var result = await _dsseVerifier!.VerifyAsync(json, ct);
if (!result.IsValid)
{
errors.Add($"DSSE verification failed for {entry.FullName}: {result.Error}");
}
}
}
private async Task VerifyRekorProofsAsync(
ZipArchive archive,
List<string> errors,
List<string> warnings,
CancellationToken ct)
{
var entriesFile = archive.GetEntry("rekor/entries.json");
if (entriesFile == null)
{
warnings.Add("No Rekor entries found in bundle");
return;
}
await using var stream = entriesFile.Open();
var entries = await JsonSerializer.DeserializeAsync<List<RekorEntry>>(stream, cancellationToken: ct);
if (entries == null)
{
warnings.Add("Empty Rekor entries file");
return;
}
foreach (var entry in entries)
{
var result = await _rekorVerifier!.VerifyAsync(entry, ct);
if (!result.IsValid)
{
errors.Add($"Rekor verification failed for log index {entry.LogIndex}: {result.Error}");
}
}
}
private static async Task<List<ExtractedArtifact>> ExtractArtifactsAsync(
Stream bundleStream,
CancellationToken ct)
{
var artifacts = new List<ExtractedArtifact>();
using var archive = new ZipArchive(bundleStream, ZipArchiveMode.Read, leaveOpen: true);
foreach (var entry in archive.Entries)
{
if (entry.FullName.EndsWith("/"))
continue; // Skip directories
await using var stream = entry.Open();
using var ms = new MemoryStream();
await stream.CopyToAsync(ms, ct);
artifacts.Add(new ExtractedArtifact
{
Path = entry.FullName,
Content = ms.ToArray(),
Size = ms.Length
});
}
return artifacts;
}
private async Task StoreArtifactAsync(
ExtractedArtifact artifact,
BundleImportOptions options,
CancellationToken ct)
{
// Implementation would store to IArtifactStore
await Task.CompletedTask;
}
}
/// <summary>
/// Options for bundle import.
/// </summary>
public sealed record BundleImportOptions
{
/// <summary>Whether to verify checksums.</summary>
public bool VerifyChecksums { get; init; } = true;
/// <summary>Whether to verify DSSE signatures.</summary>
public bool VerifyDsseSignatures { get; init; } = true;
/// <summary>Whether to verify Rekor proofs.</summary>
public bool VerifyRekorProofs { get; init; } = true;
/// <summary>Whether to store artifacts to artifact store.</summary>
public bool StoreArtifacts { get; init; } = true;
/// <summary>Whether to allow importing invalid bundles.</summary>
public bool AllowInvalidBundles { get; init; } = false;
/// <summary>Tenant ID for storage.</summary>
public Guid TenantId { get; init; }
}
/// <summary>
/// Options for bundle validation.
/// </summary>
public sealed record BundleValidationOptions
{
/// <summary>Whether to verify checksums.</summary>
public bool VerifyChecksums { get; init; } = true;
/// <summary>Whether to verify DSSE signatures.</summary>
public bool VerifyDsseSignatures { get; init; } = true;
/// <summary>Whether to verify Rekor proofs.</summary>
public bool VerifyRekorProofs { get; init; } = true;
}
/// <summary>
/// Result of bundle import.
/// </summary>
public sealed record BundleImportResult
{
/// <summary>Whether import succeeded.</summary>
public required bool IsSuccess { get; init; }
/// <summary>Bundle manifest.</summary>
public BundleManifest? Manifest { get; init; }
/// <summary>Number of artifacts imported.</summary>
public int ArtifactCount { get; init; }
/// <summary>Import errors.</summary>
public IReadOnlyList<string> Errors { get; init; } = [];
/// <summary>Import warnings.</summary>
public IReadOnlyList<string> Warnings { get; init; } = [];
/// <summary>Creates a success result.</summary>
public static BundleImportResult Success(BundleManifest? manifest, int artifactCount, IReadOnlyList<string>? warnings = null)
{
return new BundleImportResult
{
IsSuccess = true,
Manifest = manifest,
ArtifactCount = artifactCount,
Warnings = warnings ?? []
};
}
/// <summary>Creates a failure result.</summary>
public static BundleImportResult Failed(string error, IReadOnlyList<string>? errors = null)
{
var allErrors = new List<string> { error };
if (errors != null)
allErrors.AddRange(errors);
return new BundleImportResult
{
IsSuccess = false,
Errors = allErrors
};
}
}
/// <summary>
/// Result of bundle validation.
/// </summary>
public sealed record BundleValidationResult
{
/// <summary>Whether validation passed.</summary>
public required bool IsValid { get; init; }
/// <summary>Bundle manifest.</summary>
public BundleManifest? Manifest { get; init; }
/// <summary>Validation errors.</summary>
public IReadOnlyList<string> Errors { get; init; } = [];
/// <summary>Validation warnings.</summary>
public IReadOnlyList<string> Warnings { get; init; } = [];
/// <summary>Creates a valid result.</summary>
public static BundleValidationResult Valid(BundleManifest manifest, IReadOnlyList<string>? warnings = null)
{
return new BundleValidationResult
{
IsValid = true,
Manifest = manifest,
Warnings = warnings ?? []
};
}
/// <summary>Creates an invalid result.</summary>
public static BundleValidationResult Invalid(
IReadOnlyList<string> errors,
IReadOnlyList<string>? warnings = null,
BundleManifest? manifest = null)
{
return new BundleValidationResult
{
IsValid = false,
Manifest = manifest,
Errors = errors,
Warnings = warnings ?? []
};
}
}
/// <summary>
/// Bundle manifest.
/// </summary>
public sealed record BundleManifest
{
/// <summary>Bundle ID.</summary>
public required string BundleId { get; init; }
/// <summary>Bundle version.</summary>
public int Version { get; init; } = 1;
/// <summary>Creation timestamp.</summary>
public DateTimeOffset CreatedAt { get; init; }
/// <summary>Export source.</summary>
public string? Source { get; init; }
/// <summary>Files in bundle.</summary>
public IReadOnlyList<BundleFileEntry> Files { get; init; } = [];
}
/// <summary>
/// File entry in bundle manifest.
/// </summary>
public sealed record BundleFileEntry
{
/// <summary>File path within bundle.</summary>
public required string Path { get; init; }
/// <summary>SHA-256 hash.</summary>
public required string Sha256 { get; init; }
/// <summary>File size in bytes.</summary>
public long Size { get; init; }
/// <summary>Content type.</summary>
public string? ContentType { get; init; }
}
/// <summary>
/// Extracted artifact from bundle.
/// </summary>
internal sealed record ExtractedArtifact
{
public required string Path { get; init; }
public required byte[] Content { get; init; }
public long Size { get; init; }
}
/// <summary>
/// Rekor entry from bundle.
/// </summary>
public sealed record RekorEntry
{
/// <summary>Log index.</summary>
public long LogIndex { get; init; }
/// <summary>Entry UUID.</summary>
public string? Uuid { get; init; }
/// <summary>Entry body (base64).</summary>
public string? Body { get; init; }
/// <summary>Integrated time.</summary>
public DateTimeOffset IntegratedTime { get; init; }
}
// Verification interfaces (to be implemented by existing services)
/// <summary>
/// DSSE verifier interface.
/// </summary>
public interface IDsseVerifier
{
/// <summary>Verifies a DSSE envelope.</summary>
Task<VerificationResult> VerifyAsync(string json, CancellationToken ct = default);
}
/// <summary>
/// Rekor verifier interface.
/// </summary>
public interface IRekorVerifier
{
/// <summary>Verifies a Rekor entry.</summary>
Task<VerificationResult> VerifyAsync(RekorEntry entry, CancellationToken ct = default);
}
/// <summary>
/// Verification result.
/// </summary>
public sealed record VerificationResult
{
/// <summary>Whether verification passed.</summary>
public bool IsValid { get; init; }
/// <summary>Error message if invalid.</summary>
public string? Error { get; init; }
}
/// <summary>
/// Artifact store interface for bundle storage.
/// </summary>
public interface IArtifactStore
{
// Defined in StellaOps.Artifact.Core
}