up
This commit is contained in:
@@ -124,7 +124,7 @@ public sealed class EpssPriorityCalculator
|
||||
var flags = EpssChangeFlags.None;
|
||||
|
||||
// NEW_SCORED: first time we have EPSS data
|
||||
if (!oldScore.HasValue && newScore > 0)
|
||||
if (!oldScore.HasValue)
|
||||
{
|
||||
flags |= EpssChangeFlags.NewScored;
|
||||
}
|
||||
@@ -133,31 +133,37 @@ public sealed class EpssPriorityCalculator
|
||||
{
|
||||
var delta = newScore - oldScore.Value;
|
||||
|
||||
// BIG_JUMP: significant score increase
|
||||
if (delta >= _options.BigJumpDelta)
|
||||
{
|
||||
flags |= EpssChangeFlags.BigJump;
|
||||
}
|
||||
|
||||
// DROPPED_LOW: significant score decrease
|
||||
if (delta <= -_options.DroppedLowDelta)
|
||||
{
|
||||
flags |= EpssChangeFlags.DroppedLow;
|
||||
}
|
||||
}
|
||||
|
||||
// CROSSED_HIGH: moved into or out of high priority
|
||||
var oldBand = Calculate(oldScore, oldPercentile).Band;
|
||||
var newBand = Calculate(newScore, newPercentile).Band;
|
||||
|
||||
if (oldBand != newBand)
|
||||
{
|
||||
// Crossed into critical or high
|
||||
if ((newBand == EpssPriorityBand.Critical || newBand == EpssPriorityBand.High) &&
|
||||
oldBand != EpssPriorityBand.Critical && oldBand != EpssPriorityBand.High)
|
||||
if (oldScore.Value < _options.HighScore && newScore >= _options.HighScore)
|
||||
{
|
||||
flags |= EpssChangeFlags.CrossedHigh;
|
||||
}
|
||||
|
||||
if (oldScore.Value >= _options.HighScore && newScore < _options.HighScore)
|
||||
{
|
||||
flags |= EpssChangeFlags.CrossedLow;
|
||||
}
|
||||
|
||||
if (delta > _options.BigJumpDelta)
|
||||
{
|
||||
flags |= EpssChangeFlags.BigJumpUp;
|
||||
}
|
||||
|
||||
if (delta < -_options.DroppedLowDelta)
|
||||
{
|
||||
flags |= EpssChangeFlags.BigJumpDown;
|
||||
}
|
||||
}
|
||||
|
||||
if ((oldPercentile is null || oldPercentile < _options.HighPercentile)
|
||||
&& newPercentile >= _options.HighPercentile)
|
||||
{
|
||||
flags |= EpssChangeFlags.TopPercentile;
|
||||
}
|
||||
|
||||
if (oldPercentile is not null && oldPercentile >= _options.HighPercentile
|
||||
&& newPercentile < _options.HighPercentile)
|
||||
{
|
||||
flags |= EpssChangeFlags.LeftTopPercentile;
|
||||
}
|
||||
|
||||
return flags;
|
||||
@@ -174,14 +180,23 @@ public enum EpssChangeFlags
|
||||
None = 0,
|
||||
|
||||
/// <summary>CVE was scored for the first time.</summary>
|
||||
NewScored = 1 << 0,
|
||||
NewScored = 1,
|
||||
|
||||
/// <summary>Score crossed into high priority band.</summary>
|
||||
CrossedHigh = 1 << 1,
|
||||
CrossedHigh = 2,
|
||||
|
||||
/// <summary>Score crossed below the high score threshold.</summary>
|
||||
CrossedLow = 4,
|
||||
|
||||
/// <summary>Score increased significantly (above BigJumpDelta).</summary>
|
||||
BigJump = 1 << 2,
|
||||
BigJumpUp = 8,
|
||||
|
||||
/// <summary>Score dropped significantly (above DroppedLowDelta).</summary>
|
||||
DroppedLow = 1 << 3
|
||||
/// <summary>Score decreased significantly (above DroppedLowDelta).</summary>
|
||||
BigJumpDown = 16,
|
||||
|
||||
/// <summary>Entered the top percentile band.</summary>
|
||||
TopPercentile = 32,
|
||||
|
||||
/// <summary>Left the top percentile band.</summary>
|
||||
LeftTopPercentile = 64
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Messaging;
|
||||
using StellaOps.Messaging.Abstractions;
|
||||
using StellaOps.Scanner.Core.Epss;
|
||||
|
||||
@@ -58,7 +59,7 @@ public sealed class CachingEpssProvider : IEpssProvider
|
||||
{
|
||||
var cacheResult = await _cache.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (cacheResult.IsHit && cacheResult.Value is not null)
|
||||
if (cacheResult.HasValue && cacheResult.Value is not null)
|
||||
{
|
||||
_logger.LogDebug("Cache hit for EPSS score: {CveId}", cveId);
|
||||
return MapFromCacheEntry(cacheResult.Value, fromCache: true);
|
||||
@@ -119,7 +120,7 @@ public sealed class CachingEpssProvider : IEpssProvider
|
||||
var cacheKey = BuildCacheKey(cveId);
|
||||
var cacheResult = await _cache.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (cacheResult.IsHit && cacheResult.Value is not null)
|
||||
if (cacheResult.HasValue && cacheResult.Value is not null)
|
||||
{
|
||||
var evidence = MapFromCacheEntry(cacheResult.Value, fromCache: true);
|
||||
found.Add(evidence);
|
||||
@@ -214,7 +215,7 @@ public sealed class CachingEpssProvider : IEpssProvider
|
||||
{
|
||||
var cacheResult = await _cache.GetAsync(ModelDateCacheKey, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (cacheResult.IsHit && cacheResult.Value?.ModelDate is not null)
|
||||
if (cacheResult.HasValue && cacheResult.Value is not null)
|
||||
{
|
||||
return cacheResult.Value.ModelDate;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Description: Deterministic EPSS delta flag computation (mirrors SQL function).
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Core.Epss;
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Epss;
|
||||
|
||||
public static class EpssChangeDetector
|
||||
@@ -72,4 +74,3 @@ public readonly record struct EpssChangeThresholds(
|
||||
double HighScore,
|
||||
double HighPercentile,
|
||||
double BigJumpDelta);
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// EpssChangeFlags.cs
|
||||
// Sprint: SPRINT_3410_0001_0001_epss_ingestion_storage
|
||||
// Task: EPSS-3410-008
|
||||
// Description: Flag bitmask for EPSS change detection.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Epss;
|
||||
|
||||
[Flags]
|
||||
public enum EpssChangeFlags
|
||||
{
|
||||
None = 0,
|
||||
|
||||
/// <summary>0x01 - CVE newly scored (not in previous snapshot).</summary>
|
||||
NewScored = 1,
|
||||
|
||||
/// <summary>0x02 - Crossed above the high score threshold.</summary>
|
||||
CrossedHigh = 2,
|
||||
|
||||
/// <summary>0x04 - Crossed below the high score threshold.</summary>
|
||||
CrossedLow = 4,
|
||||
|
||||
/// <summary>0x08 - Score increased by more than the big jump delta.</summary>
|
||||
BigJumpUp = 8,
|
||||
|
||||
/// <summary>0x10 - Score decreased by more than the big jump delta.</summary>
|
||||
BigJumpDown = 16,
|
||||
|
||||
/// <summary>0x20 - Entered the top percentile band.</summary>
|
||||
TopPercentile = 32,
|
||||
|
||||
/// <summary>0x40 - Left the top percentile band.</summary>
|
||||
LeftTopPercentile = 64
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Messaging;
|
||||
using StellaOps.Messaging.Abstractions;
|
||||
using StellaOps.Scanner.Core.Epss;
|
||||
using StellaOps.Scanner.Storage.Epss;
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class PostgresEpssRawRepository : IEpssRawRepository
|
||||
RETURNING raw_id, ingestion_ts
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
|
||||
var result = await connection.QueryFirstOrDefaultAsync<(long raw_id, DateTimeOffset ingestion_ts)?>(sql, new
|
||||
{
|
||||
@@ -88,7 +88,7 @@ public sealed class PostgresEpssRawRepository : IEpssRawRepository
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
var row = await connection.QueryFirstOrDefaultAsync<RawRow?>(sql, new
|
||||
{
|
||||
AsOfDate = asOfDate.ToDateTime(TimeOnly.MinValue)
|
||||
@@ -112,7 +112,7 @@ public sealed class PostgresEpssRawRepository : IEpssRawRepository
|
||||
ORDER BY asof_date DESC
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
var rows = await connection.QueryAsync<RawRow>(sql, new
|
||||
{
|
||||
StartDate = startDate.ToDateTime(TimeOnly.MinValue),
|
||||
@@ -134,7 +134,7 @@ public sealed class PostgresEpssRawRepository : IEpssRawRepository
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
var row = await connection.QueryFirstOrDefaultAsync<RawRow?>(sql);
|
||||
|
||||
return row.HasValue ? MapToRaw(row.Value) : null;
|
||||
@@ -149,7 +149,7 @@ public sealed class PostgresEpssRawRepository : IEpssRawRepository
|
||||
)
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
return await connection.ExecuteScalarAsync<bool>(sql, new
|
||||
{
|
||||
AsOfDate = asOfDate.ToDateTime(TimeOnly.MinValue),
|
||||
@@ -173,7 +173,7 @@ public sealed class PostgresEpssRawRepository : IEpssRawRepository
|
||||
LIMIT @Limit
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
var rows = await connection.QueryAsync<RawRow>(sql, new
|
||||
{
|
||||
ModelVersion = modelVersion,
|
||||
@@ -187,7 +187,7 @@ public sealed class PostgresEpssRawRepository : IEpssRawRepository
|
||||
{
|
||||
var sql = $"SELECT {SchemaName}.prune_epss_raw(@RetentionDays)";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
return await connection.ExecuteScalarAsync<int>(sql, new { RetentionDays = retentionDays });
|
||||
}
|
||||
|
||||
|
||||
@@ -505,14 +505,17 @@ public sealed class PostgresEpssRepository : IEpssRepository
|
||||
LIMIT @Limit
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var rows = await connection.QueryAsync<ChangeRow>(sql, new
|
||||
{
|
||||
ModelDate = modelDate,
|
||||
Flags = flags.HasValue ? (int)flags.Value : 0,
|
||||
Limit = limit
|
||||
});
|
||||
var rows = await connection.QueryAsync<ChangeRow>(new CommandDefinition(
|
||||
sql,
|
||||
new
|
||||
{
|
||||
ModelDate = modelDate,
|
||||
Flags = flags.HasValue ? (int)flags.Value : 0,
|
||||
Limit = limit
|
||||
},
|
||||
cancellationToken: cancellationToken)).ConfigureAwait(false);
|
||||
|
||||
return rows.Select(r => new EpssChangeRecord
|
||||
{
|
||||
|
||||
@@ -46,7 +46,7 @@ public sealed class PostgresEpssSignalRepository : IEpssSignalRepository
|
||||
RETURNING signal_id, created_at
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(signal.TenantId.ToString("D"), cancellationToken);
|
||||
|
||||
var result = await connection.QueryFirstOrDefaultAsync<(long signal_id, DateTimeOffset created_at)?>(sql, new
|
||||
{
|
||||
@@ -104,34 +104,38 @@ public sealed class PostgresEpssSignalRepository : IEpssSignalRepository
|
||||
ON CONFLICT (tenant_id, dedupe_key) DO NOTHING
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
var inserted = 0;
|
||||
foreach (var signal in signalList)
|
||||
foreach (var tenantGroup in signalList.GroupBy(signal => signal.TenantId))
|
||||
{
|
||||
var affected = await connection.ExecuteAsync(sql, new
|
||||
{
|
||||
signal.TenantId,
|
||||
ModelDate = signal.ModelDate.ToDateTime(TimeOnly.MinValue),
|
||||
signal.CveId,
|
||||
signal.EventType,
|
||||
signal.RiskBand,
|
||||
signal.EpssScore,
|
||||
signal.EpssDelta,
|
||||
signal.Percentile,
|
||||
signal.PercentileDelta,
|
||||
signal.IsModelChange,
|
||||
signal.ModelVersion,
|
||||
signal.DedupeKey,
|
||||
signal.ExplainHash,
|
||||
signal.Payload
|
||||
}, transaction);
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantGroup.Key.ToString("D"), cancellationToken);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
inserted += affected;
|
||||
foreach (var signal in tenantGroup)
|
||||
{
|
||||
var affected = await connection.ExecuteAsync(sql, new
|
||||
{
|
||||
signal.TenantId,
|
||||
ModelDate = signal.ModelDate.ToDateTime(TimeOnly.MinValue),
|
||||
signal.CveId,
|
||||
signal.EventType,
|
||||
signal.RiskBand,
|
||||
signal.EpssScore,
|
||||
signal.EpssDelta,
|
||||
signal.Percentile,
|
||||
signal.PercentileDelta,
|
||||
signal.IsModelChange,
|
||||
signal.ModelVersion,
|
||||
signal.DedupeKey,
|
||||
signal.ExplainHash,
|
||||
signal.Payload
|
||||
}, transaction);
|
||||
|
||||
inserted += affected;
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
return inserted;
|
||||
}
|
||||
|
||||
@@ -159,7 +163,7 @@ public sealed class PostgresEpssSignalRepository : IEpssSignalRepository
|
||||
LIMIT 10000
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId.ToString("D"), cancellationToken);
|
||||
|
||||
var rows = await connection.QueryAsync<SignalRow>(sql, new
|
||||
{
|
||||
@@ -190,7 +194,7 @@ public sealed class PostgresEpssSignalRepository : IEpssSignalRepository
|
||||
LIMIT @Limit
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId.ToString("D"), cancellationToken);
|
||||
|
||||
var rows = await connection.QueryAsync<SignalRow>(sql, new
|
||||
{
|
||||
@@ -222,7 +226,7 @@ public sealed class PostgresEpssSignalRepository : IEpssSignalRepository
|
||||
LIMIT 10000
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId.ToString("D"), cancellationToken);
|
||||
|
||||
var rows = await connection.QueryAsync<SignalRow>(sql, new
|
||||
{
|
||||
@@ -246,7 +250,7 @@ public sealed class PostgresEpssSignalRepository : IEpssSignalRepository
|
||||
WHERE tenant_id = @TenantId
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId.ToString("D"), cancellationToken);
|
||||
|
||||
var row = await connection.QueryFirstOrDefaultAsync<ConfigRow?>(sql, new { TenantId = tenantId });
|
||||
|
||||
@@ -277,7 +281,7 @@ public sealed class PostgresEpssSignalRepository : IEpssSignalRepository
|
||||
RETURNING config_id, created_at, updated_at
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(config.TenantId.ToString("D"), cancellationToken);
|
||||
|
||||
var result = await connection.QueryFirstAsync<(Guid config_id, DateTimeOffset created_at, DateTimeOffset updated_at)>(sql, new
|
||||
{
|
||||
@@ -302,7 +306,7 @@ public sealed class PostgresEpssSignalRepository : IEpssSignalRepository
|
||||
{
|
||||
var sql = $"SELECT {SchemaName}.prune_epss_signals(@RetentionDays)";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
return await connection.ExecuteScalarAsync<int>(sql, new { RetentionDays = retentionDays });
|
||||
}
|
||||
|
||||
@@ -317,7 +321,7 @@ public sealed class PostgresEpssSignalRepository : IEpssSignalRepository
|
||||
WHERE tenant_id = @TenantId AND dedupe_key = @DedupeKey
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId.ToString("D"), cancellationToken);
|
||||
var row = await connection.QueryFirstOrDefaultAsync<SignalRow?>(sql, new { TenantId = tenantId, DedupeKey = dedupeKey });
|
||||
|
||||
return row.HasValue ? MapToSignal(row.Value) : null;
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class PostgresObservedCveRepository : IObservedCveRepository
|
||||
AND cve_id LIKE 'CVE-%'
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId.ToString("D"), cancellationToken);
|
||||
var cves = await connection.QueryAsync<string>(sql, new { TenantId = tenantId });
|
||||
|
||||
return new HashSet<string>(cves, StringComparer.OrdinalIgnoreCase);
|
||||
@@ -52,12 +52,12 @@ public sealed class PostgresObservedCveRepository : IObservedCveRepository
|
||||
var sql = $"""
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM {TriageTable}
|
||||
WHERE tenant_id = @TenantId
|
||||
AND cve_id = @CveId
|
||||
WHERE tenant_id = @TenantId
|
||||
AND cve_id = @CveId
|
||||
)
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId.ToString("D"), cancellationToken);
|
||||
return await connection.ExecuteScalarAsync<bool>(sql, new { TenantId = tenantId, CveId = cveId });
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ public sealed class PostgresObservedCveRepository : IObservedCveRepository
|
||||
AND cve_id = ANY(@CveIds)
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId.ToString("D"), cancellationToken);
|
||||
var observed = await connection.QueryAsync<string>(sql, new
|
||||
{
|
||||
TenantId = tenantId,
|
||||
@@ -99,7 +99,7 @@ public sealed class PostgresObservedCveRepository : IObservedCveRepository
|
||||
AND cve_id LIKE 'CVE-%'
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
var tenants = await connection.QueryAsync<Guid>(sql);
|
||||
|
||||
return tenants.ToList();
|
||||
@@ -122,7 +122,7 @@ public sealed class PostgresObservedCveRepository : IObservedCveRepository
|
||||
GROUP BY cve_id, tenant_id
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
var rows = await connection.QueryAsync<(string cve_id, Guid tenant_id)>(sql, new
|
||||
{
|
||||
CveIds = cveList.ToArray()
|
||||
|
||||
Reference in New Issue
Block a user