partly or unimplemented features - now implemented

This commit is contained in:
master
2026-02-09 08:53:51 +02:00
parent 1bf6bbf395
commit 4bdc298ec1
674 changed files with 90194 additions and 2271 deletions

View File

@@ -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 { }