update
This commit is contained in:
53
src/__Libraries/StellaOps.Evidence.Bundle/EvidenceBundle.cs
Normal file
53
src/__Libraries/StellaOps.Evidence.Bundle/EvidenceBundle.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace StellaOps.Evidence.Bundle;
|
||||
|
||||
/// <summary>A complete evidence bundle for a single finding/alert. Contains all evidence required for triage decision.</summary>
|
||||
public sealed class EvidenceBundle
|
||||
{
|
||||
public string BundleId { get; init; } = Guid.NewGuid().ToString("N");
|
||||
public string SchemaVersion { get; init; } = "1.0";
|
||||
public required string AlertId { get; init; }
|
||||
public required string ArtifactId { get; init; }
|
||||
public ReachabilityEvidence? Reachability { get; init; }
|
||||
public CallStackEvidence? CallStack { get; init; }
|
||||
public ProvenanceEvidence? Provenance { get; init; }
|
||||
public VexStatusEvidence? VexStatus { get; init; }
|
||||
public DiffEvidence? Diff { get; init; }
|
||||
public GraphRevisionEvidence? GraphRevision { get; init; }
|
||||
public required EvidenceHashSet Hashes { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>Compute evidence completeness score (0-4 based on core evidence types).</summary>
|
||||
public int ComputeCompletenessScore()
|
||||
{
|
||||
var score = 0;
|
||||
if (Reachability?.Status == EvidenceStatus.Available) score++;
|
||||
if (CallStack?.Status == EvidenceStatus.Available) score++;
|
||||
if (Provenance?.Status == EvidenceStatus.Available) score++;
|
||||
if (VexStatus?.Status == EvidenceStatus.Available) score++;
|
||||
return score;
|
||||
}
|
||||
|
||||
/// <summary>Create status summary from evidence.</summary>
|
||||
public EvidenceStatusSummary CreateStatusSummary() => new()
|
||||
{
|
||||
Reachability = Reachability?.Status ?? EvidenceStatus.Unavailable,
|
||||
CallStack = CallStack?.Status ?? EvidenceStatus.Unavailable,
|
||||
Provenance = Provenance?.Status ?? EvidenceStatus.Unavailable,
|
||||
VexStatus = VexStatus?.Status ?? EvidenceStatus.Unavailable,
|
||||
Diff = Diff?.Status ?? EvidenceStatus.Unavailable,
|
||||
GraphRevision = GraphRevision?.Status ?? EvidenceStatus.Unavailable
|
||||
};
|
||||
|
||||
/// <summary>Create DSSE predicate for signing.</summary>
|
||||
public EvidenceBundlePredicate ToSigningPredicate() => new()
|
||||
{
|
||||
BundleId = BundleId,
|
||||
AlertId = AlertId,
|
||||
ArtifactId = ArtifactId,
|
||||
CompletenessScore = ComputeCompletenessScore(),
|
||||
Hashes = Hashes,
|
||||
StatusSummary = CreateStatusSummary(),
|
||||
CreatedAt = CreatedAt,
|
||||
SchemaVersion = SchemaVersion
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
namespace StellaOps.Evidence.Bundle;
|
||||
|
||||
/// <summary>Builder for constructing evidence bundles.</summary>
|
||||
public sealed class EvidenceBundleBuilder
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private string? _alertId;
|
||||
private string? _artifactId;
|
||||
private ReachabilityEvidence? _reachability;
|
||||
private CallStackEvidence? _callStack;
|
||||
private ProvenanceEvidence? _provenance;
|
||||
private VexStatusEvidence? _vexStatus;
|
||||
private DiffEvidence? _diff;
|
||||
private GraphRevisionEvidence? _graphRevision;
|
||||
|
||||
public EvidenceBundleBuilder(TimeProvider timeProvider)
|
||||
{
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public EvidenceBundleBuilder() : this(TimeProvider.System) { }
|
||||
|
||||
public EvidenceBundleBuilder WithAlertId(string alertId) { _alertId = alertId; return this; }
|
||||
public EvidenceBundleBuilder WithArtifactId(string artifactId) { _artifactId = artifactId; return this; }
|
||||
public EvidenceBundleBuilder WithReachability(ReachabilityEvidence evidence) { _reachability = evidence; return this; }
|
||||
public EvidenceBundleBuilder WithCallStack(CallStackEvidence evidence) { _callStack = evidence; return this; }
|
||||
public EvidenceBundleBuilder WithProvenance(ProvenanceEvidence evidence) { _provenance = evidence; return this; }
|
||||
public EvidenceBundleBuilder WithVexStatus(VexStatusEvidence evidence) { _vexStatus = evidence; return this; }
|
||||
public EvidenceBundleBuilder WithDiff(DiffEvidence evidence) { _diff = evidence; return this; }
|
||||
public EvidenceBundleBuilder WithGraphRevision(GraphRevisionEvidence evidence) { _graphRevision = evidence; return this; }
|
||||
|
||||
public EvidenceBundle Build()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_alertId))
|
||||
throw new InvalidOperationException("AlertId is required");
|
||||
if (string.IsNullOrWhiteSpace(_artifactId))
|
||||
throw new InvalidOperationException("ArtifactId is required");
|
||||
|
||||
var hashes = new Dictionary<string, string>();
|
||||
if (_reachability?.Hash is not null) hashes["reachability"] = _reachability.Hash;
|
||||
if (_callStack?.Hash is not null) hashes["callstack"] = _callStack.Hash;
|
||||
if (_provenance?.Hash is not null) hashes["provenance"] = _provenance.Hash;
|
||||
if (_vexStatus?.Hash is not null) hashes["vex"] = _vexStatus.Hash;
|
||||
if (_diff?.Hash is not null) hashes["diff"] = _diff.Hash;
|
||||
if (_graphRevision?.Hash is not null) hashes["graph"] = _graphRevision.Hash;
|
||||
|
||||
return new EvidenceBundle
|
||||
{
|
||||
AlertId = _alertId,
|
||||
ArtifactId = _artifactId,
|
||||
Reachability = _reachability,
|
||||
CallStack = _callStack,
|
||||
Provenance = _provenance,
|
||||
VexStatus = _vexStatus,
|
||||
Diff = _diff,
|
||||
GraphRevision = _graphRevision,
|
||||
Hashes = hashes.Count > 0 ? EvidenceHashSet.Compute(hashes) : EvidenceHashSet.Empty(),
|
||||
CreatedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace StellaOps.Evidence.Bundle;
|
||||
|
||||
/// <summary>DSSE predicate for signed evidence bundles. Predicate type: stellaops.dev/predicates/evidence-bundle@v1</summary>
|
||||
public sealed class EvidenceBundlePredicate
|
||||
{
|
||||
public const string PredicateType = "stellaops.dev/predicates/evidence-bundle@v1";
|
||||
public required string BundleId { get; init; }
|
||||
public required string AlertId { get; init; }
|
||||
public required string ArtifactId { get; init; }
|
||||
public required int CompletenessScore { get; init; }
|
||||
public required EvidenceHashSet Hashes { get; init; }
|
||||
public required EvidenceStatusSummary StatusSummary { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public string SchemaVersion { get; init; } = "1.0";
|
||||
}
|
||||
37
src/__Libraries/StellaOps.Evidence.Bundle/EvidenceHashSet.cs
Normal file
37
src/__Libraries/StellaOps.Evidence.Bundle/EvidenceHashSet.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Evidence.Bundle;
|
||||
|
||||
/// <summary>Content-addressed hash set for all evidence artifacts.</summary>
|
||||
public sealed class EvidenceHashSet
|
||||
{
|
||||
public string Algorithm { get; init; } = "SHA-256";
|
||||
public required IReadOnlyList<string> Hashes { get; init; }
|
||||
public required string CombinedHash { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? LabeledHashes { get; init; }
|
||||
|
||||
public static EvidenceHashSet Compute(IDictionary<string, string> labeledHashes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(labeledHashes);
|
||||
var sorted = labeledHashes.OrderBy(kvp => kvp.Key, StringComparer.Ordinal).ToList();
|
||||
var combined = string.Join(":", sorted.Select(kvp => $"{kvp.Key}={kvp.Value}"));
|
||||
var hash = ComputeSha256(combined);
|
||||
return new EvidenceHashSet
|
||||
{
|
||||
Hashes = sorted.Select(kvp => kvp.Value).ToList(),
|
||||
CombinedHash = hash,
|
||||
LabeledHashes = sorted.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
|
||||
};
|
||||
}
|
||||
|
||||
public static EvidenceHashSet Empty() => new()
|
||||
{
|
||||
Hashes = Array.Empty<string>(),
|
||||
CombinedHash = ComputeSha256(string.Empty),
|
||||
LabeledHashes = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
private static string ComputeSha256(string input) =>
|
||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input))).ToLowerInvariant();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace StellaOps.Evidence.Bundle;
|
||||
|
||||
/// <summary>Summary of evidence status for all evidence types.</summary>
|
||||
public sealed class EvidenceStatusSummary
|
||||
{
|
||||
public required EvidenceStatus Reachability { get; init; }
|
||||
public required EvidenceStatus CallStack { get; init; }
|
||||
public required EvidenceStatus Provenance { get; init; }
|
||||
public required EvidenceStatus VexStatus { get; init; }
|
||||
public EvidenceStatus Diff { get; init; } = EvidenceStatus.Unavailable;
|
||||
public EvidenceStatus GraphRevision { get; init; } = EvidenceStatus.Unavailable;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace StellaOps.Evidence.Bundle;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddEvidenceBundleServices(this IServiceCollection services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
services.TryAddTransient<EvidenceBundleBuilder>();
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddEvidenceBundleServices(this IServiceCollection services, TimeProvider timeProvider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(timeProvider);
|
||||
services.AddSingleton(timeProvider);
|
||||
services.TryAddTransient<EvidenceBundleBuilder>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user