This commit is contained in:
StellaOps Bot
2025-11-23 23:40:10 +02:00
parent c13355923f
commit 029002ad05
93 changed files with 2160 additions and 285 deletions

View File

@@ -0,0 +1,96 @@
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Engine.Streaming;
/// <summary>
/// Request contract for POLICY-ENGINE-29-002/003 streaming simulations.
/// </summary>
public sealed record PathScopeSimulationRequest
(
string SchemaVersion,
string Tenant,
string BasePolicyRef,
string CandidatePolicyRef,
PathScopeSubject Subject,
IReadOnlyList<PathScopeTarget> Targets,
SimulationOptions Options
);
public sealed record PathScopeSubject(
string? Purl,
string? Cpe,
string? PackagePath,
string? OsImage
)
{
public bool HasCoordinates => !string.IsNullOrWhiteSpace(Purl) || !string.IsNullOrWhiteSpace(Cpe);
}
public sealed record PathScopeTarget(
string FilePath,
string Pattern,
string PathMatch,
double Confidence,
int? DepthLimit,
string? Digest,
string? TreeDigest,
string? EvidenceHash,
DateTimeOffset? IngestedAt,
string? ConnectorId
);
public sealed record SimulationOptions(
string Sort,
int? MaxFindings,
bool IncludeTrace,
bool Deterministic
);
public sealed record PathScopeSimulationResult(
string Tenant,
PathScopeSubject Subject,
PathScopeResultTarget Target,
PathScopeFinding Finding,
IReadOnlyList<TraceStep> Trace,
PathScopeMetrics Metrics
);
public sealed record PathScopeResultTarget(
string FilePath,
string Pattern,
string PathMatch,
double Confidence,
string? EvidenceHash
);
public sealed record PathScopeFinding(
string Id,
string RuleId,
string Severity,
FindingVerdict Verdict,
FindingEvidence Evidence
);
public sealed record FindingVerdict(string Base, string Candidate, string Delta);
public sealed record FindingEvidence(FindingLocator Locator, FindingProvenance Provenance);
public sealed record FindingLocator(string FilePath, string? Digest);
public sealed record FindingProvenance(DateTimeOffset? IngestedAt, string? ConnectorId);
public sealed record TraceStep(string Step, string? Rule, string? Path, string? Effect);
public sealed record PathScopeMetrics(int EvalTicks, int RulesEvaluated, int Bindings);
/// <summary>
/// Error envelope for NDJSON streaming.
/// </summary>
public sealed record PathScopeSimulationError(
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("code")] string Code,
[property: JsonPropertyName("message")] string Message
)
{
public static PathScopeSimulationError Schema(string message) => new("error", "POLICY_29_002_SCHEMA", message);
}

View File

@@ -0,0 +1,108 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using StellaOps.Policy.Engine.Services;
namespace StellaOps.Policy.Engine.Streaming;
/// <summary>
/// Minimal, deterministic implementation of path/scope-aware streaming simulation (POLICY-ENGINE-29-003).
/// Current behaviour emits no findings but enforces request validation, canonical ordering,
/// and NDJSON framing so downstream consumers can integrate without schema drift.
/// </summary>
public sealed class PathScopeSimulationService
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private readonly PolicyEvaluationService _evaluationService;
public PathScopeSimulationService(PolicyEvaluationService evaluationService)
{
_evaluationService = evaluationService ?? throw new ArgumentNullException(nameof(evaluationService));
}
public IAsyncEnumerable<string> StreamAsync(PathScopeSimulationRequest request, CancellationToken ct = default)
{
ValidateRequest(request);
var orderedTargets = request.Targets
.OrderBy(t => t.FilePath, StringComparer.Ordinal)
.ThenBy(t => t.Pattern, StringComparer.Ordinal)
.ThenByDescending(t => t.Confidence)
.ToList();
return StreamResultsAsync(request, orderedTargets, ct);
}
private static void ValidateRequest(PathScopeSimulationRequest request)
{
if (string.IsNullOrWhiteSpace(request.SchemaVersion))
{
throw new PathScopeSimulationException(PathScopeSimulationError.Schema("schemaVersion is required"));
}
if (request.Targets is null || request.Targets.Count == 0)
{
throw new PathScopeSimulationException(PathScopeSimulationError.Schema("At least one target is required"));
}
if (!request.Subject.HasCoordinates)
{
throw new PathScopeSimulationException(PathScopeSimulationError.Schema("subject.purl or subject.cpe is required"));
}
foreach (var target in request.Targets)
{
if (string.IsNullOrWhiteSpace(target.FilePath))
{
throw new PathScopeSimulationException(PathScopeSimulationError.Schema("target.filePath is required"));
}
if (string.IsNullOrWhiteSpace(target.Pattern))
{
throw new PathScopeSimulationException(PathScopeSimulationError.Schema("target.pattern is required"));
}
if (string.IsNullOrWhiteSpace(target.PathMatch))
{
throw new PathScopeSimulationException(PathScopeSimulationError.Schema("target.pathMatch is required"));
}
}
if (!request.Options.Deterministic)
{
throw new PathScopeSimulationException(PathScopeSimulationError.Schema("options.deterministic must be true"));
}
}
private async IAsyncEnumerable<string> StreamResultsAsync(
PathScopeSimulationRequest request,
IReadOnlyList<PathScopeTarget> orderedTargets,
[EnumeratorCancellation] CancellationToken ct)
{
foreach (var target in orderedTargets)
{
ct.ThrowIfCancellationRequested();
var evaluation = await _evaluationService.EvaluatePathScopeAsync(
request, target, ct).ConfigureAwait(false);
yield return evaluation.ToJsonString(SerializerOptions);
await Task.Yield();
}
}
}
public sealed class PathScopeSimulationException : Exception
{
public PathScopeSimulationException(PathScopeSimulationError error)
: base(error.Message)
{
Error = error;
}
public PathScopeSimulationError Error { get; }
}