partly or unimplemented features - now implemented
This commit is contained in:
@@ -0,0 +1,529 @@
|
||||
// <copyright file="DeltaIfPresentEndpoints.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Sprint: SPRINT_20260208_043_Policy_delta_if_present_calculations_for_missing_signals (TSF-004)
|
||||
// </copyright>
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Policy.Determinization.Models;
|
||||
using StellaOps.Policy.Determinization.Scoring;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// API endpoints for delta-if-present calculations (TSF-004).
|
||||
/// Shows hypothetical score changes when missing signals are filled with assumed values.
|
||||
/// </summary>
|
||||
public static class DeltaIfPresentEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps delta-if-present endpoints.
|
||||
/// </summary>
|
||||
public static IEndpointRouteBuilder MapDeltaIfPresentEndpoints(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
var group = endpoints.MapGroup("/api/v1/policy/delta-if-present")
|
||||
.WithTags("Delta If Present")
|
||||
.WithOpenApi();
|
||||
|
||||
// Calculate single signal delta
|
||||
group.MapPost("/signal", CalculateSingleSignalDeltaAsync)
|
||||
.WithName("CalculateSingleSignalDelta")
|
||||
.WithSummary("Calculate hypothetical score change for a single signal")
|
||||
.WithDescription("Shows what the trust score would be if a specific missing signal had a particular value")
|
||||
.Produces<SingleSignalDeltaResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.RequireAuthorization("PolicyViewer");
|
||||
|
||||
// Calculate full gap analysis
|
||||
group.MapPost("/analysis", CalculateFullAnalysisAsync)
|
||||
.WithName("CalculateFullGapAnalysis")
|
||||
.WithSummary("Calculate full gap analysis for all missing signals")
|
||||
.WithDescription("Analyzes all signal gaps with best/worst/prior case scenarios and prioritization by impact")
|
||||
.Produces<FullAnalysisResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.RequireAuthorization("PolicyViewer");
|
||||
|
||||
// Calculate score bounds
|
||||
group.MapPost("/bounds", CalculateScoreBoundsAsync)
|
||||
.WithName("CalculateScoreBounds")
|
||||
.WithSummary("Calculate minimum and maximum possible scores")
|
||||
.WithDescription("Computes the range of possible trust scores given current gaps")
|
||||
.Produces<ScoreBoundsResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.RequireAuthorization("PolicyViewer");
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static IResult CalculateSingleSignalDeltaAsync(
|
||||
[FromBody] SingleSignalDeltaRequest request,
|
||||
IDeltaIfPresentCalculator calculator,
|
||||
ILogger<DeltaIfPresentEndpoints> logger)
|
||||
{
|
||||
if (request.Snapshot is null)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid request",
|
||||
Detail = "Snapshot is required"
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.SignalName))
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid request",
|
||||
Detail = "SignalName is required"
|
||||
});
|
||||
}
|
||||
|
||||
logger.LogDebug(
|
||||
"Calculating single signal delta for {Signal} with assumed value {Value}",
|
||||
request.SignalName,
|
||||
request.AssumedValue);
|
||||
|
||||
var result = calculator.CalculateSingleSignalDelta(
|
||||
request.Snapshot,
|
||||
request.SignalName,
|
||||
request.AssumedValue,
|
||||
request.CustomWeights);
|
||||
|
||||
return Results.Ok(new SingleSignalDeltaResponse
|
||||
{
|
||||
Signal = result.Signal,
|
||||
CurrentScore = result.CurrentScore,
|
||||
HypotheticalScore = result.HypotheticalScore,
|
||||
ScoreDelta = result.Delta,
|
||||
AssumedValue = result.AssumedValue,
|
||||
SignalWeight = result.SignalWeight,
|
||||
CurrentEntropy = result.CurrentEntropy,
|
||||
HypotheticalEntropy = result.HypotheticalEntropy,
|
||||
EntropyDelta = result.EntropyDelta
|
||||
});
|
||||
}
|
||||
|
||||
private static IResult CalculateFullAnalysisAsync(
|
||||
[FromBody] FullAnalysisRequest request,
|
||||
IDeltaIfPresentCalculator calculator,
|
||||
ILogger<DeltaIfPresentEndpoints> logger)
|
||||
{
|
||||
if (request.Snapshot is null)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid request",
|
||||
Detail = "Snapshot is required"
|
||||
});
|
||||
}
|
||||
|
||||
logger.LogDebug(
|
||||
"Calculating full gap analysis for CVE {Cve}, PURL {Purl}",
|
||||
request.Snapshot.Cve,
|
||||
request.Snapshot.Purl);
|
||||
|
||||
var analysis = calculator.CalculateFullAnalysis(request.Snapshot, request.CustomWeights);
|
||||
|
||||
var gaps = analysis.GapAnalysis.Select(g => new GapAnalysisItemResponse
|
||||
{
|
||||
Signal = g.BestCase.Signal,
|
||||
GapReason = g.GapReason.ToString(),
|
||||
BestCase = MapDeltaResult(g.BestCase),
|
||||
WorstCase = MapDeltaResult(g.WorstCase),
|
||||
PriorCase = MapDeltaResult(g.PriorCase),
|
||||
MaxImpact = g.MaxImpact
|
||||
}).ToList();
|
||||
|
||||
return Results.Ok(new FullAnalysisResponse
|
||||
{
|
||||
Cve = request.Snapshot.Cve,
|
||||
Purl = request.Snapshot.Purl,
|
||||
CurrentScore = analysis.CurrentScore,
|
||||
CurrentEntropy = analysis.CurrentEntropy,
|
||||
GapAnalysis = gaps,
|
||||
PrioritizedGaps = analysis.PrioritizedGaps.ToList(),
|
||||
ComputedAt = analysis.ComputedAt
|
||||
});
|
||||
}
|
||||
|
||||
private static IResult CalculateScoreBoundsAsync(
|
||||
[FromBody] ScoreBoundsRequest request,
|
||||
IDeltaIfPresentCalculator calculator,
|
||||
ILogger<DeltaIfPresentEndpoints> logger)
|
||||
{
|
||||
if (request.Snapshot is null)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid request",
|
||||
Detail = "Snapshot is required"
|
||||
});
|
||||
}
|
||||
|
||||
logger.LogDebug(
|
||||
"Calculating score bounds for CVE {Cve}, PURL {Purl}",
|
||||
request.Snapshot.Cve,
|
||||
request.Snapshot.Purl);
|
||||
|
||||
var bounds = calculator.CalculateScoreBounds(request.Snapshot, request.CustomWeights);
|
||||
|
||||
return Results.Ok(new ScoreBoundsResponse
|
||||
{
|
||||
Cve = request.Snapshot.Cve,
|
||||
Purl = request.Snapshot.Purl,
|
||||
CurrentScore = bounds.CurrentScore,
|
||||
CurrentEntropy = bounds.CurrentEntropy,
|
||||
MinimumScore = bounds.MinimumScore,
|
||||
MaximumScore = bounds.MaximumScore,
|
||||
Range = bounds.Range,
|
||||
GapCount = bounds.GapCount,
|
||||
MissingWeightPercentage = bounds.MissingWeightPercentage,
|
||||
ComputedAt = bounds.ComputedAt
|
||||
});
|
||||
}
|
||||
|
||||
private static DeltaResultResponse MapDeltaResult(DeltaIfPresentResult result)
|
||||
{
|
||||
return new DeltaResultResponse
|
||||
{
|
||||
AssumedValue = result.AssumedValue,
|
||||
HypotheticalScore = result.HypotheticalScore,
|
||||
ScoreDelta = result.Delta,
|
||||
HypotheticalEntropy = result.HypotheticalEntropy,
|
||||
EntropyDelta = result.EntropyDelta
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#region Request DTOs
|
||||
|
||||
/// <summary>
|
||||
/// Request to calculate delta for a single signal.
|
||||
/// </summary>
|
||||
public sealed record SingleSignalDeltaRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The current signal snapshot.
|
||||
/// </summary>
|
||||
[JsonPropertyName("snapshot")]
|
||||
public required SignalSnapshot Snapshot { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the signal to simulate (VEX, EPSS, Reachability, Runtime, Backport, SBOMLineage).
|
||||
/// </summary>
|
||||
[JsonPropertyName("signal_name")]
|
||||
public required string SignalName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The assumed value for the signal (0.0 to 1.0 where 0 = lowest risk, 1 = highest risk).
|
||||
/// </summary>
|
||||
[JsonPropertyName("assumed_value")]
|
||||
public double AssumedValue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional custom signal weights. If not provided, defaults are used.
|
||||
/// </summary>
|
||||
[JsonPropertyName("custom_weights")]
|
||||
public SignalWeights? CustomWeights { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to calculate full gap analysis.
|
||||
/// </summary>
|
||||
public sealed record FullAnalysisRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The current signal snapshot.
|
||||
/// </summary>
|
||||
[JsonPropertyName("snapshot")]
|
||||
public required SignalSnapshot Snapshot { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional custom signal weights. If not provided, defaults are used.
|
||||
/// </summary>
|
||||
[JsonPropertyName("custom_weights")]
|
||||
public SignalWeights? CustomWeights { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to calculate score bounds.
|
||||
/// </summary>
|
||||
public sealed record ScoreBoundsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The current signal snapshot.
|
||||
/// </summary>
|
||||
[JsonPropertyName("snapshot")]
|
||||
public required SignalSnapshot Snapshot { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional custom signal weights. If not provided, defaults are used.
|
||||
/// </summary>
|
||||
[JsonPropertyName("custom_weights")]
|
||||
public SignalWeights? CustomWeights { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Response DTOs
|
||||
|
||||
/// <summary>
|
||||
/// Response for single signal delta calculation.
|
||||
/// </summary>
|
||||
public sealed record SingleSignalDeltaResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the signal analyzed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("signal")]
|
||||
public required string Signal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current trust score.
|
||||
/// </summary>
|
||||
[JsonPropertyName("current_score")]
|
||||
public double CurrentScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hypothetical score if the signal had the assumed value.
|
||||
/// </summary>
|
||||
[JsonPropertyName("hypothetical_score")]
|
||||
public double HypotheticalScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Change in score (hypothetical - current).
|
||||
/// </summary>
|
||||
[JsonPropertyName("score_delta")]
|
||||
public double ScoreDelta { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The assumed value used for simulation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("assumed_value")]
|
||||
public double AssumedValue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Weight of the signal in scoring.
|
||||
/// </summary>
|
||||
[JsonPropertyName("signal_weight")]
|
||||
public double SignalWeight { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current entropy (uncertainty).
|
||||
/// </summary>
|
||||
[JsonPropertyName("current_entropy")]
|
||||
public double CurrentEntropy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hypothetical entropy after adding the signal.
|
||||
/// </summary>
|
||||
[JsonPropertyName("hypothetical_entropy")]
|
||||
public double HypotheticalEntropy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Change in entropy (negative = less uncertainty).
|
||||
/// </summary>
|
||||
[JsonPropertyName("entropy_delta")]
|
||||
public double EntropyDelta { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for full gap analysis.
|
||||
/// </summary>
|
||||
public sealed record FullAnalysisResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// CVE identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cve")]
|
||||
public required string Cve { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Package URL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("purl")]
|
||||
public required string Purl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current trust score.
|
||||
/// </summary>
|
||||
[JsonPropertyName("current_score")]
|
||||
public double CurrentScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current entropy (uncertainty).
|
||||
/// </summary>
|
||||
[JsonPropertyName("current_entropy")]
|
||||
public double CurrentEntropy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Analysis of each signal gap with best/worst/prior cases.
|
||||
/// </summary>
|
||||
[JsonPropertyName("gap_analysis")]
|
||||
public required IReadOnlyList<GapAnalysisItemResponse> GapAnalysis { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signals prioritized by maximum impact (highest first).
|
||||
/// </summary>
|
||||
[JsonPropertyName("prioritized_gaps")]
|
||||
public required IReadOnlyList<string> PrioritizedGaps { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when analysis was computed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("computed_at")]
|
||||
public DateTimeOffset ComputedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual gap analysis result.
|
||||
/// </summary>
|
||||
public sealed record GapAnalysisItemResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the signal.
|
||||
/// </summary>
|
||||
[JsonPropertyName("signal")]
|
||||
public required string Signal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for the gap.
|
||||
/// </summary>
|
||||
[JsonPropertyName("gap_reason")]
|
||||
public required string GapReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Best case scenario (lowest risk assumption).
|
||||
/// </summary>
|
||||
[JsonPropertyName("best_case")]
|
||||
public required DeltaResultResponse BestCase { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Worst case scenario (highest risk assumption).
|
||||
/// </summary>
|
||||
[JsonPropertyName("worst_case")]
|
||||
public required DeltaResultResponse WorstCase { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Prior case scenario (prior probability assumption).
|
||||
/// </summary>
|
||||
[JsonPropertyName("prior_case")]
|
||||
public required DeltaResultResponse PriorCase { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum possible score impact (worst - best).
|
||||
/// </summary>
|
||||
[JsonPropertyName("max_impact")]
|
||||
public double MaxImpact { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delta result for a specific scenario.
|
||||
/// </summary>
|
||||
public sealed record DeltaResultResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Assumed value for the signal.
|
||||
/// </summary>
|
||||
[JsonPropertyName("assumed_value")]
|
||||
public double AssumedValue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hypothetical score with assumed value.
|
||||
/// </summary>
|
||||
[JsonPropertyName("hypothetical_score")]
|
||||
public double HypotheticalScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Change in score.
|
||||
/// </summary>
|
||||
[JsonPropertyName("score_delta")]
|
||||
public double ScoreDelta { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hypothetical entropy with assumed value.
|
||||
/// </summary>
|
||||
[JsonPropertyName("hypothetical_entropy")]
|
||||
public double HypotheticalEntropy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Change in entropy.
|
||||
/// </summary>
|
||||
[JsonPropertyName("entropy_delta")]
|
||||
public double EntropyDelta { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for score bounds calculation.
|
||||
/// </summary>
|
||||
public sealed record ScoreBoundsResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// CVE identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cve")]
|
||||
public required string Cve { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Package URL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("purl")]
|
||||
public required string Purl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current trust score.
|
||||
/// </summary>
|
||||
[JsonPropertyName("current_score")]
|
||||
public double CurrentScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current entropy (uncertainty).
|
||||
/// </summary>
|
||||
[JsonPropertyName("current_entropy")]
|
||||
public double CurrentEntropy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum possible score (all gaps at best case).
|
||||
/// </summary>
|
||||
[JsonPropertyName("minimum_score")]
|
||||
public double MinimumScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum possible score (all gaps at worst case).
|
||||
/// </summary>
|
||||
[JsonPropertyName("maximum_score")]
|
||||
public double MaximumScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Range of possible scores.
|
||||
/// </summary>
|
||||
[JsonPropertyName("range")]
|
||||
public double Range { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of signal gaps.
|
||||
/// </summary>
|
||||
[JsonPropertyName("gap_count")]
|
||||
public int GapCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Percentage of total weight that is missing.
|
||||
/// </summary>
|
||||
[JsonPropertyName("missing_weight_percentage")]
|
||||
public double MissingWeightPercentage { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when bounds were computed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("computed_at")]
|
||||
public DateTimeOffset ComputedAt { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Logger interface for typed logging
|
||||
internal sealed class DeltaIfPresentEndpoints { }
|
||||
Reference in New Issue
Block a user