work
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user