work work hard work

This commit is contained in:
StellaOps Bot
2025-12-18 00:47:24 +02:00
parent dee252940b
commit b4235c134c
189 changed files with 9627 additions and 3258 deletions

View File

@@ -0,0 +1,174 @@
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Storage.Models;
using StellaOps.Scanner.Storage.Repositories;
namespace StellaOps.Scanner.Storage.Services;
/// <summary>
/// Calculates FN-Drift rate with stratification.
/// </summary>
public sealed class FnDriftCalculator
{
private readonly IClassificationHistoryRepository _repository;
private readonly ILogger<FnDriftCalculator> _logger;
public FnDriftCalculator(
IClassificationHistoryRepository repository,
ILogger<FnDriftCalculator> logger)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Computes FN-Drift for a tenant over a rolling window.
/// </summary>
/// <param name="tenantId">Tenant to calculate for</param>
/// <param name="windowDays">Rolling window in days (default: 30)</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>FN-Drift summary with stratification</returns>
public async Task<FnDrift30dSummary> CalculateAsync(
Guid tenantId,
int windowDays = 30,
CancellationToken cancellationToken = default)
{
var since = DateTimeOffset.UtcNow.AddDays(-windowDays);
var changes = await _repository.GetChangesAsync(tenantId, since, cancellationToken);
var fnTransitions = changes.Where(c => c.IsFnTransition).ToList();
var totalEvaluated = changes.Count;
var summary = new FnDrift30dSummary
{
TenantId = tenantId,
TotalFnTransitions = fnTransitions.Count,
TotalEvaluated = totalEvaluated,
FnDriftPercent = totalEvaluated > 0
? Math.Round((decimal)fnTransitions.Count / totalEvaluated * 100, 4)
: 0,
FeedCaused = fnTransitions.Count(c => c.Cause == DriftCause.FeedDelta),
RuleCaused = fnTransitions.Count(c => c.Cause == DriftCause.RuleDelta),
LatticeCaused = fnTransitions.Count(c => c.Cause == DriftCause.LatticeDelta),
ReachabilityCaused = fnTransitions.Count(c => c.Cause == DriftCause.ReachabilityDelta),
EngineCaused = fnTransitions.Count(c => c.Cause == DriftCause.Engine)
};
_logger.LogInformation(
"FN-Drift for tenant {TenantId}: {Percent}% ({FnCount}/{Total}), " +
"Feed={Feed}, Rule={Rule}, Lattice={Lattice}, Reach={Reach}, Engine={Engine}",
tenantId, summary.FnDriftPercent, summary.TotalFnTransitions, summary.TotalEvaluated,
summary.FeedCaused, summary.RuleCaused, summary.LatticeCaused,
summary.ReachabilityCaused, summary.EngineCaused);
return summary;
}
/// <summary>
/// Determines the drift cause for a classification change.
/// </summary>
public DriftCause DetermineCause(
string? previousFeedVersion,
string? currentFeedVersion,
string? previousRuleHash,
string? currentRuleHash,
string? previousLatticeHash,
string? currentLatticeHash,
bool? previousReachable,
bool? currentReachable)
{
// Priority order: feed > rule > lattice > reachability > engine > other
// Check feed delta
if (!string.Equals(previousFeedVersion, currentFeedVersion, StringComparison.Ordinal))
{
_logger.LogDebug(
"Drift cause: feed_delta (prev={PrevFeed}, curr={CurrFeed})",
previousFeedVersion, currentFeedVersion);
return DriftCause.FeedDelta;
}
// Check rule delta
if (!string.Equals(previousRuleHash, currentRuleHash, StringComparison.Ordinal))
{
_logger.LogDebug(
"Drift cause: rule_delta (prev={PrevRule}, curr={CurrRule})",
previousRuleHash, currentRuleHash);
return DriftCause.RuleDelta;
}
// Check lattice delta
if (!string.Equals(previousLatticeHash, currentLatticeHash, StringComparison.Ordinal))
{
_logger.LogDebug(
"Drift cause: lattice_delta (prev={PrevLattice}, curr={CurrLattice})",
previousLatticeHash, currentLatticeHash);
return DriftCause.LatticeDelta;
}
// Check reachability delta
if (previousReachable != currentReachable)
{
_logger.LogDebug(
"Drift cause: reachability_delta (prev={PrevReach}, curr={CurrReach})",
previousReachable, currentReachable);
return DriftCause.ReachabilityDelta;
}
// If nothing external changed, it's an engine change or unknown
_logger.LogDebug("Drift cause: other (no external cause identified)");
return DriftCause.Other;
}
/// <summary>
/// Creates a ClassificationChange record for a status transition.
/// </summary>
public ClassificationChange CreateChange(
string artifactDigest,
string vulnId,
string packagePurl,
Guid tenantId,
Guid manifestId,
Guid executionId,
ClassificationStatus previousStatus,
ClassificationStatus newStatus,
DriftCause cause,
IReadOnlyDictionary<string, string>? causeDetail = null)
{
return new ClassificationChange
{
ArtifactDigest = artifactDigest,
VulnId = vulnId,
PackagePurl = packagePurl,
TenantId = tenantId,
ManifestId = manifestId,
ExecutionId = executionId,
PreviousStatus = previousStatus,
NewStatus = newStatus,
Cause = cause,
CauseDetail = causeDetail,
ChangedAt = DateTimeOffset.UtcNow
};
}
/// <summary>
/// Checks if the FN-Drift rate exceeds the threshold.
/// </summary>
/// <param name="summary">The drift summary to check</param>
/// <param name="thresholdPercent">Maximum acceptable FN-Drift rate (default: 5%)</param>
/// <returns>True if drift rate exceeds threshold</returns>
public bool ExceedsThreshold(FnDrift30dSummary summary, decimal thresholdPercent = 5.0m)
{
ArgumentNullException.ThrowIfNull(summary);
var exceeds = summary.FnDriftPercent > thresholdPercent;
if (exceeds)
{
_logger.LogWarning(
"FN-Drift for tenant {TenantId} exceeds threshold: {Percent}% > {Threshold}%",
summary.TenantId, summary.FnDriftPercent, thresholdPercent);
}
return exceeds;
}
}

View File

@@ -142,6 +142,8 @@ public sealed class FnDriftMetricsExporter : BackgroundService
private async Task RefreshMetricsAsync(CancellationToken cancellationToken)
{
await _repository.RefreshDriftStatsAsync(cancellationToken);
// Get 30-day summary for all tenants (aggregated)
// In production, this would iterate over active tenants
var now = _timeProvider.GetUtcNow();