Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
380 lines
11 KiB
C#
380 lines
11 KiB
C#
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
|
|
{
|
|
}
|