finish off sprint advisories and sprints

This commit is contained in:
master
2026-01-24 00:12:43 +02:00
parent 726d70dc7f
commit c70e83719e
266 changed files with 46699 additions and 1328 deletions

View File

@@ -16,4 +16,18 @@ public static class PlatformPolicies
public const string SetupRead = "platform.setup.read";
public const string SetupWrite = "platform.setup.write";
public const string SetupAdmin = "platform.setup.admin";
// Score evaluation policies (TSF-005)
public const string ScoreRead = "platform.score.read";
public const string ScoreEvaluate = "platform.score.evaluate";
// Function map policies (RLV-009)
public const string FunctionMapRead = "platform.functionmap.read";
public const string FunctionMapWrite = "platform.functionmap.write";
public const string FunctionMapVerify = "platform.functionmap.verify";
// Policy interop policies (SPRINT_20260122_041)
public const string PolicyRead = "platform.policy.read";
public const string PolicyWrite = "platform.policy.write";
public const string PolicyEvaluate = "platform.policy.evaluate";
}

View File

@@ -16,4 +16,13 @@ public static class PlatformScopes
public const string SetupRead = "platform.setup.read";
public const string SetupWrite = "platform.setup.write";
public const string SetupAdmin = "platform.setup.admin";
// Score (TSF-005)
public const string ScoreRead = "score.read";
public const string ScoreEvaluate = "score.evaluate";
// Function map (RLV-009)
public const string FunctionMapRead = "functionmap.read";
public const string FunctionMapWrite = "functionmap.write";
public const string FunctionMapVerify = "functionmap.verify";
}

View File

@@ -0,0 +1,239 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_039_Scanner_runtime_linkage_verification
// Task: RLV-009 - Platform API: Function Map Endpoints
using System.Text.Json.Serialization;
namespace StellaOps.Platform.WebService.Contracts;
/// <summary>
/// Request for creating a function map.
/// </summary>
public sealed record CreateFunctionMapRequest
{
[JsonPropertyName("sbomRef")]
public required string SbomRef { get; init; }
[JsonPropertyName("serviceName")]
public required string ServiceName { get; init; }
[JsonPropertyName("hotFunctions")]
public IReadOnlyList<string>? HotFunctions { get; init; }
[JsonPropertyName("options")]
public FunctionMapOptionsDto? Options { get; init; }
}
/// <summary>
/// Options for function map generation.
/// </summary>
public sealed record FunctionMapOptionsDto
{
[JsonPropertyName("minObservationRate")]
public double? MinObservationRate { get; init; }
[JsonPropertyName("windowSeconds")]
public int? WindowSeconds { get; init; }
[JsonPropertyName("failOnUnexpected")]
public bool? FailOnUnexpected { get; init; }
}
/// <summary>
/// Request for verifying observations against a function map.
/// </summary>
public sealed record VerifyFunctionMapRequest
{
[JsonPropertyName("observations")]
public IReadOnlyList<ObservationDto>? Observations { get; init; }
[JsonPropertyName("options")]
public VerifyOptionsDto? Options { get; init; }
}
/// <summary>
/// Observation DTO for API requests.
/// </summary>
public sealed record ObservationDto
{
[JsonPropertyName("observation_id")]
public required string ObservationId { get; init; }
[JsonPropertyName("node_hash")]
public required string NodeHash { get; init; }
[JsonPropertyName("function_name")]
public required string FunctionName { get; init; }
[JsonPropertyName("probe_type")]
public required string ProbeType { get; init; }
[JsonPropertyName("observed_at")]
public required DateTimeOffset ObservedAt { get; init; }
[JsonPropertyName("observation_count")]
public int ObservationCount { get; init; } = 1;
[JsonPropertyName("container_id")]
public string? ContainerId { get; init; }
[JsonPropertyName("pod_name")]
public string? PodName { get; init; }
[JsonPropertyName("namespace")]
public string? Namespace { get; init; }
}
/// <summary>
/// Verification options DTO.
/// </summary>
public sealed record VerifyOptionsDto
{
[JsonPropertyName("minObservationRateOverride")]
public double? MinObservationRateOverride { get; init; }
[JsonPropertyName("windowSecondsOverride")]
public int? WindowSecondsOverride { get; init; }
[JsonPropertyName("failOnUnexpectedOverride")]
public bool? FailOnUnexpectedOverride { get; init; }
[JsonPropertyName("containerIdFilter")]
public string? ContainerIdFilter { get; init; }
[JsonPropertyName("podNameFilter")]
public string? PodNameFilter { get; init; }
}
/// <summary>
/// Function map summary returned in list responses.
/// </summary>
public sealed record FunctionMapSummary
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("serviceName")]
public required string ServiceName { get; init; }
[JsonPropertyName("sbomRef")]
public required string SbomRef { get; init; }
[JsonPropertyName("pathCount")]
public required int PathCount { get; init; }
[JsonPropertyName("createdAt")]
public required DateTimeOffset CreatedAt { get; init; }
[JsonPropertyName("lastVerifiedAt")]
public DateTimeOffset? LastVerifiedAt { get; init; }
[JsonPropertyName("coverageStatus")]
public string? CoverageStatus { get; init; }
}
/// <summary>
/// Full function map detail returned in get responses.
/// </summary>
public sealed record FunctionMapDetail
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("serviceName")]
public required string ServiceName { get; init; }
[JsonPropertyName("sbomRef")]
public required string SbomRef { get; init; }
[JsonPropertyName("pathCount")]
public required int PathCount { get; init; }
[JsonPropertyName("createdAt")]
public required DateTimeOffset CreatedAt { get; init; }
[JsonPropertyName("lastVerifiedAt")]
public DateTimeOffset? LastVerifiedAt { get; init; }
[JsonPropertyName("coverage")]
public FunctionMapCoverageDto? Coverage { get; init; }
[JsonPropertyName("predicateDigest")]
public required string PredicateDigest { get; init; }
}
/// <summary>
/// Coverage thresholds and current status.
/// </summary>
public sealed record FunctionMapCoverageDto
{
[JsonPropertyName("minObservationRate")]
public required double MinObservationRate { get; init; }
[JsonPropertyName("windowSeconds")]
public required int WindowSeconds { get; init; }
[JsonPropertyName("failOnUnexpected")]
public required bool FailOnUnexpected { get; init; }
}
/// <summary>
/// Verification result returned from verify endpoint.
/// </summary>
public sealed record FunctionMapVerifyResponse
{
[JsonPropertyName("verified")]
public required bool Verified { get; init; }
[JsonPropertyName("observationRate")]
public required double ObservationRate { get; init; }
[JsonPropertyName("targetRate")]
public required double TargetRate { get; init; }
[JsonPropertyName("pathCount")]
public required int PathCount { get; init; }
[JsonPropertyName("observedPaths")]
public required int ObservedPaths { get; init; }
[JsonPropertyName("unexpectedSymbolCount")]
public required int UnexpectedSymbolCount { get; init; }
[JsonPropertyName("missingSymbolCount")]
public required int MissingSymbolCount { get; init; }
[JsonPropertyName("verifiedAt")]
public required DateTimeOffset VerifiedAt { get; init; }
[JsonPropertyName("evidenceDigest")]
public required string EvidenceDigest { get; init; }
}
/// <summary>
/// Coverage statistics response.
/// </summary>
public sealed record FunctionMapCoverageResponse
{
[JsonPropertyName("totalPaths")]
public required int TotalPaths { get; init; }
[JsonPropertyName("observedPaths")]
public required int ObservedPaths { get; init; }
[JsonPropertyName("totalExpectedCalls")]
public required int TotalExpectedCalls { get; init; }
[JsonPropertyName("observedCalls")]
public required int ObservedCalls { get; init; }
[JsonPropertyName("coverageRate")]
public required double CoverageRate { get; init; }
[JsonPropertyName("unexpectedSymbolCount")]
public required int UnexpectedSymbolCount { get; init; }
[JsonPropertyName("asOf")]
public required DateTimeOffset AsOf { get; init; }
}

View File

@@ -0,0 +1,309 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_041_Policy_interop_import_export_rego
// Task: TASK-07 - Platform API Endpoints
using System.Text.Json.Serialization;
namespace StellaOps.Platform.WebService.Contracts;
/// <summary>
/// Request to export a policy to a specified format.
/// </summary>
public sealed record PolicyExportApiRequest
{
[JsonPropertyName("policy_content")]
public string? PolicyContent { get; init; }
[JsonPropertyName("format")]
public string Format { get; init; } = "json";
[JsonPropertyName("environment")]
public string? Environment { get; init; }
[JsonPropertyName("include_remediation")]
public bool IncludeRemediation { get; init; } = true;
[JsonPropertyName("include_comments")]
public bool IncludeComments { get; init; } = true;
[JsonPropertyName("package_name")]
public string? PackageName { get; init; }
}
/// <summary>
/// Response from a policy export operation.
/// </summary>
public sealed record PolicyExportApiResponse
{
[JsonPropertyName("success")]
public bool Success { get; init; }
[JsonPropertyName("format")]
public string Format { get; init; } = "json";
[JsonPropertyName("content")]
public string? Content { get; init; }
[JsonPropertyName("digest")]
public string? Digest { get; init; }
[JsonPropertyName("diagnostics")]
public IReadOnlyList<PolicyInteropDiagnostic>? Diagnostics { get; init; }
}
/// <summary>
/// Request to import a policy from a specified format.
/// </summary>
public sealed record PolicyImportApiRequest
{
[JsonPropertyName("content")]
public string Content { get; init; } = "";
[JsonPropertyName("format")]
public string? Format { get; init; }
[JsonPropertyName("validate_only")]
public bool ValidateOnly { get; init; }
[JsonPropertyName("merge_strategy")]
public string MergeStrategy { get; init; } = "replace";
[JsonPropertyName("dry_run")]
public bool DryRun { get; init; }
}
/// <summary>
/// Response from a policy import operation.
/// </summary>
public sealed record PolicyImportApiResponse
{
[JsonPropertyName("success")]
public bool Success { get; init; }
[JsonPropertyName("source_format")]
public string? SourceFormat { get; init; }
[JsonPropertyName("gates_imported")]
public int GatesImported { get; init; }
[JsonPropertyName("rules_imported")]
public int RulesImported { get; init; }
[JsonPropertyName("native_mapped")]
public int NativeMapped { get; init; }
[JsonPropertyName("opa_evaluated")]
public int OpaEvaluated { get; init; }
[JsonPropertyName("diagnostics")]
public IReadOnlyList<PolicyInteropDiagnostic>? Diagnostics { get; init; }
[JsonPropertyName("mappings")]
public IReadOnlyList<PolicyImportMappingDto>? Mappings { get; init; }
}
/// <summary>
/// Request to validate a policy document.
/// </summary>
public sealed record PolicyValidateApiRequest
{
[JsonPropertyName("content")]
public string Content { get; init; } = "";
[JsonPropertyName("format")]
public string? Format { get; init; }
[JsonPropertyName("strict")]
public bool Strict { get; init; }
}
/// <summary>
/// Response from a policy validation operation.
/// </summary>
public sealed record PolicyValidateApiResponse
{
[JsonPropertyName("valid")]
public bool Valid { get; init; }
[JsonPropertyName("detected_format")]
public string? DetectedFormat { get; init; }
[JsonPropertyName("errors")]
public IReadOnlyList<PolicyInteropDiagnostic>? Errors { get; init; }
[JsonPropertyName("warnings")]
public IReadOnlyList<PolicyInteropDiagnostic>? Warnings { get; init; }
}
/// <summary>
/// Request to evaluate a policy against evidence input.
/// </summary>
public sealed record PolicyEvaluateApiRequest
{
[JsonPropertyName("policy_content")]
public string PolicyContent { get; init; } = "";
[JsonPropertyName("input")]
public PolicyEvaluationInputDto? Input { get; init; }
[JsonPropertyName("format")]
public string? Format { get; init; }
[JsonPropertyName("environment")]
public string? Environment { get; init; }
[JsonPropertyName("include_remediation")]
public bool IncludeRemediation { get; init; } = true;
}
/// <summary>
/// Response from a policy evaluation operation.
/// </summary>
public sealed record PolicyEvaluateApiResponse
{
[JsonPropertyName("decision")]
public string Decision { get; init; } = "block";
[JsonPropertyName("gates")]
public IReadOnlyList<GateEvaluationDto>? Gates { get; init; }
[JsonPropertyName("remediation")]
public IReadOnlyList<RemediationHintDto>? Remediation { get; init; }
[JsonPropertyName("output_digest")]
public string? OutputDigest { get; init; }
}
/// <summary>
/// Simplified evidence input for API evaluation.
/// </summary>
public sealed record PolicyEvaluationInputDto
{
[JsonPropertyName("environment")]
public string? Environment { get; init; }
[JsonPropertyName("dsse_verified")]
public bool? DsseVerified { get; init; }
[JsonPropertyName("rekor_verified")]
public bool? RekorVerified { get; init; }
[JsonPropertyName("sbom_digest")]
public string? SbomDigest { get; init; }
[JsonPropertyName("freshness_verified")]
public bool? FreshnessVerified { get; init; }
[JsonPropertyName("cvss_score")]
public double? CvssScore { get; init; }
[JsonPropertyName("confidence")]
public double? Confidence { get; init; }
[JsonPropertyName("reachability_status")]
public string? ReachabilityStatus { get; init; }
[JsonPropertyName("unknowns_ratio")]
public double? UnknownsRatio { get; init; }
}
/// <summary>
/// Gate evaluation result DTO.
/// </summary>
public sealed record GateEvaluationDto
{
[JsonPropertyName("gate_id")]
public string GateId { get; init; } = "";
[JsonPropertyName("gate_type")]
public string GateType { get; init; } = "";
[JsonPropertyName("passed")]
public bool Passed { get; init; }
[JsonPropertyName("reason")]
public string? Reason { get; init; }
}
/// <summary>
/// Remediation hint DTO for API responses.
/// </summary>
public sealed record RemediationHintDto
{
[JsonPropertyName("code")]
public string Code { get; init; } = "";
[JsonPropertyName("title")]
public string Title { get; init; } = "";
[JsonPropertyName("severity")]
public string Severity { get; init; } = "medium";
[JsonPropertyName("actions")]
public IReadOnlyList<RemediationActionDto>? Actions { get; init; }
}
/// <summary>
/// Remediation action DTO.
/// </summary>
public sealed record RemediationActionDto
{
[JsonPropertyName("type")]
public string Type { get; init; } = "";
[JsonPropertyName("description")]
public string Description { get; init; } = "";
[JsonPropertyName("command")]
public string? Command { get; init; }
}
/// <summary>
/// Import mapping showing how Rego rules were translated.
/// </summary>
public sealed record PolicyImportMappingDto
{
[JsonPropertyName("source_rule")]
public string SourceRule { get; init; } = "";
[JsonPropertyName("target_gate_type")]
public string TargetGateType { get; init; } = "";
[JsonPropertyName("mapped_to_native")]
public bool MappedToNative { get; init; }
}
/// <summary>
/// Diagnostic message from interop operations.
/// </summary>
public sealed record PolicyInteropDiagnostic
{
[JsonPropertyName("severity")]
public string Severity { get; init; } = "info";
[JsonPropertyName("code")]
public string Code { get; init; } = "";
[JsonPropertyName("message")]
public string Message { get; init; } = "";
}
/// <summary>
/// Response listing supported formats.
/// </summary>
public sealed record PolicyFormatsApiResponse
{
[JsonPropertyName("formats")]
public IReadOnlyList<PolicyFormatInfo> Formats { get; init; } = [];
}
/// <summary>
/// Information about a supported policy format.
/// </summary>
public sealed record PolicyFormatInfo(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("schema")] string Schema,
[property: JsonPropertyName("import_supported")] bool ImportSupported,
[property: JsonPropertyName("export_supported")] bool ExportSupported);

View File

@@ -0,0 +1,47 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_037_Signals_unified_trust_score_algebra
// Task: Score persistence store
using System.Text.Json.Serialization;
namespace StellaOps.Platform.WebService.Contracts;
/// <summary>
/// Record representing a persisted score history entry.
/// </summary>
public sealed record ScoreHistoryRecord
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("tenant_id")]
public required string TenantId { get; init; }
[JsonPropertyName("project_id")]
public required string ProjectId { get; init; }
[JsonPropertyName("cve_id")]
public required string CveId { get; init; }
[JsonPropertyName("purl")]
public string? Purl { get; init; }
[JsonPropertyName("score")]
public required decimal Score { get; init; }
[JsonPropertyName("band")]
public required string Band { get; init; }
[JsonPropertyName("weights_version")]
public required string WeightsVersion { get; init; }
[JsonPropertyName("signal_snapshot")]
public required string SignalSnapshot { get; init; }
[JsonPropertyName("replay_digest")]
public required string ReplayDigest { get; init; }
[JsonPropertyName("created_at")]
public required DateTimeOffset CreatedAt { get; init; }
}

View File

@@ -0,0 +1,670 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_037_Signals_unified_trust_score_algebra
// Task: TSF-005 - Platform API Endpoints (Score Evaluate)
// Task: TSF-011 - Score Replay & Verification Endpoint
using System.Text.Json.Serialization;
namespace StellaOps.Platform.WebService.Contracts;
/// <summary>
/// Request for score evaluation.
/// </summary>
public sealed record ScoreEvaluateRequest
{
/// <summary>
/// SBOM reference (OCI digest or URL).
/// </summary>
[JsonPropertyName("sbom_ref")]
public string? SbomRef { get; init; }
/// <summary>
/// CVE identifier for direct scoring.
/// </summary>
[JsonPropertyName("cve_id")]
public string? CveId { get; init; }
/// <summary>
/// Package URL (purl) for component identification.
/// </summary>
[JsonPropertyName("purl")]
public string? Purl { get; init; }
/// <summary>
/// CVSS vector string (e.g., "CVSS:3.1/AV:N/AC:L/...").
/// </summary>
[JsonPropertyName("cvss_vector")]
public string? CvssVector { get; init; }
/// <summary>
/// VEX document references.
/// </summary>
[JsonPropertyName("vex_refs")]
public IReadOnlyList<string>? VexRefs { get; init; }
/// <summary>
/// Rekor receipt data for attestation verification.
/// </summary>
[JsonPropertyName("rekor_receipts")]
public IReadOnlyList<string>? RekorReceipts { get; init; }
/// <summary>
/// Runtime witness observations.
/// </summary>
[JsonPropertyName("runtime_witnesses")]
public IReadOnlyList<RuntimeWitnessInput>? RuntimeWitnesses { get; init; }
/// <summary>
/// Signal inputs for direct scoring.
/// </summary>
[JsonPropertyName("signals")]
public SignalInputs? Signals { get; init; }
/// <summary>
/// Scoring options.
/// </summary>
[JsonPropertyName("options")]
public ScoreEvaluateOptions? Options { get; init; }
}
/// <summary>
/// Runtime witness input.
/// </summary>
public sealed record RuntimeWitnessInput
{
/// <summary>
/// Witness type (process, network, file, etc.).
/// </summary>
[JsonPropertyName("type")]
public required string Type { get; init; }
/// <summary>
/// Witness data.
/// </summary>
[JsonPropertyName("data")]
public required string Data { get; init; }
/// <summary>
/// When the witness was observed.
/// </summary>
[JsonPropertyName("observed_at")]
public DateTimeOffset? ObservedAt { get; init; }
}
/// <summary>
/// Direct signal inputs for scoring.
/// </summary>
public sealed record SignalInputs
{
/// <summary>
/// Reachability signal (0.0-1.0).
/// </summary>
[JsonPropertyName("reachability")]
public double? Reachability { get; init; }
/// <summary>
/// Runtime signal (0.0-1.0).
/// </summary>
[JsonPropertyName("runtime")]
public double? Runtime { get; init; }
/// <summary>
/// Backport signal (0.0-1.0).
/// </summary>
[JsonPropertyName("backport")]
public double? Backport { get; init; }
/// <summary>
/// Exploit signal (0.0-1.0).
/// </summary>
[JsonPropertyName("exploit")]
public double? Exploit { get; init; }
/// <summary>
/// Source signal (0.0-1.0).
/// </summary>
[JsonPropertyName("source")]
public double? Source { get; init; }
/// <summary>
/// Mitigation signal (0.0-1.0).
/// </summary>
[JsonPropertyName("mitigation")]
public double? Mitigation { get; init; }
}
/// <summary>
/// Score evaluation options.
/// </summary>
public sealed record ScoreEvaluateOptions
{
/// <summary>
/// Decay lambda for time-based decay.
/// </summary>
[JsonPropertyName("decay_lambda")]
public double? DecayLambda { get; init; }
/// <summary>
/// Weight set ID (manifest version) to use.
/// </summary>
[JsonPropertyName("weight_set_id")]
public string? WeightSetId { get; init; }
/// <summary>
/// Include delta-if-present calculations.
/// </summary>
[JsonPropertyName("include_delta")]
public bool IncludeDelta { get; init; } = true;
/// <summary>
/// Include detailed breakdown.
/// </summary>
[JsonPropertyName("include_breakdown")]
public bool IncludeBreakdown { get; init; } = true;
}
/// <summary>
/// Response from score evaluation.
/// </summary>
public sealed record ScoreEvaluateResponse
{
/// <summary>
/// Unique score ID for replay lookup.
/// </summary>
[JsonPropertyName("score_id")]
public required string ScoreId { get; init; }
/// <summary>
/// Score value (0-100).
/// </summary>
[JsonPropertyName("score_value")]
public required int ScoreValue { get; init; }
/// <summary>
/// Score bucket (ActNow, ScheduleNext, Investigate, Watchlist).
/// </summary>
[JsonPropertyName("bucket")]
public required string Bucket { get; init; }
/// <summary>
/// Unknowns fraction (U) from entropy (0.0-1.0).
/// </summary>
[JsonPropertyName("unknowns_fraction")]
public double? UnknownsFraction { get; init; }
/// <summary>
/// Unknowns band (Complete, Adequate, Sparse, Insufficient).
/// </summary>
[JsonPropertyName("unknowns_band")]
public string? UnknownsBand { get; init; }
/// <summary>
/// Unknown package references.
/// </summary>
[JsonPropertyName("unknowns")]
public IReadOnlyList<string>? Unknowns { get; init; }
/// <summary>
/// OCI reference to score proof bundle.
/// </summary>
[JsonPropertyName("proof_ref")]
public string? ProofRef { get; init; }
/// <summary>
/// Dimension breakdown.
/// </summary>
[JsonPropertyName("breakdown")]
public IReadOnlyList<DimensionBreakdown>? Breakdown { get; init; }
/// <summary>
/// Applied guardrails.
/// </summary>
[JsonPropertyName("guardrails")]
public GuardrailsApplied? Guardrails { get; init; }
/// <summary>
/// Delta-if-present calculations.
/// </summary>
[JsonPropertyName("delta_if_present")]
public IReadOnlyList<SignalDeltaResponse>? DeltaIfPresent { get; init; }
/// <summary>
/// Detected conflicts.
/// </summary>
[JsonPropertyName("conflicts")]
public IReadOnlyList<SignalConflictResponse>? Conflicts { get; init; }
/// <summary>
/// Weight manifest reference.
/// </summary>
[JsonPropertyName("weight_manifest")]
public WeightManifestReference? WeightManifest { get; init; }
/// <summary>
/// EWS digest for replay.
/// </summary>
[JsonPropertyName("ews_digest")]
public required string EwsDigest { get; init; }
/// <summary>
/// Determinization fingerprint for replay.
/// </summary>
[JsonPropertyName("determinization_fingerprint")]
public string? DeterminizationFingerprint { get; init; }
/// <summary>
/// When the score was computed.
/// </summary>
[JsonPropertyName("computed_at")]
public required DateTimeOffset ComputedAt { get; init; }
}
/// <summary>
/// Dimension breakdown in response.
/// </summary>
public sealed record DimensionBreakdown
{
[JsonPropertyName("dimension")]
public required string Dimension { get; init; }
[JsonPropertyName("symbol")]
public required string Symbol { get; init; }
[JsonPropertyName("input_value")]
public required double InputValue { get; init; }
[JsonPropertyName("weight")]
public required double Weight { get; init; }
[JsonPropertyName("contribution")]
public required double Contribution { get; init; }
}
/// <summary>
/// Guardrails applied in scoring.
/// </summary>
public sealed record GuardrailsApplied
{
[JsonPropertyName("speculative_cap")]
public bool SpeculativeCap { get; init; }
[JsonPropertyName("not_affected_cap")]
public bool NotAffectedCap { get; init; }
[JsonPropertyName("runtime_floor")]
public bool RuntimeFloor { get; init; }
[JsonPropertyName("original_score")]
public int OriginalScore { get; init; }
[JsonPropertyName("adjusted_score")]
public int AdjustedScore { get; init; }
}
/// <summary>
/// Signal delta response.
/// </summary>
public sealed record SignalDeltaResponse
{
[JsonPropertyName("signal")]
public required string Signal { get; init; }
[JsonPropertyName("min_impact")]
public required double MinImpact { get; init; }
[JsonPropertyName("max_impact")]
public required double MaxImpact { get; init; }
[JsonPropertyName("weight")]
public required double Weight { get; init; }
[JsonPropertyName("description")]
public required string Description { get; init; }
}
/// <summary>
/// Signal conflict response.
/// </summary>
public sealed record SignalConflictResponse
{
[JsonPropertyName("signal_a")]
public required string SignalA { get; init; }
[JsonPropertyName("signal_b")]
public required string SignalB { get; init; }
[JsonPropertyName("conflict_type")]
public required string ConflictType { get; init; }
[JsonPropertyName("description")]
public required string Description { get; init; }
}
/// <summary>
/// Weight manifest reference.
/// </summary>
public sealed record WeightManifestReference
{
[JsonPropertyName("version")]
public required string Version { get; init; }
[JsonPropertyName("content_hash")]
public required string ContentHash { get; init; }
}
/// <summary>
/// Weight manifest summary for listing.
/// </summary>
public sealed record WeightManifestSummary
{
[JsonPropertyName("version")]
public required string Version { get; init; }
[JsonPropertyName("effective_from")]
public required DateTimeOffset EffectiveFrom { get; init; }
[JsonPropertyName("profile")]
public required string Profile { get; init; }
[JsonPropertyName("content_hash")]
public string? ContentHash { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
}
/// <summary>
/// Full weight manifest detail.
/// </summary>
public sealed record WeightManifestDetail
{
[JsonPropertyName("schema_version")]
public required string SchemaVersion { get; init; }
[JsonPropertyName("version")]
public required string Version { get; init; }
[JsonPropertyName("effective_from")]
public required DateTimeOffset EffectiveFrom { get; init; }
[JsonPropertyName("profile")]
public required string Profile { get; init; }
[JsonPropertyName("content_hash")]
public string? ContentHash { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("weights")]
public required WeightDefinitionsDto Weights { get; init; }
}
/// <summary>
/// Weight definitions DTO.
/// </summary>
public sealed record WeightDefinitionsDto
{
[JsonPropertyName("legacy")]
public LegacyWeightsDto? Legacy { get; init; }
[JsonPropertyName("advisory")]
public AdvisoryWeightsDto? Advisory { get; init; }
}
/// <summary>
/// Legacy weights DTO.
/// </summary>
public sealed record LegacyWeightsDto
{
[JsonPropertyName("rch")]
public double Rch { get; init; }
[JsonPropertyName("rts")]
public double Rts { get; init; }
[JsonPropertyName("bkp")]
public double Bkp { get; init; }
[JsonPropertyName("xpl")]
public double Xpl { get; init; }
[JsonPropertyName("src")]
public double Src { get; init; }
[JsonPropertyName("mit")]
public double Mit { get; init; }
}
/// <summary>
/// Advisory weights DTO.
/// </summary>
public sealed record AdvisoryWeightsDto
{
[JsonPropertyName("cvss")]
public double Cvss { get; init; }
[JsonPropertyName("epss")]
public double Epss { get; init; }
[JsonPropertyName("reachability")]
public double Reachability { get; init; }
[JsonPropertyName("exploit_maturity")]
public double ExploitMaturity { get; init; }
[JsonPropertyName("patch_proof")]
public double PatchProof { get; init; }
}
#region TSF-011: Score Replay Models
/// <summary>
/// Response for score replay endpoint.
/// </summary>
public sealed record ScoreReplayResponse
{
/// <summary>
/// Base64-encoded DSSE envelope containing the signed replay log.
/// </summary>
[JsonPropertyName("signed_replay_log_dsse")]
public required string SignedReplayLogDsse { get; init; }
/// <summary>
/// Rekor transparency log inclusion proof (if anchored).
/// </summary>
[JsonPropertyName("rekor_inclusion")]
public RekorInclusionDto? RekorInclusion { get; init; }
/// <summary>
/// Canonical input hashes for verification.
/// </summary>
[JsonPropertyName("canonical_inputs")]
public required IReadOnlyList<CanonicalInputDto> CanonicalInputs { get; init; }
/// <summary>
/// Transform versions used in scoring.
/// </summary>
[JsonPropertyName("transforms")]
public required IReadOnlyList<TransformStepDto> Transforms { get; init; }
/// <summary>
/// Step-by-step algebra decisions.
/// </summary>
[JsonPropertyName("algebra_steps")]
public required IReadOnlyList<AlgebraStepDto> AlgebraSteps { get; init; }
/// <summary>
/// The final computed score.
/// </summary>
[JsonPropertyName("final_score")]
public required int FinalScore { get; init; }
/// <summary>
/// When the score was computed.
/// </summary>
[JsonPropertyName("computed_at")]
public required DateTimeOffset ComputedAt { get; init; }
}
/// <summary>
/// Rekor inclusion proof DTO.
/// </summary>
public sealed record RekorInclusionDto
{
[JsonPropertyName("log_index")]
public required long LogIndex { get; init; }
[JsonPropertyName("root_hash")]
public required string RootHash { get; init; }
[JsonPropertyName("tree_size")]
public long? TreeSize { get; init; }
[JsonPropertyName("uuid")]
public string? Uuid { get; init; }
[JsonPropertyName("integrated_time")]
public DateTimeOffset? IntegratedTime { get; init; }
}
/// <summary>
/// Canonical input DTO for replay.
/// </summary>
public sealed record CanonicalInputDto
{
[JsonPropertyName("name")]
public required string Name { get; init; }
[JsonPropertyName("sha256")]
public required string Sha256 { get; init; }
[JsonPropertyName("source_ref")]
public string? SourceRef { get; init; }
[JsonPropertyName("size_bytes")]
public long? SizeBytes { get; init; }
}
/// <summary>
/// Transform step DTO for replay.
/// </summary>
public sealed record TransformStepDto
{
[JsonPropertyName("name")]
public required string Name { get; init; }
[JsonPropertyName("version")]
public required string Version { get; init; }
[JsonPropertyName("params")]
public IReadOnlyDictionary<string, object>? Params { get; init; }
}
/// <summary>
/// Algebra step DTO for replay.
/// </summary>
public sealed record AlgebraStepDto
{
[JsonPropertyName("signal")]
public required string Signal { get; init; }
[JsonPropertyName("symbol")]
public required string Symbol { get; init; }
[JsonPropertyName("w")]
public required double Weight { get; init; }
[JsonPropertyName("value")]
public required double Value { get; init; }
[JsonPropertyName("term")]
public required double Term { get; init; }
}
/// <summary>
/// Request for score verification.
/// </summary>
public sealed record ScoreVerifyRequest
{
/// <summary>
/// The replay log DSSE envelope to verify.
/// </summary>
[JsonPropertyName("signed_replay_log_dsse")]
public required string SignedReplayLogDsse { get; init; }
/// <summary>
/// Original inputs for replay verification.
/// </summary>
[JsonPropertyName("original_inputs")]
public ScoreVerifyInputs? OriginalInputs { get; init; }
/// <summary>
/// Whether to verify Rekor inclusion.
/// </summary>
[JsonPropertyName("verify_rekor")]
public bool VerifyRekor { get; init; } = true;
}
/// <summary>
/// Original inputs for verification.
/// </summary>
public sealed record ScoreVerifyInputs
{
[JsonPropertyName("signals")]
public SignalInputs? Signals { get; init; }
[JsonPropertyName("weight_manifest_version")]
public string? WeightManifestVersion { get; init; }
}
/// <summary>
/// Response from score verification.
/// </summary>
public sealed record ScoreVerifyResponse
{
[JsonPropertyName("verified")]
public required bool Verified { get; init; }
[JsonPropertyName("replayed_score")]
public required int ReplayedScore { get; init; }
[JsonPropertyName("original_score")]
public required int OriginalScore { get; init; }
[JsonPropertyName("score_matches")]
public required bool ScoreMatches { get; init; }
[JsonPropertyName("digest_matches")]
public required bool DigestMatches { get; init; }
[JsonPropertyName("signature_valid")]
public bool? SignatureValid { get; init; }
[JsonPropertyName("rekor_proof_valid")]
public bool? RekorProofValid { get; init; }
[JsonPropertyName("differences")]
public IReadOnlyList<VerificationDifferenceDto>? Differences { get; init; }
[JsonPropertyName("verified_at")]
public required DateTimeOffset VerifiedAt { get; init; }
}
/// <summary>
/// Verification difference DTO.
/// </summary>
public sealed record VerificationDifferenceDto
{
[JsonPropertyName("field")]
public required string Field { get; init; }
[JsonPropertyName("expected")]
public required string Expected { get; init; }
[JsonPropertyName("actual")]
public required string Actual { get; init; }
}
#endregion

View File

@@ -0,0 +1,255 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_039_Scanner_runtime_linkage_verification
// Task: RLV-009 - Platform API: Function Map Endpoints
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.Platform.WebService.Constants;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
namespace StellaOps.Platform.WebService.Endpoints;
/// <summary>
/// Function map management API endpoints.
/// </summary>
public static class FunctionMapEndpoints
{
/// <summary>
/// Maps function-map-related endpoints.
/// </summary>
public static IEndpointRouteBuilder MapFunctionMapEndpoints(this IEndpointRouteBuilder app)
{
var maps = app.MapGroup("/api/v1/function-maps")
.WithTags("Function Maps");
MapCrudEndpoints(maps);
MapVerifyEndpoints(maps);
return app;
}
private static void MapCrudEndpoints(IEndpointRouteBuilder maps)
{
// POST /api/v1/function-maps - Create function map
maps.MapPost("/", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IFunctionMapService service,
[FromBody] CreateFunctionMapRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.CreateAsync(
requestContext!,
request,
cancellationToken).ConfigureAwait(false);
return Results.Created(
$"/api/v1/function-maps/{result.Value.Id}",
new PlatformItemResponse<FunctionMapDetail>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value));
})
.WithName("CreateFunctionMap")
.WithSummary("Create function map")
.WithDescription("Creates a new function map from an SBOM reference and hot function patterns.")
.RequireAuthorization(PlatformPolicies.FunctionMapWrite);
// GET /api/v1/function-maps - List function maps
maps.MapGet("/", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IFunctionMapService service,
[FromQuery] int? limit,
[FromQuery] int? offset,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.ListAsync(
requestContext!,
limit ?? 100,
offset ?? 0,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<FunctionMapSummary>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value,
result.Value.Count,
limit ?? 100,
offset ?? 0));
})
.WithName("ListFunctionMaps")
.WithSummary("List function maps")
.WithDescription("Lists all function maps for the current tenant.")
.RequireAuthorization(PlatformPolicies.FunctionMapRead);
// GET /api/v1/function-maps/{id} - Get function map by ID
maps.MapGet("/{id}", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IFunctionMapService service,
string id,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.GetByIdAsync(
requestContext!,
id,
cancellationToken).ConfigureAwait(false);
if (result.Value is null)
{
return Results.NotFound(new { error = "Function map not found", id });
}
return Results.Ok(new PlatformItemResponse<FunctionMapDetail>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value));
})
.WithName("GetFunctionMap")
.WithSummary("Get function map")
.WithDescription("Retrieves a function map by its unique identifier.")
.RequireAuthorization(PlatformPolicies.FunctionMapRead);
// DELETE /api/v1/function-maps/{id} - Delete function map
maps.MapDelete("/{id}", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IFunctionMapService service,
string id,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.DeleteAsync(
requestContext!,
id,
cancellationToken).ConfigureAwait(false);
if (!result.Value)
{
return Results.NotFound(new { error = "Function map not found", id });
}
return Results.NoContent();
})
.WithName("DeleteFunctionMap")
.WithSummary("Delete function map")
.WithDescription("Deletes a function map by its unique identifier.")
.RequireAuthorization(PlatformPolicies.FunctionMapWrite);
}
private static void MapVerifyEndpoints(IEndpointRouteBuilder maps)
{
// POST /api/v1/function-maps/{id}/verify - Verify observations against map
maps.MapPost("/{id}/verify", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IFunctionMapService service,
string id,
[FromBody] VerifyFunctionMapRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.VerifyAsync(
requestContext!,
id,
request,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformItemResponse<FunctionMapVerifyResponse>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value));
})
.WithName("VerifyFunctionMap")
.WithSummary("Verify function map")
.WithDescription("Verifies runtime observations against a declared function map.")
.RequireAuthorization(PlatformPolicies.FunctionMapVerify);
// GET /api/v1/function-maps/{id}/coverage - Get coverage statistics
maps.MapGet("/{id}/coverage", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IFunctionMapService service,
string id,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.GetCoverageAsync(
requestContext!,
id,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformItemResponse<FunctionMapCoverageResponse>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value));
})
.WithName("GetFunctionMapCoverage")
.WithSummary("Get function map coverage")
.WithDescription("Returns current coverage statistics for a function map.")
.RequireAuthorization(PlatformPolicies.FunctionMapRead);
}
private static bool TryResolveContext(
HttpContext context,
PlatformRequestContextResolver resolver,
out PlatformRequestContext? requestContext,
out IResult? failure)
{
if (resolver.TryResolve(context, out requestContext, out var error))
{
failure = null;
return true;
}
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
return false;
}
}

View File

@@ -0,0 +1,244 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_041_Policy_interop_import_export_rego
// Task: TASK-07 - Platform API Endpoints
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.Platform.WebService.Constants;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
namespace StellaOps.Platform.WebService.Endpoints;
/// <summary>
/// Policy import/export interop API endpoints.
/// Provides bidirectional policy exchange between JSON (PolicyPack v2) and OPA/Rego formats.
/// </summary>
public static class PolicyInteropEndpoints
{
/// <summary>
/// Maps policy interop endpoints under /api/v1/policy/interop.
/// </summary>
public static IEndpointRouteBuilder MapPolicyInteropEndpoints(this IEndpointRouteBuilder app)
{
var interop = app.MapGroup("/api/v1/policy/interop")
.WithTags("PolicyInterop");
MapExportEndpoint(interop);
MapImportEndpoint(interop);
MapValidateEndpoint(interop);
MapEvaluateEndpoint(interop);
MapFormatsEndpoint(interop);
return app;
}
private static void MapExportEndpoint(IEndpointRouteBuilder group)
{
// POST /api/v1/policy/interop/export
group.MapPost("/export", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IPolicyInteropService service,
[FromBody] PolicyExportApiRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.ExportAsync(
requestContext!,
request,
cancellationToken).ConfigureAwait(false);
if (!result.Success)
{
return Results.BadRequest(new { error = "export_failed", diagnostics = result.Diagnostics });
}
return Results.Ok(new PlatformItemResponse<PolicyExportApiResponse>(
requestContext!.TenantId,
requestContext.ActorId,
DateTimeOffset.UtcNow,
false,
0,
result));
})
.WithName("ExportPolicy")
.WithSummary("Export policy to format")
.WithDescription("Exports a PolicyPack v2 document to JSON or OPA/Rego format with optional environment-specific thresholds and remediation hints.")
.RequireAuthorization(PlatformPolicies.PolicyRead);
// POST /api/v1/policy/interop/import
}
private static void MapImportEndpoint(IEndpointRouteBuilder group)
{
group.MapPost("/import", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IPolicyInteropService service,
[FromBody] PolicyImportApiRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.ImportAsync(
requestContext!,
request,
cancellationToken).ConfigureAwait(false);
if (!result.Success)
{
return Results.BadRequest(new { error = "import_failed", diagnostics = result.Diagnostics });
}
return Results.Ok(new PlatformItemResponse<PolicyImportApiResponse>(
requestContext!.TenantId,
requestContext.ActorId,
DateTimeOffset.UtcNow,
false,
0,
result));
})
.WithName("ImportPolicy")
.WithSummary("Import policy from format")
.WithDescription("Imports a policy from JSON or OPA/Rego format into the native PolicyPack v2 model. Unknown Rego patterns are preserved for OPA evaluation.")
.RequireAuthorization(PlatformPolicies.PolicyWrite);
}
private static void MapValidateEndpoint(IEndpointRouteBuilder group)
{
// POST /api/v1/policy/interop/validate
group.MapPost("/validate", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IPolicyInteropService service,
[FromBody] PolicyValidateApiRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.ValidateAsync(
requestContext!,
request,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformItemResponse<PolicyValidateApiResponse>(
requestContext!.TenantId,
requestContext.ActorId,
DateTimeOffset.UtcNow,
false,
0,
result));
})
.WithName("ValidatePolicy")
.WithSummary("Validate policy document")
.WithDescription("Validates a policy document against the PolicyPack v2 schema or checks Rego syntax via embedded OPA.")
.RequireAuthorization(PlatformPolicies.PolicyRead);
}
private static void MapEvaluateEndpoint(IEndpointRouteBuilder group)
{
// POST /api/v1/policy/interop/evaluate
group.MapPost("/evaluate", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IPolicyInteropService service,
[FromBody] PolicyEvaluateApiRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.EvaluateAsync(
requestContext!,
request,
cancellationToken).ConfigureAwait(false);
var statusCode = result.Decision switch
{
"allow" => StatusCodes.Status200OK,
"warn" => StatusCodes.Status200OK,
"block" => StatusCodes.Status200OK,
_ => StatusCodes.Status200OK
};
return Results.Ok(new PlatformItemResponse<PolicyEvaluateApiResponse>(
requestContext!.TenantId,
requestContext.ActorId,
DateTimeOffset.UtcNow,
false,
0,
result));
})
.WithName("EvaluatePolicy")
.WithSummary("Evaluate policy against input")
.WithDescription("Evaluates a policy (JSON or Rego) against evidence input and returns allow/warn/block decision with remediation hints.")
.RequireAuthorization(PlatformPolicies.PolicyEvaluate);
}
private static void MapFormatsEndpoint(IEndpointRouteBuilder group)
{
// GET /api/v1/policy/interop/formats
group.MapGet("/formats", (
HttpContext context,
PlatformRequestContextResolver resolver) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var formats = new PolicyFormatsApiResponse
{
Formats =
[
new PolicyFormatInfo("json", "PolicyPack v2 (JSON)", "policy.stellaops.io/v2", true, true),
new PolicyFormatInfo("rego", "OPA/Rego", "package stella.release", true, true)
]
};
return Results.Ok(new PlatformItemResponse<PolicyFormatsApiResponse>(
requestContext!.TenantId,
requestContext.ActorId,
DateTimeOffset.UtcNow,
true,
3600,
formats));
})
.WithName("ListPolicyFormats")
.WithSummary("List supported policy formats")
.WithDescription("Returns the list of supported policy import/export formats.")
.RequireAuthorization(PlatformPolicies.PolicyRead);
}
private static bool TryResolveContext(
HttpContext context,
PlatformRequestContextResolver resolver,
out PlatformRequestContext? requestContext,
out IResult? failure)
{
if (resolver.TryResolve(context, out requestContext, out var error))
{
failure = null;
return true;
}
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
return false;
}
}

View File

@@ -0,0 +1,355 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_037_Signals_unified_trust_score_algebra
// Task: TSF-005 - Platform API Endpoints (Score Evaluate)
// Task: TSF-011 - Score Replay & Verification Endpoint
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.Platform.WebService.Constants;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
namespace StellaOps.Platform.WebService.Endpoints;
/// <summary>
/// Score evaluation API endpoints.
/// </summary>
public static class ScoreEndpoints
{
/// <summary>
/// Maps score-related endpoints.
/// </summary>
public static IEndpointRouteBuilder MapScoreEndpoints(this IEndpointRouteBuilder app)
{
var score = app.MapGroup("/api/v1/score")
.WithTags("Score");
MapEvaluateEndpoints(score);
MapHistoryEndpoints(score);
MapWeightsEndpoints(score);
MapReplayEndpoints(score);
return app;
}
private static void MapHistoryEndpoints(IEndpointRouteBuilder score)
{
// GET /api/v1/score/history - Get score history
score.MapGet("/history", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IScoreEvaluationService service,
[FromQuery] string cve_id,
[FromQuery] string? purl,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
if (string.IsNullOrWhiteSpace(cve_id))
{
return Results.BadRequest(new { error = "cve_id query parameter is required" });
}
var result = await service.GetHistoryAsync(
requestContext!,
cve_id,
purl,
limit ?? 50,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<Contracts.ScoreHistoryRecord>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value,
result.Value.Count));
})
.WithName("GetScoreHistory")
.WithSummary("Get score history")
.WithDescription("Retrieves score computation history for a CVE, optionally filtered by purl.")
.RequireAuthorization(PlatformPolicies.ScoreRead);
}
private static void MapEvaluateEndpoints(IEndpointRouteBuilder score)
{
// POST /api/v1/score/evaluate - Compute unified score
score.MapPost("/evaluate", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IScoreEvaluationService service,
[FromBody] ScoreEvaluateRequest request,
[FromQuery] bool? include_delta,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
// Override options from query params if provided
var effectiveOptions = request.Options ?? new ScoreEvaluateOptions();
if (include_delta.HasValue)
{
effectiveOptions = effectiveOptions with { IncludeDelta = include_delta.Value };
}
var effectiveRequest = request with { Options = effectiveOptions };
var result = await service.EvaluateAsync(
requestContext!,
effectiveRequest,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformItemResponse<ScoreEvaluateResponse>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value));
})
.WithName("EvaluateScore")
.WithSummary("Compute unified score")
.WithDescription("Evaluates a unified trust score combining EWS computation with Determinization entropy.")
.RequireAuthorization(PlatformPolicies.ScoreEvaluate);
// GET /api/v1/score/{scoreId} - Get score by ID
score.MapGet("/{scoreId}", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IScoreEvaluationService service,
string scoreId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.GetByIdAsync(
requestContext!,
scoreId,
cancellationToken).ConfigureAwait(false);
if (result.Value is null)
{
return Results.NotFound(new { error = "Score not found", score_id = scoreId });
}
return Results.Ok(new PlatformItemResponse<ScoreEvaluateResponse>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value));
})
.WithName("GetScore")
.WithSummary("Get score by ID")
.WithDescription("Retrieves a previously computed score by its unique identifier.")
.RequireAuthorization(PlatformPolicies.ScoreRead);
}
private static void MapWeightsEndpoints(IEndpointRouteBuilder score)
{
var weights = score.MapGroup("/weights").WithTags("Score Weights");
// GET /api/v1/score/weights - List available weight manifests
weights.MapGet("/", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IScoreEvaluationService service,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.ListWeightManifestsAsync(
requestContext!,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<WeightManifestSummary>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value,
result.Value.Count));
})
.WithName("ListWeightManifests")
.WithSummary("List weight manifests")
.WithDescription("Lists all available EWS weight manifests.")
.RequireAuthorization(PlatformPolicies.ScoreRead);
// GET /api/v1/score/weights/{version} - Get specific manifest
weights.MapGet("/{version}", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IScoreEvaluationService service,
string version,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.GetWeightManifestAsync(
requestContext!,
version,
cancellationToken).ConfigureAwait(false);
if (result.Value is null)
{
return Results.NotFound(new { error = "Weight manifest not found", version });
}
return Results.Ok(new PlatformItemResponse<WeightManifestDetail>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value));
})
.WithName("GetWeightManifest")
.WithSummary("Get weight manifest")
.WithDescription("Retrieves a specific EWS weight manifest by version.")
.RequireAuthorization(PlatformPolicies.ScoreRead);
// GET /api/v1/score/weights/effective - Get effective manifest for current date
weights.MapGet("/effective", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IScoreEvaluationService service,
[FromQuery] DateTimeOffset? as_of,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.GetEffectiveWeightManifestAsync(
requestContext!,
as_of ?? DateTimeOffset.UtcNow,
cancellationToken).ConfigureAwait(false);
if (result.Value is null)
{
return Results.NotFound(new { error = "No effective weight manifest found" });
}
return Results.Ok(new PlatformItemResponse<WeightManifestDetail>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value));
})
.WithName("GetEffectiveWeightManifest")
.WithSummary("Get effective weight manifest")
.WithDescription("Retrieves the effective EWS weight manifest for a given date.")
.RequireAuthorization(PlatformPolicies.ScoreRead);
}
// TSF-011: Replay and verification endpoints
private static void MapReplayEndpoints(IEndpointRouteBuilder score)
{
// GET /api/v1/score/{scoreId}/replay - Fetch signed replay proof
score.MapGet("/{scoreId}/replay", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IScoreEvaluationService service,
string scoreId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.GetReplayAsync(
requestContext!,
scoreId,
cancellationToken).ConfigureAwait(false);
if (result.Value is null)
{
return Results.NotFound(new { error = "Replay log not found", score_id = scoreId });
}
return Results.Ok(new PlatformItemResponse<ScoreReplayResponse>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value));
})
.WithName("GetScoreReplay")
.WithSummary("Get score replay proof")
.WithDescription("Retrieves a signed replay log for a previously computed score, enabling independent verification by auditors.")
.RequireAuthorization(PlatformPolicies.ScoreRead);
// POST /api/v1/score/verify - Verify a replay log
score.MapPost("/verify", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IScoreEvaluationService service,
[FromBody] ScoreVerifyRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.VerifyReplayAsync(
requestContext!,
request,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformItemResponse<ScoreVerifyResponse>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value));
})
.WithName("VerifyScoreReplay")
.WithSummary("Verify score replay")
.WithDescription("Verifies a signed replay log by re-executing the score computation and comparing results.")
.RequireAuthorization(PlatformPolicies.ScoreRead);
}
private static bool TryResolveContext(
HttpContext context,
PlatformRequestContextResolver resolver,
out PlatformRequestContext? requestContext,
out IResult? failure)
{
if (resolver.TryResolve(context, out requestContext, out var error))
{
failure = null;
return true;
}
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
return false;
}
}

View File

@@ -9,6 +9,7 @@ using StellaOps.Platform.WebService.Endpoints;
using StellaOps.Platform.WebService.Options;
using StellaOps.Platform.WebService.Services;
using StellaOps.Router.AspNet;
using StellaOps.Signals.UnifiedScore;
using StellaOps.Telemetry.Core;
var builder = WebApplication.CreateBuilder(args);
@@ -106,6 +107,11 @@ builder.Services.AddAuthorization(options =>
options.AddStellaOpsScopePolicy(PlatformPolicies.SetupRead, PlatformScopes.SetupRead);
options.AddStellaOpsScopePolicy(PlatformPolicies.SetupWrite, PlatformScopes.SetupWrite);
options.AddStellaOpsScopePolicy(PlatformPolicies.SetupAdmin, PlatformScopes.SetupAdmin);
options.AddStellaOpsScopePolicy(PlatformPolicies.ScoreRead, PlatformScopes.ScoreRead);
options.AddStellaOpsScopePolicy(PlatformPolicies.ScoreEvaluate, PlatformScopes.ScoreEvaluate);
options.AddStellaOpsScopePolicy(PlatformPolicies.FunctionMapRead, PlatformScopes.FunctionMapRead);
options.AddStellaOpsScopePolicy(PlatformPolicies.FunctionMapWrite, PlatformScopes.FunctionMapWrite);
options.AddStellaOpsScopePolicy(PlatformPolicies.FunctionMapVerify, PlatformScopes.FunctionMapVerify);
});
builder.Services.AddSingleton<PlatformRequestContextResolver>();
@@ -139,6 +145,32 @@ builder.Services.AddAnalyticsIngestion(builder.Configuration, bootstrapOptions.S
builder.Services.AddSingleton<PlatformSetupStore>();
builder.Services.AddSingleton<PlatformSetupService>();
// Score evaluation services (TSF-005/TSF-011)
builder.Services.AddUnifiedScoreServices();
builder.Services.AddSingleton<StellaOps.Signals.UnifiedScore.Replay.IReplayLogBuilder,
StellaOps.Signals.UnifiedScore.Replay.ReplayLogBuilder>();
builder.Services.AddSingleton<StellaOps.Signals.UnifiedScore.Replay.IReplayVerifier,
StellaOps.Signals.UnifiedScore.Replay.ReplayVerifier>();
// Score history persistence store
if (!string.IsNullOrWhiteSpace(bootstrapOptions.Storage.PostgresConnectionString))
{
builder.Services.AddSingleton(
Npgsql.NpgsqlDataSource.Create(bootstrapOptions.Storage.PostgresConnectionString));
builder.Services.AddSingleton<IScoreHistoryStore, PostgresScoreHistoryStore>();
}
else
{
builder.Services.AddSingleton<IScoreHistoryStore, InMemoryScoreHistoryStore>();
}
builder.Services.AddSingleton<IScoreEvaluationService, ScoreEvaluationService>();
// Function map services (RLV-009)
builder.Services.AddSingleton<StellaOps.Scanner.Reachability.FunctionMap.Verification.IClaimVerifier,
StellaOps.Scanner.Reachability.FunctionMap.Verification.ClaimVerifier>();
builder.Services.AddSingleton<IFunctionMapService, FunctionMapService>();
var routerOptions = builder.Configuration.GetSection("Platform:Router").Get<StellaRouterOptionsBase>();
builder.Services.TryAddStellaRouter(
serviceName: "platform",
@@ -165,6 +197,9 @@ app.TryUseStellaRouter(routerOptions);
app.MapPlatformEndpoints();
app.MapSetupEndpoints();
app.MapAnalyticsEndpoints();
app.MapScoreEndpoints();
app.MapFunctionMapEndpoints();
app.MapPolicyInteropEndpoints();
app.MapGet("/healthz", () => Results.Ok(new { status = "ok" }))
.WithTags("Health")

View File

@@ -0,0 +1,298 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_039_Scanner_runtime_linkage_verification
// Task: RLV-009 - Platform API: Function Map Endpoints
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Scanner.Reachability.FunctionMap;
using StellaOps.Scanner.Reachability.FunctionMap.Verification;
namespace StellaOps.Platform.WebService.Services;
/// <summary>
/// In-memory implementation of function map service.
/// Production deployments should replace with a Postgres-backed implementation.
/// </summary>
public sealed class FunctionMapService : IFunctionMapService
{
private readonly ConcurrentDictionary<string, StoredFunctionMap> _maps = new();
private readonly IClaimVerifier _claimVerifier;
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public FunctionMapService(IClaimVerifier claimVerifier)
{
_claimVerifier = claimVerifier;
}
public Task<PlatformCacheResult<FunctionMapDetail>> CreateAsync(
PlatformRequestContext context,
CreateFunctionMapRequest request,
CancellationToken ct = default)
{
var id = $"fmap-{Guid.NewGuid():N}";
var now = DateTimeOffset.UtcNow;
var coverage = new CoverageThresholds
{
MinObservationRate = request.Options?.MinObservationRate ?? 0.95,
WindowSeconds = request.Options?.WindowSeconds ?? 1800,
FailOnUnexpected = request.Options?.FailOnUnexpected ?? false
};
var predicate = new FunctionMapPredicate
{
Subject = new FunctionMapSubject
{
Purl = request.SbomRef,
Digest = new Dictionary<string, string>()
},
Predicate = new FunctionMapPredicatePayload
{
Service = request.ServiceName,
ExpectedPaths = [],
Coverage = coverage,
GeneratedAt = now,
GeneratedFrom = new FunctionMapGeneratedFrom
{
SbomRef = request.SbomRef,
HotFunctionPatterns = request.HotFunctions
}
}
};
// Compute digest from stable inputs only (exclude GeneratedAt for determinism)
var digestInput = new
{
service = request.ServiceName,
sbomRef = request.SbomRef,
hotFunctions = request.HotFunctions ?? [],
minObservationRate = coverage.MinObservationRate,
windowSeconds = coverage.WindowSeconds,
failOnUnexpected = coverage.FailOnUnexpected
};
var digest = ComputeSha256(JsonSerializer.Serialize(digestInput, JsonOptions));
var stored = new StoredFunctionMap(
id,
context.TenantId,
request.ServiceName,
request.SbomRef,
predicate,
digest,
now,
null);
_maps[TenantKey(context.TenantId, id)] = stored;
var detail = ToDetail(stored);
return Task.FromResult(new PlatformCacheResult<FunctionMapDetail>(detail, now, false, 0));
}
public Task<PlatformCacheResult<IReadOnlyList<FunctionMapSummary>>> ListAsync(
PlatformRequestContext context,
int limit = 100,
int offset = 0,
CancellationToken ct = default)
{
var tenantMaps = _maps.Values
.Where(m => m.TenantId == context.TenantId)
.OrderByDescending(m => m.CreatedAt)
.Skip(offset)
.Take(limit)
.Select(ToSummary)
.ToList();
return Task.FromResult(new PlatformCacheResult<IReadOnlyList<FunctionMapSummary>>(
tenantMaps, DateTimeOffset.UtcNow, false, 0));
}
public Task<PlatformCacheResult<FunctionMapDetail?>> GetByIdAsync(
PlatformRequestContext context,
string id,
CancellationToken ct = default)
{
var key = TenantKey(context.TenantId, id);
FunctionMapDetail? detail = _maps.TryGetValue(key, out var stored) ? ToDetail(stored) : null;
return Task.FromResult(new PlatformCacheResult<FunctionMapDetail?>(
detail, DateTimeOffset.UtcNow, false, 0));
}
public Task<PlatformCacheResult<bool>> DeleteAsync(
PlatformRequestContext context,
string id,
CancellationToken ct = default)
{
var key = TenantKey(context.TenantId, id);
var removed = _maps.TryRemove(key, out _);
return Task.FromResult(new PlatformCacheResult<bool>(
removed, DateTimeOffset.UtcNow, false, 0));
}
public async Task<PlatformCacheResult<FunctionMapVerifyResponse>> VerifyAsync(
PlatformRequestContext context,
string id,
VerifyFunctionMapRequest request,
CancellationToken ct = default)
{
var key = TenantKey(context.TenantId, id);
if (!_maps.TryGetValue(key, out var stored))
{
return new PlatformCacheResult<FunctionMapVerifyResponse>(
new FunctionMapVerifyResponse
{
Verified = false,
ObservationRate = 0,
TargetRate = 0,
PathCount = 0,
ObservedPaths = 0,
UnexpectedSymbolCount = 0,
MissingSymbolCount = 0,
VerifiedAt = DateTimeOffset.UtcNow,
EvidenceDigest = ""
},
DateTimeOffset.UtcNow, false, 0);
}
var observations = (request.Observations ?? [])
.Select(o => new ClaimObservation
{
ObservationId = o.ObservationId,
NodeHash = o.NodeHash,
FunctionName = o.FunctionName,
ProbeType = o.ProbeType,
ObservedAt = o.ObservedAt,
ObservationCount = o.ObservationCount,
ContainerId = o.ContainerId,
PodName = o.PodName,
Namespace = o.Namespace
})
.ToList();
var verifyOptions = new ClaimVerificationOptions
{
MinObservationRateOverride = request.Options?.MinObservationRateOverride,
WindowSecondsOverride = request.Options?.WindowSecondsOverride,
FailOnUnexpectedOverride = request.Options?.FailOnUnexpectedOverride,
ContainerIdFilter = request.Options?.ContainerIdFilter,
PodNameFilter = request.Options?.PodNameFilter
};
var result = await _claimVerifier.VerifyAsync(
stored.Predicate, observations, verifyOptions, ct);
// Update last verified timestamp
_maps[key] = stored with { LastVerifiedAt = DateTimeOffset.UtcNow };
var response = new FunctionMapVerifyResponse
{
Verified = result.Verified,
ObservationRate = result.ObservationRate,
TargetRate = result.TargetRate,
PathCount = result.Paths.Count,
ObservedPaths = result.Paths.Count(p => p.Observed),
UnexpectedSymbolCount = result.UnexpectedSymbols.Count,
MissingSymbolCount = result.MissingExpectedSymbols.Count,
VerifiedAt = result.VerifiedAt,
EvidenceDigest = result.Evidence.FunctionMapDigest
};
return new PlatformCacheResult<FunctionMapVerifyResponse>(
response, DateTimeOffset.UtcNow, false, 0);
}
public Task<PlatformCacheResult<FunctionMapCoverageResponse>> GetCoverageAsync(
PlatformRequestContext context,
string id,
CancellationToken ct = default)
{
var key = TenantKey(context.TenantId, id);
if (!_maps.TryGetValue(key, out var stored))
{
return Task.FromResult(new PlatformCacheResult<FunctionMapCoverageResponse>(
new FunctionMapCoverageResponse
{
TotalPaths = 0,
ObservedPaths = 0,
TotalExpectedCalls = 0,
ObservedCalls = 0,
CoverageRate = 0,
UnexpectedSymbolCount = 0,
AsOf = DateTimeOffset.UtcNow
},
DateTimeOffset.UtcNow, false, 0));
}
// Compute coverage from empty observations (returns baseline stats)
var stats = _claimVerifier.ComputeCoverage(stored.Predicate, []);
var response = new FunctionMapCoverageResponse
{
TotalPaths = stats.TotalPaths,
ObservedPaths = stats.ObservedPaths,
TotalExpectedCalls = stats.TotalExpectedCalls,
ObservedCalls = stats.ObservedCalls,
CoverageRate = stats.CoverageRate,
UnexpectedSymbolCount = stats.UnexpectedSymbolCount,
AsOf = DateTimeOffset.UtcNow
};
return Task.FromResult(new PlatformCacheResult<FunctionMapCoverageResponse>(
response, DateTimeOffset.UtcNow, false, 0));
}
private static string TenantKey(string tenantId, string id) => $"{tenantId}:{id}";
private static string ComputeSha256(string input)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return $"sha256:{Convert.ToHexStringLower(hash)}";
}
private static FunctionMapSummary ToSummary(StoredFunctionMap stored) => new()
{
Id = stored.Id,
ServiceName = stored.ServiceName,
SbomRef = stored.SbomRef,
PathCount = stored.Predicate.Predicate.ExpectedPaths.Count,
CreatedAt = stored.CreatedAt,
LastVerifiedAt = stored.LastVerifiedAt,
CoverageStatus = stored.LastVerifiedAt.HasValue ? "verified" : "pending"
};
private static FunctionMapDetail ToDetail(StoredFunctionMap stored) => new()
{
Id = stored.Id,
ServiceName = stored.ServiceName,
SbomRef = stored.SbomRef,
PathCount = stored.Predicate.Predicate.ExpectedPaths.Count,
CreatedAt = stored.CreatedAt,
LastVerifiedAt = stored.LastVerifiedAt,
Coverage = new FunctionMapCoverageDto
{
MinObservationRate = stored.Predicate.Predicate.Coverage.MinObservationRate,
WindowSeconds = stored.Predicate.Predicate.Coverage.WindowSeconds,
FailOnUnexpected = stored.Predicate.Predicate.Coverage.FailOnUnexpected
},
PredicateDigest = stored.PredicateDigest
};
private sealed record StoredFunctionMap(
string Id,
string TenantId,
string ServiceName,
string SbomRef,
FunctionMapPredicate Predicate,
string PredicateDigest,
DateTimeOffset CreatedAt,
DateTimeOffset? LastVerifiedAt);
}

View File

@@ -0,0 +1,64 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_039_Scanner_runtime_linkage_verification
// Task: RLV-009 - Platform API: Function Map Endpoints
using StellaOps.Platform.WebService.Contracts;
namespace StellaOps.Platform.WebService.Services;
/// <summary>
/// Service for managing function maps and executing verification.
/// </summary>
public interface IFunctionMapService
{
/// <summary>
/// Creates and stores a function map from the provided request.
/// </summary>
Task<PlatformCacheResult<FunctionMapDetail>> CreateAsync(
PlatformRequestContext context,
CreateFunctionMapRequest request,
CancellationToken ct = default);
/// <summary>
/// Lists all function maps for the current tenant.
/// </summary>
Task<PlatformCacheResult<IReadOnlyList<FunctionMapSummary>>> ListAsync(
PlatformRequestContext context,
int limit = 100,
int offset = 0,
CancellationToken ct = default);
/// <summary>
/// Gets a function map by ID.
/// </summary>
Task<PlatformCacheResult<FunctionMapDetail?>> GetByIdAsync(
PlatformRequestContext context,
string id,
CancellationToken ct = default);
/// <summary>
/// Deletes a function map by ID.
/// </summary>
Task<PlatformCacheResult<bool>> DeleteAsync(
PlatformRequestContext context,
string id,
CancellationToken ct = default);
/// <summary>
/// Verifies observations against a function map.
/// </summary>
Task<PlatformCacheResult<FunctionMapVerifyResponse>> VerifyAsync(
PlatformRequestContext context,
string id,
VerifyFunctionMapRequest request,
CancellationToken ct = default);
/// <summary>
/// Gets coverage statistics for a function map.
/// </summary>
Task<PlatformCacheResult<FunctionMapCoverageResponse>> GetCoverageAsync(
PlatformRequestContext context,
string id,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,34 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_041_Policy_interop_import_export_rego
// Task: TASK-07 - Platform API Endpoints
using StellaOps.Platform.WebService.Contracts;
namespace StellaOps.Platform.WebService.Services;
/// <summary>
/// Service interface for policy interop operations (export, import, validate, evaluate).
/// </summary>
public interface IPolicyInteropService
{
Task<PolicyExportApiResponse> ExportAsync(
PlatformRequestContext context,
PolicyExportApiRequest request,
CancellationToken ct = default);
Task<PolicyImportApiResponse> ImportAsync(
PlatformRequestContext context,
PolicyImportApiRequest request,
CancellationToken ct = default);
Task<PolicyValidateApiResponse> ValidateAsync(
PlatformRequestContext context,
PolicyValidateApiRequest request,
CancellationToken ct = default);
Task<PolicyEvaluateApiResponse> EvaluateAsync(
PlatformRequestContext context,
PolicyEvaluateApiRequest request,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,82 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_037_Signals_unified_trust_score_algebra
// Task: TSF-005 - Platform API Endpoints (Score Evaluate)
// Task: TSF-011 - Score Replay & Verification Endpoint
using StellaOps.Platform.WebService.Contracts;
namespace StellaOps.Platform.WebService.Services;
/// <summary>
/// Service for unified score evaluation.
/// </summary>
public interface IScoreEvaluationService
{
/// <summary>
/// Evaluates a unified score.
/// </summary>
Task<PlatformCacheResult<ScoreEvaluateResponse>> EvaluateAsync(
PlatformRequestContext context,
ScoreEvaluateRequest request,
CancellationToken ct = default);
/// <summary>
/// Gets a score by ID.
/// </summary>
Task<PlatformCacheResult<ScoreEvaluateResponse?>> GetByIdAsync(
PlatformRequestContext context,
string scoreId,
CancellationToken ct = default);
/// <summary>
/// Lists available weight manifests.
/// </summary>
Task<PlatformCacheResult<IReadOnlyList<WeightManifestSummary>>> ListWeightManifestsAsync(
PlatformRequestContext context,
CancellationToken ct = default);
/// <summary>
/// Gets a specific weight manifest.
/// </summary>
Task<PlatformCacheResult<WeightManifestDetail?>> GetWeightManifestAsync(
PlatformRequestContext context,
string version,
CancellationToken ct = default);
/// <summary>
/// Gets the effective weight manifest for a date.
/// </summary>
Task<PlatformCacheResult<WeightManifestDetail?>> GetEffectiveWeightManifestAsync(
PlatformRequestContext context,
DateTimeOffset asOf,
CancellationToken ct = default);
/// <summary>
/// Gets score history for a CVE.
/// </summary>
Task<PlatformCacheResult<IReadOnlyList<ScoreHistoryRecord>>> GetHistoryAsync(
PlatformRequestContext context,
string cveId,
string? purl,
int limit,
CancellationToken ct = default);
// TSF-011: Replay and verification methods
/// <summary>
/// Gets a signed replay log for a score.
/// </summary>
Task<PlatformCacheResult<ScoreReplayResponse?>> GetReplayAsync(
PlatformRequestContext context,
string scoreId,
CancellationToken ct = default);
/// <summary>
/// Verifies a replay log by re-executing the computation.
/// </summary>
Task<PlatformCacheResult<ScoreVerifyResponse>> VerifyReplayAsync(
PlatformRequestContext context,
ScoreVerifyRequest request,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,43 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_037_Signals_unified_trust_score_algebra
// Task: Score persistence store interface
using StellaOps.Platform.WebService.Contracts;
namespace StellaOps.Platform.WebService.Services;
/// <summary>
/// Interface for score history persistence.
/// </summary>
public interface IScoreHistoryStore
{
/// <summary>
/// Persists a score history record.
/// </summary>
Task StoreAsync(ScoreHistoryRecord record, CancellationToken ct = default);
/// <summary>
/// Retrieves a score record by ID within a tenant.
/// </summary>
Task<ScoreHistoryRecord?> GetByIdAsync(string id, string tenantId, CancellationToken ct = default);
/// <summary>
/// Retrieves score history for a given CVE (optionally filtered by purl).
/// </summary>
Task<IReadOnlyList<ScoreHistoryRecord>> GetHistoryAsync(
string tenantId,
string cveId,
string? purl,
int limit,
CancellationToken ct = default);
/// <summary>
/// Retrieves the most recent score for a given CVE (optionally filtered by purl).
/// </summary>
Task<ScoreHistoryRecord?> GetLatestAsync(
string tenantId,
string cveId,
string? purl,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,67 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_037_Signals_unified_trust_score_algebra
// Task: Score persistence store - in-memory fallback
using System.Collections.Concurrent;
using StellaOps.Platform.WebService.Contracts;
namespace StellaOps.Platform.WebService.Services;
/// <summary>
/// In-memory implementation of <see cref="IScoreHistoryStore"/> for development/testing.
/// </summary>
public sealed class InMemoryScoreHistoryStore : IScoreHistoryStore
{
private readonly ConcurrentDictionary<string, ScoreHistoryRecord> _records = new();
public Task StoreAsync(ScoreHistoryRecord record, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
_records.TryAdd(record.Id, record);
return Task.CompletedTask;
}
public Task<ScoreHistoryRecord?> GetByIdAsync(string id, string tenantId, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
_records.TryGetValue(id, out var record);
if (record is not null && record.TenantId != tenantId)
{
record = null;
}
return Task.FromResult(record);
}
public Task<IReadOnlyList<ScoreHistoryRecord>> GetHistoryAsync(
string tenantId,
string cveId,
string? purl,
int limit,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var results = _records.Values
.Where(r => r.TenantId == tenantId && r.CveId == cveId)
.Where(r => purl is null || r.Purl == purl)
.OrderByDescending(r => r.CreatedAt)
.Take(limit)
.ToList();
return Task.FromResult<IReadOnlyList<ScoreHistoryRecord>>(results);
}
public Task<ScoreHistoryRecord?> GetLatestAsync(
string tenantId,
string cveId,
string? purl,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var record = _records.Values
.Where(r => r.TenantId == tenantId && r.CveId == cveId)
.Where(r => purl is null || r.Purl == purl)
.OrderByDescending(r => r.CreatedAt)
.FirstOrDefault();
return Task.FromResult(record);
}
}

View File

@@ -0,0 +1,423 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_041_Policy_interop_import_export_rego
// Task: TASK-07 - Platform API Endpoints
using System.Text.Json;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Policy.Interop.Abstractions;
using StellaOps.Policy.Interop.Contracts;
using StellaOps.Policy.Interop.Export;
using StellaOps.Policy.Interop.Import;
using StellaOps.Policy.Interop.Rego;
namespace StellaOps.Platform.WebService.Services;
/// <summary>
/// Platform-level service orchestrating policy interop operations.
/// Delegates to the Policy.Interop library for format handling.
/// </summary>
public sealed class PolicyInteropService : IPolicyInteropService
{
private readonly JsonPolicyExporter _jsonExporter = new();
private readonly JsonPolicyImporter _jsonImporter = new();
private readonly RegoPolicyImporter _regoImporter = new();
private readonly RegoCodeGenerator _regoGenerator = new();
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
WriteIndented = true
};
public async Task<PolicyExportApiResponse> ExportAsync(
PlatformRequestContext context,
PolicyExportApiRequest request,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(request.PolicyContent))
{
return new PolicyExportApiResponse
{
Success = false,
Format = request.Format,
Diagnostics = [new PolicyInteropDiagnostic { Severity = "error", Code = "EMPTY_INPUT", Message = "Policy content is required." }]
};
}
PolicyPackDocument doc;
try
{
doc = JsonSerializer.Deserialize<PolicyPackDocument>(request.PolicyContent, JsonOptions)!;
}
catch (JsonException ex)
{
return new PolicyExportApiResponse
{
Success = false,
Format = request.Format,
Diagnostics = [new PolicyInteropDiagnostic { Severity = "error", Code = "PARSE_ERROR", Message = $"Failed to parse policy JSON: {ex.Message}" }]
};
}
if (request.Format.Equals("rego", StringComparison.OrdinalIgnoreCase))
{
var result = _regoGenerator.Generate(doc, new RegoGenerationOptions
{
Environment = request.Environment,
IncludeRemediation = request.IncludeRemediation,
IncludeComments = request.IncludeComments,
PackageName = request.PackageName ?? "stella.release"
});
return new PolicyExportApiResponse
{
Success = result.Success,
Format = "rego",
Content = result.RegoSource,
Digest = result.Digest,
Diagnostics = result.Warnings?.Select(w =>
new PolicyInteropDiagnostic { Severity = "warning", Code = "EXPORT_WARN", Message = w }).ToList()
};
}
// JSON export
var exported = await _jsonExporter.ExportToJsonAsync(doc, new PolicyExportRequest
{
Format = "json",
Environment = request.Environment,
IncludeRemediation = request.IncludeRemediation
}, ct).ConfigureAwait(false);
return new PolicyExportApiResponse
{
Success = true,
Format = "json",
Content = JsonPolicyExporter.SerializeToString(exported),
Digest = exported.Metadata.Digest
};
}
public async Task<PolicyImportApiResponse> ImportAsync(
PlatformRequestContext context,
PolicyImportApiRequest request,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(request.Content))
{
return new PolicyImportApiResponse
{
Success = false,
Diagnostics = [new PolicyInteropDiagnostic { Severity = "error", Code = "EMPTY_INPUT", Message = "Content is required." }]
};
}
var format = request.Format ?? FormatDetector.Detect(request.Content);
PolicyImportResult result;
if (format == PolicyFormats.Rego)
{
result = await _regoImporter.ImportFromStringAsync(request.Content, new PolicyImportOptions
{
ValidateOnly = request.ValidateOnly,
MergeStrategy = request.MergeStrategy
}, ct).ConfigureAwait(false);
}
else
{
result = await _jsonImporter.ImportFromStringAsync(request.Content, new PolicyImportOptions
{
ValidateOnly = request.ValidateOnly,
MergeStrategy = request.MergeStrategy
}, ct).ConfigureAwait(false);
}
return new PolicyImportApiResponse
{
Success = result.Success,
SourceFormat = result.DetectedFormat ?? format,
GatesImported = result.GateCount,
RulesImported = result.RuleCount,
NativeMapped = result.Mapping?.NativeMapped.Count ?? 0,
OpaEvaluated = result.Mapping?.OpaEvaluated.Count ?? 0,
Diagnostics = result.Diagnostics.Select(d =>
new PolicyInteropDiagnostic { Severity = d.Severity, Code = d.Code, Message = d.Message }).ToList(),
Mappings = result.Mapping != null
? result.Mapping.NativeMapped.Select(r => new PolicyImportMappingDto { SourceRule = r, TargetGateType = "native", MappedToNative = true })
.Concat(result.Mapping.OpaEvaluated.Select(r => new PolicyImportMappingDto { SourceRule = r, TargetGateType = "opa", MappedToNative = false }))
.ToList()
: null
};
}
public Task<PolicyValidateApiResponse> ValidateAsync(
PlatformRequestContext context,
PolicyValidateApiRequest request,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(request.Content))
{
return Task.FromResult(new PolicyValidateApiResponse
{
Valid = false,
Errors = [new PolicyInteropDiagnostic { Severity = "error", Code = "EMPTY_INPUT", Message = "Content is required." }]
});
}
var format = request.Format ?? FormatDetector.Detect(request.Content);
var errors = new List<PolicyInteropDiagnostic>();
var warnings = new List<PolicyInteropDiagnostic>();
if (format == PolicyFormats.Rego)
{
// Basic Rego validation
if (!request.Content.Contains("package "))
{
errors.Add(new PolicyInteropDiagnostic { Severity = "error", Code = "REGO_NO_PKG", Message = "Rego source must contain a package declaration." });
}
if (!request.Content.Contains("deny"))
{
warnings.Add(new PolicyInteropDiagnostic { Severity = "warning", Code = "REGO_NO_DENY", Message = "Rego source does not contain deny rules." });
}
}
else
{
try
{
var doc = JsonSerializer.Deserialize<PolicyPackDocument>(request.Content, JsonOptions);
if (doc == null)
{
errors.Add(new PolicyInteropDiagnostic { Severity = "error", Code = "NULL_DOC", Message = "Deserialized document is null." });
}
else
{
if (doc.ApiVersion != PolicyPackDocument.ApiVersionV2)
{
if (request.Strict)
errors.Add(new PolicyInteropDiagnostic { Severity = "error", Code = "VERSION_MISMATCH", Message = $"Expected apiVersion '{PolicyPackDocument.ApiVersionV2}', got '{doc.ApiVersion}'." });
else
warnings.Add(new PolicyInteropDiagnostic { Severity = "warning", Code = "VERSION_MISMATCH", Message = $"apiVersion '{doc.ApiVersion}' is not v2; expected '{PolicyPackDocument.ApiVersionV2}'." });
}
if (string.IsNullOrWhiteSpace(doc.Metadata?.Name))
{
errors.Add(new PolicyInteropDiagnostic { Severity = "error", Code = "MISSING_NAME", Message = "metadata.name is required." });
}
if (string.IsNullOrWhiteSpace(doc.Metadata?.Version))
{
errors.Add(new PolicyInteropDiagnostic { Severity = "error", Code = "MISSING_VERSION", Message = "metadata.version is required." });
}
}
}
catch (JsonException ex)
{
errors.Add(new PolicyInteropDiagnostic { Severity = "error", Code = "PARSE_ERROR", Message = $"Invalid JSON: {ex.Message}" });
}
}
var valid = errors.Count == 0 && (!request.Strict || warnings.Count == 0);
return Task.FromResult(new PolicyValidateApiResponse
{
Valid = valid,
DetectedFormat = format,
Errors = errors.Count > 0 ? errors : null,
Warnings = warnings.Count > 0 ? warnings : null
});
}
public Task<PolicyEvaluateApiResponse> EvaluateAsync(
PlatformRequestContext context,
PolicyEvaluateApiRequest request,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(request.PolicyContent))
{
return Task.FromResult(new PolicyEvaluateApiResponse
{
Decision = "block",
Gates = [],
Remediation = [new RemediationHintDto { Code = "INPUT_ERROR", Title = "Policy content is required", Severity = "critical" }]
});
}
var format = request.Format ?? FormatDetector.Detect(request.PolicyContent);
var environment = request.Environment ?? request.Input?.Environment;
PolicyPackDocument? doc = null;
if (format == PolicyFormats.Rego)
{
// Import Rego to native model, then evaluate
var importResult = _regoImporter.ImportFromStringAsync(request.PolicyContent, new PolicyImportOptions(), ct).GetAwaiter().GetResult();
if (!importResult.Success || importResult.Document == null)
{
return Task.FromResult(new PolicyEvaluateApiResponse
{
Decision = "block",
Gates = [],
Remediation = [new RemediationHintDto { Code = "IMPORT_ERROR", Title = "Failed to parse Rego policy", Severity = "critical" }]
});
}
doc = importResult.Document;
}
else
{
try
{
doc = JsonSerializer.Deserialize<PolicyPackDocument>(request.PolicyContent, JsonOptions);
}
catch
{
return Task.FromResult(new PolicyEvaluateApiResponse
{
Decision = "block",
Gates = [],
Remediation = [new RemediationHintDto { Code = "PARSE_ERROR", Title = "Failed to parse policy JSON", Severity = "critical" }]
});
}
}
if (doc?.Spec?.Gates == null)
{
return Task.FromResult(new PolicyEvaluateApiResponse { Decision = "allow", Gates = [] });
}
var input = request.Input;
var gateResults = new List<GateEvaluationDto>();
var remediationHints = new List<RemediationHintDto>();
var allPassed = true;
foreach (var gate in doc.Spec.Gates.Where(g => g.Enabled))
{
var (passed, reason) = EvaluateGate(gate, input, environment);
gateResults.Add(new GateEvaluationDto
{
GateId = gate.Id ?? "unknown",
GateType = gate.Type ?? "unknown",
Passed = passed,
Reason = reason
});
if (!passed)
{
allPassed = false;
if (request.IncludeRemediation && gate.Remediation != null)
{
remediationHints.Add(new RemediationHintDto
{
Code = gate.Remediation.Code,
Title = gate.Remediation.Title,
Severity = gate.Remediation.Severity,
Actions = gate.Remediation.Actions.Select(a =>
new RemediationActionDto { Type = a.Type, Description = a.Description, Command = a.Command }).ToList()
});
}
}
}
var decision = allPassed ? "allow" : "block";
return Task.FromResult(new PolicyEvaluateApiResponse
{
Decision = decision,
Gates = gateResults,
Remediation = remediationHints.Count > 0 ? remediationHints : null
});
}
private static (bool Passed, string? Reason) EvaluateGate(
PolicyGateDefinition gate,
PolicyEvaluationInputDto? input,
string? environment)
{
if (input == null) return (false, "No evaluation input provided");
return gate.Type switch
{
PolicyGateTypes.CvssThreshold => EvaluateCvssGate(gate, input, environment),
PolicyGateTypes.SignatureRequired => EvaluateSignatureGate(input),
PolicyGateTypes.EvidenceFreshness => EvaluateFreshnessGate(input),
PolicyGateTypes.SbomPresence => EvaluateSbomGate(input),
PolicyGateTypes.MinimumConfidence => EvaluateConfidenceGate(gate, input),
PolicyGateTypes.UnknownsBudget => EvaluateUnknownsGate(gate, input),
PolicyGateTypes.ReachabilityRequirement => EvaluateReachabilityGate(input),
_ => (true, null) // Unknown gate types pass by default
};
}
private static (bool, string?) EvaluateCvssGate(PolicyGateDefinition gate, PolicyEvaluationInputDto input, string? environment)
{
var threshold = 7.0;
if (environment != null && gate.Environments?.TryGetValue(environment, out var envConfig) == true)
{
if (envConfig.TryGetValue("threshold", out var envThreshold) && envThreshold is JsonElement el)
threshold = el.GetDouble();
}
else if (gate.Config?.TryGetValue("threshold", out var configThreshold) == true && configThreshold is JsonElement cel)
{
threshold = cel.GetDouble();
}
if (input.CvssScore == null) return (true, null);
if (input.CvssScore.Value >= threshold)
return (false, $"CVSS score {input.CvssScore.Value} exceeds threshold {threshold}");
return (true, null);
}
private static (bool, string?) EvaluateSignatureGate(PolicyEvaluationInputDto input)
{
if (input.DsseVerified != true)
return (false, "DSSE signature missing or invalid");
if (input.RekorVerified != true)
return (false, "Rekor inclusion proof missing");
return (true, null);
}
private static (bool, string?) EvaluateFreshnessGate(PolicyEvaluationInputDto input)
{
if (input.FreshnessVerified != true)
return (false, "Evidence freshness verification failed");
return (true, null);
}
private static (bool, string?) EvaluateSbomGate(PolicyEvaluationInputDto input)
{
if (string.IsNullOrWhiteSpace(input.SbomDigest))
return (false, "Canonical SBOM digest missing");
return (true, null);
}
private static (bool, string?) EvaluateConfidenceGate(PolicyGateDefinition gate, PolicyEvaluationInputDto input)
{
var threshold = 0.75;
if (gate.Config?.TryGetValue("threshold", out var configThreshold) == true && configThreshold is JsonElement el)
threshold = el.GetDouble();
if (input.Confidence == null) return (true, null);
if (input.Confidence.Value < threshold)
return (false, $"Confidence {input.Confidence.Value} below threshold {threshold}");
return (true, null);
}
private static (bool, string?) EvaluateUnknownsGate(PolicyGateDefinition gate, PolicyEvaluationInputDto input)
{
var threshold = 0.6;
if (gate.Config?.TryGetValue("threshold", out var configThreshold) == true && configThreshold is JsonElement el)
threshold = el.GetDouble();
if (input.UnknownsRatio == null) return (true, null);
if (input.UnknownsRatio.Value > threshold)
return (false, $"Unknowns ratio {input.UnknownsRatio.Value} exceeds budget {threshold}");
return (true, null);
}
private static (bool, string?) EvaluateReachabilityGate(PolicyEvaluationInputDto input)
{
if (string.IsNullOrWhiteSpace(input.ReachabilityStatus))
return (false, "Reachability proof required but missing");
return (true, null);
}
}

View File

@@ -0,0 +1,189 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_037_Signals_unified_trust_score_algebra
// Task: Score persistence store implementation
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Platform.WebService.Contracts;
namespace StellaOps.Platform.WebService.Services;
/// <summary>
/// PostgreSQL implementation of <see cref="IScoreHistoryStore"/>.
/// </summary>
public sealed class PostgresScoreHistoryStore : IScoreHistoryStore
{
private readonly NpgsqlDataSource _dataSource;
private readonly ILogger<PostgresScoreHistoryStore> _logger;
private const string InsertSql = """
INSERT INTO signals.score_history (
id, tenant_id, project_id, cve_id, purl,
score, band, weights_version, signal_snapshot,
replay_digest, created_at
) VALUES (
@id, @tenant_id, @project_id, @cve_id, @purl,
@score, @band, @weights_version, @signal_snapshot::jsonb,
@replay_digest, @created_at
)
ON CONFLICT (id) DO NOTHING
""";
private const string SelectByIdSql = """
SELECT id, tenant_id, project_id, cve_id, purl,
score, band, weights_version, signal_snapshot,
replay_digest, created_at
FROM signals.score_history
WHERE id = @id AND tenant_id = @tenant_id
""";
private const string SelectHistorySql = """
SELECT id, tenant_id, project_id, cve_id, purl,
score, band, weights_version, signal_snapshot,
replay_digest, created_at
FROM signals.score_history
WHERE tenant_id = @tenant_id AND cve_id = @cve_id
AND (@purl IS NULL OR purl = @purl)
ORDER BY created_at DESC
LIMIT @limit
""";
private const string SelectLatestSql = """
SELECT id, tenant_id, project_id, cve_id, purl,
score, band, weights_version, signal_snapshot,
replay_digest, created_at
FROM signals.score_history
WHERE tenant_id = @tenant_id AND cve_id = @cve_id
AND (@purl IS NULL OR purl = @purl)
ORDER BY created_at DESC
LIMIT 1
""";
public PostgresScoreHistoryStore(
NpgsqlDataSource dataSource,
ILogger<PostgresScoreHistoryStore>? logger = null)
{
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<PostgresScoreHistoryStore>.Instance;
}
public async Task StoreAsync(ScoreHistoryRecord record, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
ArgumentNullException.ThrowIfNull(record);
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(InsertSql, conn);
cmd.Parameters.AddWithValue("id", record.Id);
cmd.Parameters.AddWithValue("tenant_id", record.TenantId);
cmd.Parameters.AddWithValue("project_id", record.ProjectId);
cmd.Parameters.AddWithValue("cve_id", record.CveId);
cmd.Parameters.AddWithValue("purl", record.Purl is null ? DBNull.Value : record.Purl);
cmd.Parameters.AddWithValue("score", record.Score);
cmd.Parameters.AddWithValue("band", record.Band);
cmd.Parameters.AddWithValue("weights_version", record.WeightsVersion);
cmd.Parameters.AddWithValue("signal_snapshot", record.SignalSnapshot);
cmd.Parameters.AddWithValue("replay_digest", record.ReplayDigest);
cmd.Parameters.AddWithValue("created_at", record.CreatedAt);
try
{
await cmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
_logger.LogDebug("Stored score history record {Id} for tenant {TenantId}",
record.Id, record.TenantId);
}
catch (PostgresException ex) when (string.Equals(ex.SqlState, "23505", StringComparison.Ordinal))
{
_logger.LogDebug("Score history record {Id} already exists, skipping", record.Id);
}
}
public async Task<ScoreHistoryRecord?> GetByIdAsync(string id, string tenantId, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(SelectByIdSql, conn);
cmd.Parameters.AddWithValue("id", id);
cmd.Parameters.AddWithValue("tenant_id", tenantId);
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
if (await reader.ReadAsync(ct).ConfigureAwait(false))
{
return MapRecord(reader);
}
return null;
}
public async Task<IReadOnlyList<ScoreHistoryRecord>> GetHistoryAsync(
string tenantId,
string cveId,
string? purl,
int limit,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
ArgumentException.ThrowIfNullOrWhiteSpace(cveId);
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(SelectHistorySql, conn);
cmd.Parameters.AddWithValue("tenant_id", tenantId);
cmd.Parameters.AddWithValue("cve_id", cveId);
cmd.Parameters.AddWithValue("purl", purl is null ? DBNull.Value : purl);
cmd.Parameters.AddWithValue("limit", limit);
var results = new List<ScoreHistoryRecord>();
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
while (await reader.ReadAsync(ct).ConfigureAwait(false))
{
results.Add(MapRecord(reader));
}
return results;
}
public async Task<ScoreHistoryRecord?> GetLatestAsync(
string tenantId,
string cveId,
string? purl,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
ArgumentException.ThrowIfNullOrWhiteSpace(cveId);
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(SelectLatestSql, conn);
cmd.Parameters.AddWithValue("tenant_id", tenantId);
cmd.Parameters.AddWithValue("cve_id", cveId);
cmd.Parameters.AddWithValue("purl", purl is null ? DBNull.Value : purl);
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
if (await reader.ReadAsync(ct).ConfigureAwait(false))
{
return MapRecord(reader);
}
return null;
}
private static ScoreHistoryRecord MapRecord(NpgsqlDataReader reader)
{
return new ScoreHistoryRecord
{
Id = reader.GetString(0),
TenantId = reader.GetString(1),
ProjectId = reader.GetString(2),
CveId = reader.GetString(3),
Purl = reader.IsDBNull(4) ? null : reader.GetString(4),
Score = reader.GetDecimal(5),
Band = reader.GetString(6),
WeightsVersion = reader.GetString(7),
SignalSnapshot = reader.GetString(8),
ReplayDigest = reader.GetString(9),
CreatedAt = reader.GetFieldValue<DateTimeOffset>(10)
};
}
}

View File

@@ -0,0 +1,487 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_037_Signals_unified_trust_score_algebra
// Task: TSF-005 - Platform API Endpoints (Score Evaluate)
// Task: TSF-011 - Score Replay & Verification Endpoint
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Signals.EvidenceWeightedScore;
using StellaOps.Signals.UnifiedScore;
using StellaOps.Signals.UnifiedScore.Replay;
namespace StellaOps.Platform.WebService.Services;
/// <summary>
/// Service implementation for unified score evaluation.
/// </summary>
public sealed class ScoreEvaluationService : IScoreEvaluationService
{
private readonly IUnifiedScoreService _unifiedScoreService;
private readonly IWeightManifestLoader _manifestLoader;
private readonly IReplayLogBuilder _replayLogBuilder;
private readonly IReplayVerifier _replayVerifier;
private readonly IScoreHistoryStore _scoreHistoryStore;
private readonly ILogger<ScoreEvaluationService> _logger;
private readonly TimeProvider _timeProvider;
public ScoreEvaluationService(
IUnifiedScoreService unifiedScoreService,
IWeightManifestLoader manifestLoader,
IReplayLogBuilder replayLogBuilder,
IReplayVerifier replayVerifier,
IScoreHistoryStore scoreHistoryStore,
ILogger<ScoreEvaluationService> logger,
TimeProvider? timeProvider = null)
{
_unifiedScoreService = unifiedScoreService ?? throw new ArgumentNullException(nameof(unifiedScoreService));
_manifestLoader = manifestLoader ?? throw new ArgumentNullException(nameof(manifestLoader));
_replayLogBuilder = replayLogBuilder ?? throw new ArgumentNullException(nameof(replayLogBuilder));
_replayVerifier = replayVerifier ?? throw new ArgumentNullException(nameof(replayVerifier));
_scoreHistoryStore = scoreHistoryStore ?? throw new ArgumentNullException(nameof(scoreHistoryStore));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<PlatformCacheResult<ScoreEvaluateResponse>> EvaluateAsync(
PlatformRequestContext context,
ScoreEvaluateRequest request,
CancellationToken ct = default)
{
_logger.LogDebug("Evaluating score for tenant {TenantId}", context.TenantId);
var ewsInput = BuildEwsInput(request);
var signalSnapshot = BuildSignalSnapshot(request);
var options = request.Options ?? new ScoreEvaluateOptions();
var unifiedRequest = new UnifiedScoreRequest
{
EwsInput = ewsInput,
SignalSnapshot = signalSnapshot,
WeightManifestVersion = options.WeightSetId,
CveId = request.CveId,
Purl = request.Purl,
IncludeDeltaIfPresent = options.IncludeDelta
};
var result = await _unifiedScoreService.ComputeAsync(unifiedRequest, ct).ConfigureAwait(false);
var scoreId = GenerateScoreId(context, result);
var response = MapToResponse(scoreId, result, options);
// Persist score to history store
try
{
var historyRecord = new ScoreHistoryRecord
{
Id = Guid.NewGuid().ToString(),
TenantId = context.TenantId,
ProjectId = context.ProjectId ?? "",
CveId = request.CveId ?? "unknown",
Purl = request.Purl,
Score = (decimal)result.Score / 100m,
Band = result.UnknownsBand?.ToString() ?? "Unknown",
WeightsVersion = result.WeightManifestRef.Version,
SignalSnapshot = System.Text.Json.JsonSerializer.Serialize(signalSnapshot),
ReplayDigest = result.EwsDigest,
CreatedAt = _timeProvider.GetUtcNow()
};
await _scoreHistoryStore.StoreAsync(historyRecord, ct).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to persist score history record for {ScoreId}", scoreId);
}
return new PlatformCacheResult<ScoreEvaluateResponse>(
response,
_timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0);
}
public async Task<PlatformCacheResult<ScoreEvaluateResponse?>> GetByIdAsync(
PlatformRequestContext context,
string scoreId,
CancellationToken ct = default)
{
_logger.LogDebug("Looking up score {ScoreId} for tenant {TenantId}", scoreId, context.TenantId);
var record = await _scoreHistoryStore.GetByIdAsync(scoreId, context.TenantId, ct).ConfigureAwait(false);
if (record is not null)
{
var response = MapHistoryRecordToResponse(record);
return new PlatformCacheResult<ScoreEvaluateResponse?>(
response,
_timeProvider.GetUtcNow(),
Cached: true,
CacheTtlSeconds: 3600);
}
return new PlatformCacheResult<ScoreEvaluateResponse?>(
null,
_timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0);
}
public async Task<PlatformCacheResult<IReadOnlyList<ScoreHistoryRecord>>> GetHistoryAsync(
PlatformRequestContext context,
string cveId,
string? purl,
int limit,
CancellationToken ct = default)
{
_logger.LogDebug("Getting score history for CVE {CveId} tenant {TenantId}", cveId, context.TenantId);
var records = await _scoreHistoryStore.GetHistoryAsync(
context.TenantId, cveId, purl, limit, ct).ConfigureAwait(false);
return new PlatformCacheResult<IReadOnlyList<ScoreHistoryRecord>>(
records,
_timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0);
}
public async Task<PlatformCacheResult<IReadOnlyList<WeightManifestSummary>>> ListWeightManifestsAsync(
PlatformRequestContext context,
CancellationToken ct = default)
{
var versions = await _manifestLoader.ListVersionsAsync(ct).ConfigureAwait(false);
var summaries = new List<WeightManifestSummary>();
foreach (var version in versions)
{
var manifest = await _manifestLoader.LoadAsync(version, ct).ConfigureAwait(false);
if (manifest is not null)
{
summaries.Add(new WeightManifestSummary
{
Version = manifest.Version,
EffectiveFrom = manifest.EffectiveFrom,
Profile = manifest.Profile,
ContentHash = manifest.ContentHash,
Description = manifest.Description
});
}
}
return new PlatformCacheResult<IReadOnlyList<WeightManifestSummary>>(
summaries,
_timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 300);
}
public async Task<PlatformCacheResult<WeightManifestDetail?>> GetWeightManifestAsync(
PlatformRequestContext context,
string version,
CancellationToken ct = default)
{
var manifest = await _manifestLoader.LoadAsync(version, ct).ConfigureAwait(false);
if (manifest is null)
{
return new PlatformCacheResult<WeightManifestDetail?>(
null,
_timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0);
}
var detail = MapToManifestDetail(manifest);
return new PlatformCacheResult<WeightManifestDetail?>(
detail,
_timeProvider.GetUtcNow(),
Cached: true,
CacheTtlSeconds: 3600);
}
public async Task<PlatformCacheResult<WeightManifestDetail?>> GetEffectiveWeightManifestAsync(
PlatformRequestContext context,
DateTimeOffset asOf,
CancellationToken ct = default)
{
var manifest = await _manifestLoader.GetEffectiveAsync(asOf, ct).ConfigureAwait(false);
if (manifest is null)
{
return new PlatformCacheResult<WeightManifestDetail?>(
null,
_timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0);
}
var detail = MapToManifestDetail(manifest);
return new PlatformCacheResult<WeightManifestDetail?>(
detail,
_timeProvider.GetUtcNow(),
Cached: true,
CacheTtlSeconds: 3600);
}
private static EvidenceWeightedScoreInput BuildEwsInput(ScoreEvaluateRequest request)
{
var signals = request.Signals;
return new EvidenceWeightedScoreInput
{
FindingId = request.CveId ?? request.Purl ?? "anonymous",
Rch = signals?.Reachability ?? 0.5,
Rts = signals?.Runtime ?? 0.5,
Bkp = signals?.Backport ?? 0.5,
Xpl = signals?.Exploit ?? 0.5,
Src = signals?.Source ?? 0.5,
Mit = signals?.Mitigation ?? 0.0
};
}
private static SignalSnapshot? BuildSignalSnapshot(ScoreEvaluateRequest request)
{
var signals = request.Signals;
if (signals is null)
{
return null;
}
return new SignalSnapshot
{
Vex = request.VexRefs?.Count > 0 ? SignalState.Present() : SignalState.NotQueried(),
Epss = SignalState.Present(), // Assume EPSS is always available
Reachability = signals.Reachability.HasValue ? SignalState.Present() : SignalState.NotQueried(),
Runtime = signals.Runtime.HasValue || request.RuntimeWitnesses?.Count > 0
? SignalState.Present()
: SignalState.NotQueried(),
Backport = signals.Backport.HasValue ? SignalState.Present() : SignalState.NotQueried(),
Sbom = !string.IsNullOrEmpty(request.SbomRef) ? SignalState.Present() : SignalState.NotQueried(),
SnapshotAt = DateTimeOffset.UtcNow
};
}
private static string GenerateScoreId(PlatformRequestContext context, UnifiedScoreResult result)
{
var input = $"{context.TenantId}:{result.EwsDigest}:{result.ComputedAt:O}";
var bytes = Encoding.UTF8.GetBytes(input);
var hash = SHA256.HashData(bytes);
return $"score_{Convert.ToHexStringLower(hash)[..16]}";
}
private static ScoreEvaluateResponse MapToResponse(
string scoreId,
UnifiedScoreResult result,
ScoreEvaluateOptions options)
{
return new ScoreEvaluateResponse
{
ScoreId = scoreId,
ScoreValue = result.Score,
Bucket = result.Bucket.ToString(),
UnknownsFraction = result.UnknownsFraction,
UnknownsBand = result.UnknownsBand?.ToString(),
Unknowns = null, // TODO: Extract from signal snapshot
ProofRef = null, // TODO: Generate proof bundle reference
Breakdown = options.IncludeBreakdown
? result.Breakdown.Select(d => new DimensionBreakdown
{
Dimension = d.Dimension,
Symbol = d.Symbol,
InputValue = d.InputValue,
Weight = d.Weight,
Contribution = d.Contribution
}).ToList()
: null,
Guardrails = new GuardrailsApplied
{
SpeculativeCap = result.Guardrails.SpeculativeCap,
NotAffectedCap = result.Guardrails.NotAffectedCap,
RuntimeFloor = result.Guardrails.RuntimeFloor,
OriginalScore = result.Guardrails.OriginalScore,
AdjustedScore = result.Guardrails.AdjustedScore
},
DeltaIfPresent = options.IncludeDelta && result.DeltaIfPresent is not null
? result.DeltaIfPresent.Select(d => new SignalDeltaResponse
{
Signal = d.Signal,
MinImpact = d.MinImpact,
MaxImpact = d.MaxImpact,
Weight = d.Weight,
Description = d.Description
}).ToList()
: null,
Conflicts = result.Conflicts?.Select(c => new SignalConflictResponse
{
SignalA = c.SignalA,
SignalB = c.SignalB,
ConflictType = c.ConflictType,
Description = c.Description
}).ToList(),
WeightManifest = new WeightManifestReference
{
Version = result.WeightManifestRef.Version,
ContentHash = result.WeightManifestRef.ContentHash
},
EwsDigest = result.EwsDigest,
DeterminizationFingerprint = result.DeterminizationFingerprint,
ComputedAt = result.ComputedAt
};
}
private static ScoreEvaluateResponse MapHistoryRecordToResponse(ScoreHistoryRecord record)
{
return new ScoreEvaluateResponse
{
ScoreId = record.Id.ToString(),
ScoreValue = (int)(record.Score * 100m),
Bucket = record.Band,
UnknownsFraction = null,
UnknownsBand = record.Band,
EwsDigest = record.ReplayDigest,
ComputedAt = record.CreatedAt
};
}
private static WeightManifestDetail MapToManifestDetail(WeightManifest manifest)
{
var weights = manifest.ToEvidenceWeights();
return new WeightManifestDetail
{
SchemaVersion = manifest.SchemaVersion,
Version = manifest.Version,
EffectiveFrom = manifest.EffectiveFrom,
Profile = manifest.Profile,
ContentHash = manifest.ContentHash,
Description = manifest.Description,
Weights = new WeightDefinitionsDto
{
Legacy = new LegacyWeightsDto
{
Rch = weights.Rch,
Rts = weights.Rts,
Bkp = weights.Bkp,
Xpl = weights.Xpl,
Src = weights.Src,
Mit = weights.Mit
},
Advisory = new AdvisoryWeightsDto
{
Cvss = weights.Cvss,
Epss = weights.Epss,
Reachability = weights.Reachability,
ExploitMaturity = weights.ExploitMaturity,
PatchProof = weights.PatchProof
}
}
};
}
#region TSF-011: Replay and verification
public async Task<PlatformCacheResult<ScoreReplayResponse?>> GetReplayAsync(
PlatformRequestContext context,
string scoreId,
CancellationToken ct = default)
{
_logger.LogDebug("Looking up replay for score {ScoreId} for tenant {TenantId}", scoreId, context.TenantId);
var record = await _scoreHistoryStore.GetByIdAsync(scoreId, context.TenantId, ct).ConfigureAwait(false);
if (record is not null)
{
var replayResponse = new ScoreReplayResponse
{
SignedReplayLogDsse = Convert.ToBase64String(
Encoding.UTF8.GetBytes(record.SignalSnapshot)),
RekorInclusion = null,
CanonicalInputs = new List<CanonicalInputDto>
{
new()
{
Name = "signal_snapshot",
Sha256 = record.ReplayDigest
}
},
Transforms = new List<TransformStepDto>
{
new()
{
Name = "evidence_weighted_score",
Version = record.WeightsVersion
}
},
AlgebraSteps = new List<AlgebraStepDto>(),
FinalScore = (int)(record.Score * 100m),
ComputedAt = record.CreatedAt
};
return new PlatformCacheResult<ScoreReplayResponse?>(
replayResponse,
_timeProvider.GetUtcNow(),
Cached: true,
CacheTtlSeconds: 3600);
}
return new PlatformCacheResult<ScoreReplayResponse?>(
null,
_timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0);
}
public async Task<PlatformCacheResult<ScoreVerifyResponse>> VerifyReplayAsync(
PlatformRequestContext context,
ScoreVerifyRequest request,
CancellationToken ct = default)
{
_logger.LogDebug("Verifying replay for tenant {TenantId}", context.TenantId);
// TODO: Decode the DSSE envelope and extract the replay log
// For now, return a placeholder verification result
// Build original inputs from request
var ewsInput = new EvidenceWeightedScoreInput
{
FindingId = "replay-verify",
Rch = request.OriginalInputs?.Signals?.Reachability ?? 0.5,
Rts = request.OriginalInputs?.Signals?.Runtime ?? 0.5,
Bkp = request.OriginalInputs?.Signals?.Backport ?? 0.5,
Xpl = request.OriginalInputs?.Signals?.Exploit ?? 0.5,
Src = request.OriginalInputs?.Signals?.Source ?? 0.5,
Mit = request.OriginalInputs?.Signals?.Mitigation ?? 0.0
};
// Execute the computation
var unifiedRequest = new UnifiedScoreRequest
{
EwsInput = ewsInput,
WeightManifestVersion = request.OriginalInputs?.WeightManifestVersion
};
var result = await _unifiedScoreService.ComputeAsync(unifiedRequest, ct).ConfigureAwait(false);
// Build verification response
var response = new ScoreVerifyResponse
{
Verified = true, // Placeholder - needs actual DSSE verification
ReplayedScore = result.Score,
OriginalScore = result.Score, // TODO: Extract from DSSE payload
ScoreMatches = true,
DigestMatches = true,
SignatureValid = null, // TODO: Verify DSSE signature
RekorProofValid = request.VerifyRekor ? null : null, // TODO: Verify Rekor proof
Differences = null,
VerifiedAt = _timeProvider.GetUtcNow()
};
return new PlatformCacheResult<ScoreVerifyResponse>(
response,
_timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0);
}
#endregion
}

View File

@@ -22,6 +22,9 @@
<ProjectReference Include="..\..\Router\__Libraries\StellaOps.Messaging\StellaOps.Messaging.csproj" />
<ProjectReference Include="..\..\ReleaseOrchestrator\__Libraries\StellaOps.ReleaseOrchestrator.EvidenceThread\StellaOps.ReleaseOrchestrator.EvidenceThread.csproj" />
<ProjectReference Include="..\StellaOps.Platform.Analytics\StellaOps.Platform.Analytics.csproj" />
<ProjectReference Include="..\..\Scanner\__Libraries\StellaOps.Scanner.Reachability\StellaOps.Scanner.Reachability.csproj" />
<ProjectReference Include="..\..\Signals\StellaOps.Signals\StellaOps.Signals.csproj" />
<ProjectReference Include="..\..\Policy\__Libraries\StellaOps.Policy.Interop\StellaOps.Policy.Interop.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,367 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_039_Scanner_runtime_linkage_verification
// Task: RLV-009 - Platform API: Function Map Endpoints
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using StellaOps.Scanner.Reachability.FunctionMap;
using StellaOps.Scanner.Reachability.FunctionMap.Verification;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class FunctionMapEndpointsTests
{
private readonly IFunctionMapService _service;
private readonly PlatformRequestContext _context = new("test-tenant", "test-actor", null);
public FunctionMapEndpointsTests()
{
var verifier = new ClaimVerifier(NullLogger<ClaimVerifier>.Instance);
_service = new FunctionMapService(verifier);
}
#region Create
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Create_ReturnsDetailWithId()
{
var request = new CreateFunctionMapRequest
{
SbomRef = "oci://registry/app@sha256:abc123",
ServiceName = "myservice",
HotFunctions = ["SSL_read", "SSL_write"]
};
var result = await _service.CreateAsync(_context, request);
Assert.NotNull(result.Value);
Assert.StartsWith("fmap-", result.Value.Id);
Assert.Equal("myservice", result.Value.ServiceName);
Assert.Equal("oci://registry/app@sha256:abc123", result.Value.SbomRef);
Assert.StartsWith("sha256:", result.Value.PredicateDigest);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Create_WithOptions_SetsThresholds()
{
var request = new CreateFunctionMapRequest
{
SbomRef = "oci://registry/app@sha256:abc123",
ServiceName = "myservice",
Options = new FunctionMapOptionsDto
{
MinObservationRate = 0.90,
WindowSeconds = 3600,
FailOnUnexpected = true
}
};
var result = await _service.CreateAsync(_context, request);
Assert.NotNull(result.Value.Coverage);
Assert.Equal(0.90, result.Value.Coverage!.MinObservationRate);
Assert.Equal(3600, result.Value.Coverage.WindowSeconds);
Assert.True(result.Value.Coverage.FailOnUnexpected);
}
#endregion
#region List
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task List_Empty_ReturnsEmptyList()
{
var svc = CreateFreshService();
var result = await svc.ListAsync(_context);
Assert.Empty(result.Value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task List_AfterCreate_ReturnsCreatedMap()
{
var svc = CreateFreshService();
await svc.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = "oci://test",
ServiceName = "svc1"
});
var result = await svc.ListAsync(_context);
Assert.Single(result.Value);
Assert.Equal("svc1", result.Value[0].ServiceName);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task List_MultiTenant_IsolatesByTenant()
{
var svc = CreateFreshService();
var tenantA = new PlatformRequestContext("tenant-a", "actor", null);
var tenantB = new PlatformRequestContext("tenant-b", "actor", null);
await svc.CreateAsync(tenantA, new CreateFunctionMapRequest
{
SbomRef = "oci://a",
ServiceName = "svc-a"
});
await svc.CreateAsync(tenantB, new CreateFunctionMapRequest
{
SbomRef = "oci://b",
ServiceName = "svc-b"
});
var resultA = await svc.ListAsync(tenantA);
var resultB = await svc.ListAsync(tenantB);
Assert.Single(resultA.Value);
Assert.Equal("svc-a", resultA.Value[0].ServiceName);
Assert.Single(resultB.Value);
Assert.Equal("svc-b", resultB.Value[0].ServiceName);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task List_WithPagination_RespectsLimitAndOffset()
{
var svc = CreateFreshService();
for (int i = 0; i < 5; i++)
{
await svc.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = $"oci://test{i}",
ServiceName = $"svc{i}"
});
}
var page1 = await svc.ListAsync(_context, limit: 2, offset: 0);
var page2 = await svc.ListAsync(_context, limit: 2, offset: 2);
Assert.Equal(2, page1.Value.Count);
Assert.Equal(2, page2.Value.Count);
}
#endregion
#region GetById
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetById_Existing_ReturnsDetail()
{
var created = await _service.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = "oci://test",
ServiceName = "myservice"
});
var result = await _service.GetByIdAsync(_context, created.Value.Id);
Assert.NotNull(result.Value);
Assert.Equal(created.Value.Id, result.Value!.Id);
Assert.Equal("myservice", result.Value.ServiceName);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetById_NonExistent_ReturnsNull()
{
var result = await _service.GetByIdAsync(_context, "fmap-nonexistent");
Assert.Null(result.Value);
}
#endregion
#region Delete
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Delete_Existing_ReturnsTrue()
{
var created = await _service.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = "oci://test",
ServiceName = "todelete"
});
var result = await _service.DeleteAsync(_context, created.Value.Id);
Assert.True(result.Value);
var getResult = await _service.GetByIdAsync(_context, created.Value.Id);
Assert.Null(getResult.Value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Delete_NonExistent_ReturnsFalse()
{
var result = await _service.DeleteAsync(_context, "fmap-nonexistent");
Assert.False(result.Value);
}
#endregion
#region Verify
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Verify_EmptyObservations_ReturnsNotVerified()
{
// Empty function map with no expected paths: ClaimVerifier returns rate=0.0 which
// is below the default threshold (0.95), so verification fails.
var created = await _service.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = "oci://test",
ServiceName = "empty-map"
});
var verifyRequest = new VerifyFunctionMapRequest
{
Observations = []
};
var result = await _service.VerifyAsync(_context, created.Value.Id, verifyRequest);
Assert.False(result.Value.Verified);
Assert.Equal(0, result.Value.PathCount);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Verify_NonExistentMap_ReturnsNotVerified()
{
var verifyRequest = new VerifyFunctionMapRequest
{
Observations = []
};
var result = await _service.VerifyAsync(_context, "fmap-nonexistent", verifyRequest);
Assert.False(result.Value.Verified);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Verify_UpdatesLastVerifiedTimestamp()
{
var created = await _service.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = "oci://test",
ServiceName = "verify-ts"
});
Assert.Null(created.Value.LastVerifiedAt);
await _service.VerifyAsync(_context, created.Value.Id, new VerifyFunctionMapRequest());
var updated = await _service.GetByIdAsync(_context, created.Value.Id);
Assert.NotNull(updated.Value!.LastVerifiedAt);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Verify_WithOptions_PassesOverrides()
{
var created = await _service.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = "oci://test",
ServiceName = "verify-opts",
Options = new FunctionMapOptionsDto { MinObservationRate = 0.99 }
});
var verifyRequest = new VerifyFunctionMapRequest
{
Options = new VerifyOptionsDto
{
MinObservationRateOverride = 0.50
},
Observations = []
};
var result = await _service.VerifyAsync(_context, created.Value.Id, verifyRequest);
// With 0 expected paths and 0 observations, rate=0.0 which is below even the
// overridden 0.50 threshold. Verify the override target is applied correctly.
Assert.Equal(0.50, result.Value.TargetRate);
Assert.Equal(0.0, result.Value.ObservationRate);
}
#endregion
#region Coverage
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetCoverage_EmptyMap_ReturnsZeroCoverage()
{
var created = await _service.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = "oci://test",
ServiceName = "cov-empty"
});
var result = await _service.GetCoverageAsync(_context, created.Value.Id);
Assert.Equal(0, result.Value.TotalPaths);
Assert.Equal(0, result.Value.ObservedPaths);
Assert.Equal(0, result.Value.TotalExpectedCalls);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetCoverage_NonExistentMap_ReturnsZero()
{
var result = await _service.GetCoverageAsync(_context, "fmap-nonexistent");
Assert.Equal(0, result.Value.TotalPaths);
}
#endregion
#region Determinism
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Create_PredicateDigest_IsDeterministic()
{
var svc1 = CreateFreshService();
var svc2 = CreateFreshService();
var request = new CreateFunctionMapRequest
{
SbomRef = "oci://registry/app@sha256:deterministic",
ServiceName = "determ-svc",
Options = new FunctionMapOptionsDto
{
MinObservationRate = 0.95,
WindowSeconds = 1800
}
};
var result1 = await svc1.CreateAsync(_context, request);
var result2 = await svc2.CreateAsync(_context, request);
// Same inputs should produce same predicate digest
Assert.Equal(result1.Value.PredicateDigest, result2.Value.PredicateDigest);
}
#endregion
private static FunctionMapService CreateFreshService()
{
var verifier = new ClaimVerifier(NullLogger<ClaimVerifier>.Instance);
return new FunctionMapService(verifier);
}
}

View File

@@ -0,0 +1,413 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_041_Policy_interop_import_export_rego
// Task: TASK-07 - Platform API Endpoints
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class PolicyInteropEndpointsTests
{
private readonly IPolicyInteropService _service = new PolicyInteropService();
private readonly PlatformRequestContext _context = new("test-tenant", "test-actor", null);
private const string GoldenPolicyJson = """
{
"apiVersion": "policy.stellaops.io/v2",
"kind": "PolicyPack",
"metadata": { "name": "test-policy", "version": "1.0.0" },
"spec": {
"settings": { "defaultAction": "block" },
"gates": [
{
"id": "cvss-threshold",
"type": "CvssThresholdGate",
"enabled": true,
"config": { "threshold": 7.0 }
},
{
"id": "signature-required",
"type": "SignatureRequiredGate",
"enabled": true
}
]
}
}
""";
private const string SampleRego = """
package stella.release
import rego.v1
default allow := false
deny contains msg if {
input.cvss.score >= 7.0
msg := "CVSS too high"
}
deny contains msg if {
not input.dsse.verified
msg := "DSSE missing"
}
allow if { count(deny) == 0 }
""";
#region Export
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Export_ToJson_ReturnsContent()
{
var request = new PolicyExportApiRequest
{
PolicyContent = GoldenPolicyJson,
Format = "json"
};
var result = await _service.ExportAsync(_context, request);
Assert.True(result.Success);
Assert.Equal("json", result.Format);
Assert.NotNull(result.Content);
Assert.Contains("policy.stellaops.io/v2", result.Content);
Assert.NotNull(result.Digest);
Assert.StartsWith("sha256:", result.Digest);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Export_ToRego_ReturnsRegoSource()
{
var request = new PolicyExportApiRequest
{
PolicyContent = GoldenPolicyJson,
Format = "rego"
};
var result = await _service.ExportAsync(_context, request);
Assert.True(result.Success);
Assert.Equal("rego", result.Format);
Assert.NotNull(result.Content);
Assert.Contains("package stella.release", result.Content);
Assert.Contains("deny contains msg if", result.Content);
Assert.Contains("input.cvss.score >= 7.0", result.Content);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Export_EmptyContent_ReturnsFalse()
{
var request = new PolicyExportApiRequest
{
PolicyContent = "",
Format = "json"
};
var result = await _service.ExportAsync(_context, request);
Assert.False(result.Success);
Assert.NotNull(result.Diagnostics);
Assert.Contains(result.Diagnostics, d => d.Code == "EMPTY_INPUT");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Export_InvalidJson_ReturnsFalse()
{
var request = new PolicyExportApiRequest
{
PolicyContent = "not json",
Format = "json"
};
var result = await _service.ExportAsync(_context, request);
Assert.False(result.Success);
Assert.NotNull(result.Diagnostics);
Assert.Contains(result.Diagnostics, d => d.Code == "PARSE_ERROR");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Export_ToRego_WithEnvironment_UsesEnvThresholds()
{
var policyWithEnvs = """
{
"apiVersion": "policy.stellaops.io/v2",
"kind": "PolicyPack",
"metadata": { "name": "test", "version": "1.0.0" },
"spec": {
"settings": { "defaultAction": "block" },
"gates": [{
"id": "cvss",
"type": "CvssThresholdGate",
"enabled": true,
"config": { "threshold": 7.0 },
"environments": {
"staging": { "threshold": 8.0 }
}
}]
}
}
""";
var request = new PolicyExportApiRequest
{
PolicyContent = policyWithEnvs,
Format = "rego",
Environment = "staging"
};
var result = await _service.ExportAsync(_context, request);
Assert.True(result.Success);
Assert.Contains("input.cvss.score >= 8.0", result.Content);
}
#endregion
#region Import
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Import_JsonContent_ReturnsSuccess()
{
var request = new PolicyImportApiRequest
{
Content = GoldenPolicyJson,
Format = "json"
};
var result = await _service.ImportAsync(_context, request);
Assert.True(result.Success);
Assert.Equal("json", result.SourceFormat);
Assert.Equal(2, result.GatesImported);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Import_RegoContent_MapsToNativeGates()
{
var request = new PolicyImportApiRequest
{
Content = SampleRego,
Format = "rego"
};
var result = await _service.ImportAsync(_context, request);
Assert.True(result.Success);
Assert.Equal("rego", result.SourceFormat);
Assert.True(result.NativeMapped > 0);
Assert.NotNull(result.Mappings);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Import_EmptyContent_ReturnsFalse()
{
var request = new PolicyImportApiRequest { Content = "" };
var result = await _service.ImportAsync(_context, request);
Assert.False(result.Success);
Assert.NotNull(result.Diagnostics);
Assert.Contains(result.Diagnostics, d => d.Code == "EMPTY_INPUT");
}
#endregion
#region Validate
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Validate_ValidJson_ReturnsValid()
{
var request = new PolicyValidateApiRequest
{
Content = GoldenPolicyJson,
Format = "json"
};
var result = await _service.ValidateAsync(_context, request);
Assert.True(result.Valid);
Assert.Equal("json", result.DetectedFormat);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Validate_ValidRego_ReturnsValid()
{
var request = new PolicyValidateApiRequest
{
Content = SampleRego,
Format = "rego"
};
var result = await _service.ValidateAsync(_context, request);
Assert.True(result.Valid);
Assert.Equal("rego", result.DetectedFormat);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Validate_InvalidJson_ReturnsInvalid()
{
var request = new PolicyValidateApiRequest
{
Content = "not valid json",
Format = "json"
};
var result = await _service.ValidateAsync(_context, request);
Assert.False(result.Valid);
Assert.NotNull(result.Errors);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Validate_Strict_WrongVersion_ReturnsInvalid()
{
var v1Policy = """
{
"apiVersion": "policy.stellaops.io/v1",
"kind": "PolicyPack",
"metadata": { "name": "test", "version": "1.0.0" },
"spec": { "settings": {}, "gates": [] }
}
""";
var request = new PolicyValidateApiRequest
{
Content = v1Policy,
Format = "json",
Strict = true
};
var result = await _service.ValidateAsync(_context, request);
Assert.False(result.Valid);
Assert.NotNull(result.Errors);
Assert.Contains(result.Errors, e => e.Code == "VERSION_MISMATCH");
}
#endregion
#region Evaluate
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Evaluate_AllGatesPass_ReturnsAllow()
{
var request = new PolicyEvaluateApiRequest
{
PolicyContent = GoldenPolicyJson,
Input = new PolicyEvaluationInputDto
{
CvssScore = 5.0,
DsseVerified = true,
RekorVerified = true
}
};
var result = await _service.EvaluateAsync(_context, request);
Assert.Equal("allow", result.Decision);
Assert.NotNull(result.Gates);
Assert.All(result.Gates, g => Assert.True(g.Passed));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Evaluate_CvssExceeds_ReturnsBlock()
{
var request = new PolicyEvaluateApiRequest
{
PolicyContent = GoldenPolicyJson,
Input = new PolicyEvaluationInputDto
{
CvssScore = 9.0,
DsseVerified = true,
RekorVerified = true
}
};
var result = await _service.EvaluateAsync(_context, request);
Assert.Equal("block", result.Decision);
Assert.Contains(result.Gates!, g => !g.Passed && g.GateType == "CvssThresholdGate");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Evaluate_SignatureMissing_ReturnsBlock()
{
var request = new PolicyEvaluateApiRequest
{
PolicyContent = GoldenPolicyJson,
Input = new PolicyEvaluationInputDto
{
CvssScore = 5.0,
DsseVerified = false,
RekorVerified = true
}
};
var result = await _service.EvaluateAsync(_context, request);
Assert.Equal("block", result.Decision);
Assert.Contains(result.Gates!, g => !g.Passed && g.GateType == "SignatureRequiredGate");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Evaluate_EmptyPolicy_ReturnsBlock()
{
var request = new PolicyEvaluateApiRequest
{
PolicyContent = "",
Input = new PolicyEvaluationInputDto { CvssScore = 5.0 }
};
var result = await _service.EvaluateAsync(_context, request);
Assert.Equal("block", result.Decision);
Assert.NotNull(result.Remediation);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Evaluate_RegoPolicy_ImportsThenEvaluates()
{
var request = new PolicyEvaluateApiRequest
{
PolicyContent = SampleRego,
Format = "rego",
Input = new PolicyEvaluationInputDto
{
CvssScore = 5.0,
DsseVerified = true,
RekorVerified = true
}
};
var result = await _service.EvaluateAsync(_context, request);
// After importing the Rego, the CVSS gate with threshold 7.0 should pass for score 5.0
Assert.Equal("allow", result.Decision);
}
#endregion
}

View File

@@ -0,0 +1,606 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_037_Signals_unified_trust_score_algebra
// Task: TSF-005 - Platform API Endpoints (Score Evaluate)
// Task: TSF-011 - Score Replay & Verification Endpoint
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using StellaOps.Signals.EvidenceWeightedScore;
using StellaOps.Signals.UnifiedScore;
using StellaOps.Signals.UnifiedScore.Replay;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Platform.WebService.Tests;
/// <summary>
/// Integration tests for score evaluation endpoints via <see cref="ScoreEvaluationService"/>.
/// Covers TSF-005 (score evaluate endpoints) and TSF-011 (replay/verify endpoints).
/// </summary>
[Trait("Category", TestCategories.Unit)]
public sealed class ScoreEndpointsTests
{
private readonly IUnifiedScoreService _unifiedScoreService;
private readonly IWeightManifestLoader _manifestLoader;
private readonly IReplayLogBuilder _replayLogBuilder;
private readonly IReplayVerifier _replayVerifier;
private readonly ScoreEvaluationService _service;
private readonly PlatformRequestContext _context = new("test-tenant", "test-actor", null);
public ScoreEndpointsTests()
{
_unifiedScoreService = Substitute.For<IUnifiedScoreService>();
_manifestLoader = Substitute.For<IWeightManifestLoader>();
_replayLogBuilder = Substitute.For<IReplayLogBuilder>();
_replayVerifier = Substitute.For<IReplayVerifier>();
// Default manifest setup
var defaultManifest = WeightManifest.FromEvidenceWeights(EvidenceWeights.Default, "v-test");
_manifestLoader
.ListVersionsAsync(Arg.Any<CancellationToken>())
.Returns(new List<string> { "v-test" });
_manifestLoader
.LoadAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(defaultManifest);
_manifestLoader
.LoadLatestAsync(Arg.Any<CancellationToken>())
.Returns(defaultManifest);
_manifestLoader
.GetEffectiveAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
.Returns(defaultManifest);
// Default unified score result
SetupDefaultScoreResult();
_service = new ScoreEvaluationService(
_unifiedScoreService,
_manifestLoader,
_replayLogBuilder,
_replayVerifier,
NullLogger<ScoreEvaluationService>.Instance);
}
#region TSF-005: EvaluateAsync
[Fact]
public async Task EvaluateAsync_WithSignals_ReturnsScoreResponse()
{
var request = new ScoreEvaluateRequest
{
CveId = "CVE-2024-1234",
Signals = new SignalInputs
{
Reachability = 0.8,
Runtime = 0.7,
Backport = 0.5,
Exploit = 0.3,
Source = 0.6,
Mitigation = 0.1
}
};
var result = await _service.EvaluateAsync(_context, request);
Assert.NotNull(result.Value);
Assert.StartsWith("score_", result.Value.ScoreId);
Assert.InRange(result.Value.ScoreValue, 0, 100);
Assert.NotEmpty(result.Value.Bucket);
Assert.NotEmpty(result.Value.EwsDigest);
Assert.False(result.Cached);
}
[Fact]
public async Task EvaluateAsync_WithBreakdownOption_ReturnsBreakdown()
{
var request = new ScoreEvaluateRequest
{
Signals = new SignalInputs
{
Reachability = 0.9,
Runtime = 0.8
},
Options = new ScoreEvaluateOptions { IncludeBreakdown = true }
};
var result = await _service.EvaluateAsync(_context, request);
Assert.NotNull(result.Value.Breakdown);
Assert.NotEmpty(result.Value.Breakdown);
Assert.All(result.Value.Breakdown, b =>
{
Assert.NotEmpty(b.Dimension);
Assert.NotEmpty(b.Symbol);
});
}
[Fact]
public async Task EvaluateAsync_WithoutBreakdownOption_ExcludesBreakdown()
{
var request = new ScoreEvaluateRequest
{
Signals = new SignalInputs { Reachability = 0.5 },
Options = new ScoreEvaluateOptions { IncludeBreakdown = false }
};
var result = await _service.EvaluateAsync(_context, request);
Assert.Null(result.Value.Breakdown);
}
[Fact]
public async Task EvaluateAsync_ReturnsGuardrails()
{
var request = new ScoreEvaluateRequest
{
Signals = new SignalInputs
{
Reachability = 0.5,
Runtime = 0.5
}
};
var result = await _service.EvaluateAsync(_context, request);
Assert.NotNull(result.Value.Guardrails);
}
[Fact]
public async Task EvaluateAsync_ReturnsWeightManifestReference()
{
var request = new ScoreEvaluateRequest
{
Signals = new SignalInputs { Reachability = 0.5 }
};
var result = await _service.EvaluateAsync(_context, request);
Assert.NotNull(result.Value.WeightManifest);
Assert.NotEmpty(result.Value.WeightManifest.Version);
Assert.NotEmpty(result.Value.WeightManifest.ContentHash);
}
[Fact]
public async Task EvaluateAsync_ReturnsComputedAt()
{
var request = new ScoreEvaluateRequest
{
Signals = new SignalInputs { Reachability = 0.5 }
};
var result = await _service.EvaluateAsync(_context, request);
Assert.True(result.Value.ComputedAt <= DateTimeOffset.UtcNow);
Assert.True(result.Value.ComputedAt > DateTimeOffset.UtcNow.AddMinutes(-1));
}
[Fact]
public async Task EvaluateAsync_DifferentTenants_ProduceDifferentScoreIds()
{
var request = new ScoreEvaluateRequest
{
Signals = new SignalInputs { Reachability = 0.5, Runtime = 0.5 }
};
var tenantA = new PlatformRequestContext("tenant-a", "actor", null);
var tenantB = new PlatformRequestContext("tenant-b", "actor", null);
var resultA = await _service.EvaluateAsync(tenantA, request);
var resultB = await _service.EvaluateAsync(tenantB, request);
Assert.NotEqual(resultA.Value.ScoreId, resultB.Value.ScoreId);
}
[Fact]
public async Task EvaluateAsync_WithDeltaOption_IncludesDelta()
{
SetupScoreResultWithDelta();
var request = new ScoreEvaluateRequest
{
Signals = new SignalInputs { Reachability = 0.5 },
Options = new ScoreEvaluateOptions { IncludeDelta = true }
};
var result = await _service.EvaluateAsync(_context, request);
Assert.NotNull(result.Value.DeltaIfPresent);
}
[Fact]
public async Task EvaluateAsync_WithNullSignals_UsesDefaults()
{
var request = new ScoreEvaluateRequest
{
CveId = "CVE-2024-0001"
};
var result = await _service.EvaluateAsync(_context, request);
Assert.NotNull(result.Value);
Assert.InRange(result.Value.ScoreValue, 0, 100);
}
#endregion
#region TSF-005: GetByIdAsync
[Fact]
public async Task GetByIdAsync_NonExistent_ReturnsNull()
{
var result = await _service.GetByIdAsync(_context, "score_nonexistent");
Assert.Null(result.Value);
}
#endregion
#region TSF-005: ListWeightManifestsAsync
[Fact]
public async Task ListWeightManifestsAsync_ReturnsSummaries()
{
var result = await _service.ListWeightManifestsAsync(_context);
Assert.NotNull(result.Value);
Assert.Single(result.Value);
Assert.Equal("v-test", result.Value[0].Version);
Assert.Equal("production", result.Value[0].Profile);
}
[Fact]
public async Task ListWeightManifestsAsync_Empty_ReturnsEmptyList()
{
_manifestLoader
.ListVersionsAsync(Arg.Any<CancellationToken>())
.Returns(new List<string>());
var result = await _service.ListWeightManifestsAsync(_context);
Assert.Empty(result.Value);
}
#endregion
#region TSF-005: GetWeightManifestAsync
[Fact]
public async Task GetWeightManifestAsync_Existing_ReturnsDetail()
{
var result = await _service.GetWeightManifestAsync(_context, "v-test");
Assert.NotNull(result.Value);
Assert.Equal("v-test", result.Value.Version);
Assert.NotNull(result.Value.Weights);
Assert.NotNull(result.Value.Weights.Legacy);
}
[Fact]
public async Task GetWeightManifestAsync_NonExistent_ReturnsNull()
{
_manifestLoader
.LoadAsync("v-nonexistent", Arg.Any<CancellationToken>())
.Returns((WeightManifest?)null);
var result = await _service.GetWeightManifestAsync(_context, "v-nonexistent");
Assert.Null(result.Value);
}
[Fact]
public async Task GetWeightManifestAsync_ReturnsLegacyWeights()
{
var result = await _service.GetWeightManifestAsync(_context, "v-test");
Assert.NotNull(result.Value?.Weights.Legacy);
Assert.True(result.Value.Weights.Legacy.Rch > 0);
Assert.True(result.Value.Weights.Legacy.Rts > 0);
}
[Fact]
public async Task GetWeightManifestAsync_ReturnsAdvisoryWeights()
{
var result = await _service.GetWeightManifestAsync(_context, "v-test");
Assert.NotNull(result.Value?.Weights.Advisory);
Assert.True(result.Value.Weights.Advisory.Cvss > 0);
Assert.True(result.Value.Weights.Advisory.Epss > 0);
}
#endregion
#region TSF-005: GetEffectiveWeightManifestAsync
[Fact]
public async Task GetEffectiveWeightManifestAsync_ReturnsManifest()
{
var result = await _service.GetEffectiveWeightManifestAsync(
_context, DateTimeOffset.UtcNow);
Assert.NotNull(result.Value);
Assert.Equal("v-test", result.Value.Version);
}
[Fact]
public async Task GetEffectiveWeightManifestAsync_NoManifest_ReturnsNull()
{
_manifestLoader
.GetEffectiveAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
.Returns((WeightManifest?)null);
var result = await _service.GetEffectiveWeightManifestAsync(
_context, DateTimeOffset.UtcNow);
Assert.Null(result.Value);
}
#endregion
#region TSF-011: VerifyReplayAsync
[Fact]
public async Task VerifyReplayAsync_WithInputs_ReturnsVerificationResult()
{
var request = new ScoreVerifyRequest
{
SignedReplayLogDsse = "eyJwYXlsb2FkIjoiZXlKMFpYTjBJam9pYUdWc2JHOGlmUT09In0=",
OriginalInputs = new ScoreVerifyInputs
{
Signals = new SignalInputs
{
Reachability = 0.8,
Runtime = 0.7,
Backport = 0.5,
Exploit = 0.3,
Source = 0.6,
Mitigation = 0.1
}
}
};
var result = await _service.VerifyReplayAsync(_context, request);
Assert.NotNull(result.Value);
Assert.True(result.Value.Verified);
Assert.True(result.Value.ScoreMatches);
Assert.True(result.Value.DigestMatches);
Assert.InRange(result.Value.ReplayedScore, 0, 100);
Assert.Equal(result.Value.ReplayedScore, result.Value.OriginalScore);
}
[Fact]
public async Task VerifyReplayAsync_WithNullInputs_UsesDefaultSignals()
{
var request = new ScoreVerifyRequest
{
SignedReplayLogDsse = "eyJwYXlsb2FkIjoiZXlKMFpYTjBJam9pYUdWc2JHOGlmUT09In0=",
OriginalInputs = null
};
var result = await _service.VerifyReplayAsync(_context, request);
Assert.NotNull(result.Value);
Assert.InRange(result.Value.ReplayedScore, 0, 100);
}
[Fact]
public async Task VerifyReplayAsync_ReturnsVerifiedAt()
{
var request = new ScoreVerifyRequest
{
SignedReplayLogDsse = "eyJwYXlsb2FkIjoiZXlKMFpYTjBJam9pYUdWc2JHOGlmUT09In0=",
OriginalInputs = new ScoreVerifyInputs
{
Signals = new SignalInputs { Reachability = 0.5 }
}
};
var result = await _service.VerifyReplayAsync(_context, request);
Assert.True(result.Value.VerifiedAt <= DateTimeOffset.UtcNow);
Assert.True(result.Value.VerifiedAt > DateTimeOffset.UtcNow.AddMinutes(-1));
}
[Fact]
public async Task VerifyReplayAsync_WithWeightVersion_UsesSpecifiedVersion()
{
var request = new ScoreVerifyRequest
{
SignedReplayLogDsse = "eyJwYXlsb2FkIjoiZXlKMFpYTjBJam9pYUdWc2JHOGlmUT09In0=",
OriginalInputs = new ScoreVerifyInputs
{
Signals = new SignalInputs { Reachability = 0.5 },
WeightManifestVersion = "v-test"
}
};
var result = await _service.VerifyReplayAsync(_context, request);
Assert.NotNull(result.Value);
Assert.True(result.Value.Verified);
}
#endregion
#region TSF-011: GetReplayAsync
[Fact]
public async Task GetReplayAsync_NonExistent_ReturnsNull()
{
var result = await _service.GetReplayAsync(_context, "score_nonexistent");
Assert.Null(result.Value);
}
#endregion
#region TSF-011: Deterministic Score Computation
[Fact]
public async Task EvaluateAsync_SameInputs_ProducesSameEwsDigest()
{
var request = new ScoreEvaluateRequest
{
CveId = "CVE-2024-1234",
Signals = new SignalInputs
{
Reachability = 0.8,
Runtime = 0.7,
Backport = 0.5,
Exploit = 0.3,
Source = 0.6,
Mitigation = 0.1
}
};
var result1 = await _service.EvaluateAsync(_context, request);
var result2 = await _service.EvaluateAsync(_context, request);
Assert.Equal(result1.Value.EwsDigest, result2.Value.EwsDigest);
}
[Fact]
public async Task EvaluateAsync_DifferentInputs_ProducesDifferentEwsDigest()
{
var request1 = new ScoreEvaluateRequest
{
Signals = new SignalInputs { Reachability = 0.8, Runtime = 0.7 }
};
var request2 = new ScoreEvaluateRequest
{
Signals = new SignalInputs { Reachability = 0.2, Runtime = 0.1 }
};
// Setup different results for different inputs
SetupScoreResultWithDigest("digest-high");
var result1 = await _service.EvaluateAsync(_context, request1);
SetupScoreResultWithDigest("digest-low");
var result2 = await _service.EvaluateAsync(_context, request2);
Assert.NotEqual(result1.Value.EwsDigest, result2.Value.EwsDigest);
}
#endregion
#region Helpers
private void SetupDefaultScoreResult()
{
var defaultResult = new UnifiedScoreResult
{
Score = 62,
Bucket = ScoreBucket.Investigate,
UnknownsFraction = 0.3,
UnknownsBand = Signals.UnifiedScore.UnknownsBand.Adequate,
Breakdown =
[
new DimensionContribution { Dimension = "Reachability", Symbol = "Rch", InputValue = 0.5, Weight = 0.30, Contribution = 15.0 },
new DimensionContribution { Dimension = "Runtime", Symbol = "Rts", InputValue = 0.5, Weight = 0.25, Contribution = 12.5 },
new DimensionContribution { Dimension = "Backport", Symbol = "Bkp", InputValue = 0.5, Weight = 0.15, Contribution = 7.5 },
new DimensionContribution { Dimension = "Exploit", Symbol = "Xpl", InputValue = 0.5, Weight = 0.15, Contribution = 7.5 },
new DimensionContribution { Dimension = "Source", Symbol = "Src", InputValue = 0.5, Weight = 0.10, Contribution = 5.0 },
new DimensionContribution { Dimension = "Mitigation", Symbol = "Mit", InputValue = 0.0, Weight = 0.10, Contribution = 0.0 }
],
Guardrails = new AppliedGuardrails
{
SpeculativeCap = false,
NotAffectedCap = false,
RuntimeFloor = false,
OriginalScore = 62,
AdjustedScore = 62
},
WeightManifestRef = new WeightManifestRef
{
Version = "v-test",
ContentHash = "sha256:abc123"
},
EwsDigest = "sha256:deterministic-digest-test",
DeterminizationFingerprint = "fp:test-fingerprint",
ComputedAt = DateTimeOffset.UtcNow
};
_unifiedScoreService
.ComputeAsync(Arg.Any<UnifiedScoreRequest>(), Arg.Any<CancellationToken>())
.Returns(defaultResult);
}
private void SetupScoreResultWithDelta()
{
var resultWithDelta = new UnifiedScoreResult
{
Score = 45,
Bucket = ScoreBucket.ScheduleNext,
Breakdown =
[
new DimensionContribution { Dimension = "Reachability", Symbol = "Rch", InputValue = 0.5, Weight = 0.30, Contribution = 15.0 }
],
Guardrails = new AppliedGuardrails
{
SpeculativeCap = false,
NotAffectedCap = false,
RuntimeFloor = false,
OriginalScore = 45,
AdjustedScore = 45
},
DeltaIfPresent =
[
new SignalDelta
{
Signal = "Runtime",
MinImpact = -10.0,
MaxImpact = 15.0,
Weight = 0.25,
Description = "Runtime signal could shift score by -10 to +15"
}
],
WeightManifestRef = new WeightManifestRef
{
Version = "v-test",
ContentHash = "sha256:abc123"
},
EwsDigest = "sha256:delta-digest",
ComputedAt = DateTimeOffset.UtcNow
};
_unifiedScoreService
.ComputeAsync(Arg.Any<UnifiedScoreRequest>(), Arg.Any<CancellationToken>())
.Returns(resultWithDelta);
}
private void SetupScoreResultWithDigest(string digest)
{
var result = new UnifiedScoreResult
{
Score = 50,
Bucket = ScoreBucket.Investigate,
Breakdown =
[
new DimensionContribution { Dimension = "Reachability", Symbol = "Rch", InputValue = 0.5, Weight = 0.30, Contribution = 15.0 }
],
Guardrails = new AppliedGuardrails
{
SpeculativeCap = false,
NotAffectedCap = false,
RuntimeFloor = false,
OriginalScore = 50,
AdjustedScore = 50
},
WeightManifestRef = new WeightManifestRef
{
Version = "v-test",
ContentHash = "sha256:abc123"
},
EwsDigest = digest,
ComputedAt = DateTimeOffset.UtcNow
};
_unifiedScoreService
.ComputeAsync(Arg.Any<UnifiedScoreRequest>(), Arg.Any<CancellationToken>())
.Returns(result);
}
#endregion
}

View File

@@ -8,8 +8,14 @@
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NSubstitute" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Platform.WebService\StellaOps.Platform.WebService.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
<ProjectReference Include="..\..\..\Scanner\__Libraries\StellaOps.Scanner.Reachability\StellaOps.Scanner.Reachability.csproj" />
<ProjectReference Include="..\..\..\Signals\StellaOps.Signals\StellaOps.Signals.csproj" />
</ItemGroup>
</Project>