feat(metrics): Implement scan metrics repository and PostgreSQL integration
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added IScanMetricsRepository interface for scan metrics persistence and retrieval.
- Implemented PostgresScanMetricsRepository for PostgreSQL database interactions, including methods for saving and retrieving scan metrics and execution phases.
- Introduced methods for obtaining TTE statistics and recent scans for tenants.
- Implemented deletion of old metrics for retention purposes.

test(tests): Add SCA Failure Catalogue tests for FC6-FC10

- Created ScaCatalogueDeterminismTests to validate determinism properties of SCA Failure Catalogue fixtures.
- Developed ScaFailureCatalogueTests to ensure correct handling of specific failure modes in the scanner.
- Included tests for manifest validation, file existence, and expected findings across multiple failure cases.

feat(telemetry): Integrate scan completion metrics into the pipeline

- Introduced IScanCompletionMetricsIntegration interface and ScanCompletionMetricsIntegration class to record metrics upon scan completion.
- Implemented proof coverage and TTE metrics recording with logging for scan completion summaries.
This commit is contained in:
master
2025-12-16 14:00:35 +02:00
parent b55d9fa68d
commit 415eff1207
27 changed files with 3620 additions and 35 deletions

View File

@@ -0,0 +1,129 @@
// -----------------------------------------------------------------------------
// FreshnessAwareScoringService.cs
// Sprint: SPRINT_3401_0001_0001_determinism_scoring_foundations
// Task: DET-3401-003
// Description: Integrates freshness multiplier into evidence scoring pipeline
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Logging;
namespace StellaOps.Policy.Scoring;
/// <summary>
/// Integrates evidence freshness into the scoring pipeline.
/// </summary>
public interface IFreshnessAwareScoringService
{
/// <summary>
/// Applies freshness adjustment to a finding's score.
/// </summary>
/// <param name="baseScore">Original score (0-100 or 0-10 depending on scale).</param>
/// <param name="evidenceTimestamp">When the evidence was collected.</param>
/// <param name="evaluationTime">Time of evaluation (for deterministic replay).</param>
/// <returns>Adjusted score with explanation.</returns>
FreshnessAdjustedScore AdjustForFreshness(
int baseScore,
DateTimeOffset evidenceTimestamp,
DateTimeOffset evaluationTime);
/// <summary>
/// Gets the freshness bucket for a given evidence age.
/// </summary>
FreshnessBucketResult GetFreshnessBucket(
DateTimeOffset evidenceTimestamp,
DateTimeOffset evaluationTime);
}
/// <summary>
/// Result of freshness adjustment with explanation data.
/// </summary>
/// <param name="OriginalScore">The score before freshness adjustment.</param>
/// <param name="AdjustedScore">The score after freshness adjustment.</param>
/// <param name="MultiplierBps">The multiplier applied (basis points).</param>
/// <param name="EvidenceAgeDays">Age of the evidence in days.</param>
/// <param name="BucketName">Name of the freshness bucket.</param>
public sealed record FreshnessAdjustedScore(
int OriginalScore,
int AdjustedScore,
int MultiplierBps,
int EvidenceAgeDays,
string BucketName);
/// <summary>
/// Result of freshness bucket lookup.
/// </summary>
/// <param name="AgeDays">Age of evidence in days.</param>
/// <param name="BucketName">Human-readable bucket name.</param>
/// <param name="MultiplierBps">Multiplier in basis points.</param>
/// <param name="MultiplierPercent">Multiplier as percentage.</param>
public sealed record FreshnessBucketResult(
int AgeDays,
string BucketName,
int MultiplierBps,
decimal MultiplierPercent);
public sealed class FreshnessAwareScoringService : IFreshnessAwareScoringService
{
private readonly EvidenceFreshnessCalculator _calculator;
private readonly ILogger<FreshnessAwareScoringService> _logger;
public FreshnessAwareScoringService(
FreshnessMultiplierConfig? config = null,
ILogger<FreshnessAwareScoringService>? logger = null)
{
_calculator = new EvidenceFreshnessCalculator(config);
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<FreshnessAwareScoringService>.Instance;
}
public FreshnessAdjustedScore AdjustForFreshness(
int baseScore,
DateTimeOffset evidenceTimestamp,
DateTimeOffset evaluationTime)
{
var ageDays = (int)(evaluationTime - evidenceTimestamp).TotalDays;
if (ageDays < 0) ageDays = 0;
var multiplierBps = _calculator.CalculateMultiplierBps(evidenceTimestamp, evaluationTime);
var adjustedScore = _calculator.ApplyFreshness(baseScore, evidenceTimestamp, evaluationTime);
var bucketName = GetBucketName(ageDays);
_logger.LogDebug(
"Freshness adjustment: base={BaseScore}, adjusted={AdjustedScore}, age={AgeDays}d, bucket={Bucket}, multiplier={Mult}bps",
baseScore, adjustedScore, ageDays, bucketName, multiplierBps);
return new FreshnessAdjustedScore(
OriginalScore: baseScore,
AdjustedScore: adjustedScore,
MultiplierBps: multiplierBps,
EvidenceAgeDays: ageDays,
BucketName: bucketName);
}
public FreshnessBucketResult GetFreshnessBucket(
DateTimeOffset evidenceTimestamp,
DateTimeOffset evaluationTime)
{
var ageDays = (int)(evaluationTime - evidenceTimestamp).TotalDays;
if (ageDays < 0) ageDays = 0;
var multiplierBps = _calculator.CalculateMultiplierBps(evidenceTimestamp, evaluationTime);
var bucketName = GetBucketName(ageDays);
var multiplierPercent = multiplierBps / 100m;
return new FreshnessBucketResult(
AgeDays: ageDays,
BucketName: bucketName,
MultiplierBps: multiplierBps,
MultiplierPercent: multiplierPercent);
}
private static string GetBucketName(int ageDays) => ageDays switch
{
<= 7 => "fresh_7d",
<= 30 => "recent_30d",
<= 90 => "moderate_90d",
<= 180 => "aging_180d",
<= 365 => "stale_365d",
_ => "ancient"
};
}