work work hard work
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user