up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
This commit is contained in:
379
src/Policy/StellaOps.Policy.Engine/Telemetry/EvidenceBundle.cs
Normal file
379
src/Policy/StellaOps.Policy.Engine/Telemetry/EvidenceBundle.cs
Normal file
@@ -0,0 +1,379 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Telemetry;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an evaluation evidence bundle containing all inputs, outputs,
|
||||
/// and metadata for a policy evaluation run.
|
||||
/// </summary>
|
||||
public sealed class EvidenceBundle
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this evidence bundle.
|
||||
/// </summary>
|
||||
public required string BundleId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Run identifier this bundle is associated with.
|
||||
/// </summary>
|
||||
public required string RunId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant identifier.
|
||||
/// </summary>
|
||||
public required string Tenant { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy identifier.
|
||||
/// </summary>
|
||||
public required string PolicyId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy version.
|
||||
/// </summary>
|
||||
public required string PolicyVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when the bundle was created.
|
||||
/// </summary>
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 hash of the bundle contents for integrity verification.
|
||||
/// </summary>
|
||||
public string? ContentHash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determinism hash from the evaluation run.
|
||||
/// </summary>
|
||||
public string? DeterminismHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Input references for the evaluation.
|
||||
/// </summary>
|
||||
public required EvidenceInputs Inputs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Output summary from the evaluation.
|
||||
/// </summary>
|
||||
public required EvidenceOutputs Outputs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment and configuration metadata.
|
||||
/// </summary>
|
||||
public required EvidenceEnvironment Environment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Manifest listing all artifacts in the bundle.
|
||||
/// </summary>
|
||||
public required EvidenceManifest Manifest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// References to inputs used in the policy evaluation.
|
||||
/// </summary>
|
||||
public sealed class EvidenceInputs
|
||||
{
|
||||
/// <summary>
|
||||
/// SBOM document references with content hashes.
|
||||
/// </summary>
|
||||
public List<EvidenceArtifactRef> SbomRefs { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Advisory document references from Concelier.
|
||||
/// </summary>
|
||||
public List<EvidenceArtifactRef> AdvisoryRefs { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// VEX document references from Excititor.
|
||||
/// </summary>
|
||||
public List<EvidenceArtifactRef> VexRefs { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Reachability evidence references.
|
||||
/// </summary>
|
||||
public List<EvidenceArtifactRef> ReachabilityRefs { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Policy pack IR digest.
|
||||
/// </summary>
|
||||
public string? PolicyIrDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Cursor positions for incremental evaluation.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Cursors { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary of evaluation outputs.
|
||||
/// </summary>
|
||||
public sealed class EvidenceOutputs
|
||||
{
|
||||
/// <summary>
|
||||
/// Total findings evaluated.
|
||||
/// </summary>
|
||||
public int TotalFindings { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Findings by verdict status.
|
||||
/// </summary>
|
||||
public Dictionary<string, int> FindingsByVerdict { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Findings by severity.
|
||||
/// </summary>
|
||||
public Dictionary<string, int> FindingsBySeverity { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Total rules evaluated.
|
||||
/// </summary>
|
||||
public int RulesEvaluated { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total rules that fired.
|
||||
/// </summary>
|
||||
public int RulesFired { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// VEX overrides applied.
|
||||
/// </summary>
|
||||
public int VexOverridesApplied { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration of the evaluation in seconds.
|
||||
/// </summary>
|
||||
public double DurationSeconds { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Outcome of the evaluation (success, failure, canceled).
|
||||
/// </summary>
|
||||
public required string Outcome { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error details if outcome is failure.
|
||||
/// </summary>
|
||||
public string? ErrorDetails { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Environment and configuration metadata for the evaluation.
|
||||
/// </summary>
|
||||
public sealed class EvidenceEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Policy Engine service version.
|
||||
/// </summary>
|
||||
public required string ServiceVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Evaluation mode (full, incremental, simulate).
|
||||
/// </summary>
|
||||
public required string Mode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether sealed/air-gapped mode was active.
|
||||
/// </summary>
|
||||
public bool SealedMode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Host machine identifier.
|
||||
/// </summary>
|
||||
public string? HostId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Trace ID for correlation.
|
||||
/// </summary>
|
||||
public string? TraceId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Configuration snapshot relevant to the evaluation.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> ConfigSnapshot { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manifest listing all artifacts in the evidence bundle.
|
||||
/// </summary>
|
||||
public sealed class EvidenceManifest
|
||||
{
|
||||
/// <summary>
|
||||
/// Version of the manifest schema.
|
||||
/// </summary>
|
||||
public string SchemaVersion { get; init; } = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// List of artifacts in the bundle.
|
||||
/// </summary>
|
||||
public List<EvidenceArtifact> Artifacts { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Adds an artifact to the manifest.
|
||||
/// </summary>
|
||||
public void AddArtifact(string name, string mediaType, long sizeBytes, string contentHash)
|
||||
{
|
||||
Artifacts.Add(new EvidenceArtifact
|
||||
{
|
||||
Name = name,
|
||||
MediaType = mediaType,
|
||||
SizeBytes = sizeBytes,
|
||||
ContentHash = contentHash,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to an external artifact used as input.
|
||||
/// </summary>
|
||||
public sealed class EvidenceArtifactRef
|
||||
{
|
||||
/// <summary>
|
||||
/// URI or identifier for the artifact.
|
||||
/// </summary>
|
||||
public required string Uri { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content hash (SHA-256) of the artifact.
|
||||
/// </summary>
|
||||
public required string ContentHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Media type of the artifact.
|
||||
/// </summary>
|
||||
public string? MediaType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when the artifact was fetched.
|
||||
/// </summary>
|
||||
public DateTimeOffset? FetchedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An artifact included in the evidence bundle.
|
||||
/// </summary>
|
||||
public sealed class EvidenceArtifact
|
||||
{
|
||||
/// <summary>
|
||||
/// Name/path of the artifact within the bundle.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Media type of the artifact.
|
||||
/// </summary>
|
||||
public required string MediaType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Size in bytes.
|
||||
/// </summary>
|
||||
public long SizeBytes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 content hash.
|
||||
/// </summary>
|
||||
public required string ContentHash { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for creating and managing evaluation evidence bundles.
|
||||
/// </summary>
|
||||
public sealed class EvidenceBundleService
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public EvidenceBundleService(TimeProvider timeProvider)
|
||||
{
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new evidence bundle for a policy evaluation run.
|
||||
/// </summary>
|
||||
public EvidenceBundle CreateBundle(
|
||||
string runId,
|
||||
string tenant,
|
||||
string policyId,
|
||||
string policyVersion,
|
||||
string mode,
|
||||
string serviceVersion,
|
||||
bool sealedMode = false,
|
||||
string? traceId = null)
|
||||
{
|
||||
var bundleId = GenerateBundleId(runId);
|
||||
|
||||
return new EvidenceBundle
|
||||
{
|
||||
BundleId = bundleId,
|
||||
RunId = runId,
|
||||
Tenant = tenant,
|
||||
PolicyId = policyId,
|
||||
PolicyVersion = policyVersion,
|
||||
CreatedAt = _timeProvider.GetUtcNow(),
|
||||
Inputs = new EvidenceInputs(),
|
||||
Outputs = new EvidenceOutputs { Outcome = "pending" },
|
||||
Environment = new EvidenceEnvironment
|
||||
{
|
||||
ServiceVersion = serviceVersion,
|
||||
Mode = mode,
|
||||
SealedMode = sealedMode,
|
||||
TraceId = traceId,
|
||||
HostId = Environment.MachineName,
|
||||
},
|
||||
Manifest = new EvidenceManifest(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes the bundle by computing the content hash.
|
||||
/// </summary>
|
||||
public void FinalizeBundle(EvidenceBundle bundle)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bundle);
|
||||
|
||||
var json = JsonSerializer.Serialize(bundle, EvidenceBundleJsonContext.Default.EvidenceBundle);
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
|
||||
bundle.ContentHash = Convert.ToHexStringLower(hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the bundle to JSON.
|
||||
/// </summary>
|
||||
public string SerializeBundle(EvidenceBundle bundle)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bundle);
|
||||
return JsonSerializer.Serialize(bundle, EvidenceBundleJsonContext.Default.EvidenceBundle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a bundle from JSON.
|
||||
/// </summary>
|
||||
public EvidenceBundle? DeserializeBundle(string json)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(json);
|
||||
return JsonSerializer.Deserialize(json, EvidenceBundleJsonContext.Default.EvidenceBundle);
|
||||
}
|
||||
|
||||
private static string GenerateBundleId(string runId)
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
return $"bundle-{runId}-{timestamp:x}";
|
||||
}
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(EvidenceBundle))]
|
||||
[JsonSerializable(typeof(EvidenceInputs))]
|
||||
[JsonSerializable(typeof(EvidenceOutputs))]
|
||||
[JsonSerializable(typeof(EvidenceEnvironment))]
|
||||
[JsonSerializable(typeof(EvidenceManifest))]
|
||||
[JsonSerializable(typeof(EvidenceArtifact))]
|
||||
[JsonSerializable(typeof(EvidenceArtifactRef))]
|
||||
[JsonSourceGenerationOptions(
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
internal partial class EvidenceBundleJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user