finish secrets finding work and audit remarks work save

This commit is contained in:
StellaOps Bot
2026-01-04 21:48:13 +02:00
parent 75611a505f
commit 8862e112c4
157 changed files with 11702 additions and 416 deletions

View File

@@ -18,7 +18,17 @@ public sealed record BoundaryExtractionContext
/// <summary>
/// Empty context for simple extractions.
/// </summary>
public static readonly BoundaryExtractionContext Empty = new();
/// <remarks>Uses system time. For deterministic timestamps, use <see cref="CreateEmpty"/>.</remarks>
[Obsolete("Use CreateEmpty(TimeProvider) for deterministic timestamps")]
public static BoundaryExtractionContext Empty => CreateEmpty();
/// <summary>
/// Creates an empty context for simple extractions.
/// </summary>
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
/// <returns>An empty boundary extraction context.</returns>
public static BoundaryExtractionContext CreateEmpty(TimeProvider? timeProvider = null) =>
new() { Timestamp = (timeProvider ?? TimeProvider.System).GetUtcNow() };
/// <summary>
/// Environment identifier (e.g., "production", "staging").
@@ -61,7 +71,7 @@ public sealed record BoundaryExtractionContext
/// <summary>
/// Timestamp for the context (for cache invalidation).
/// </summary>
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
public required DateTimeOffset Timestamp { get; init; }
/// <summary>
/// Source of this context (e.g., "k8s", "iac", "runtime").
@@ -71,20 +81,28 @@ public sealed record BoundaryExtractionContext
/// <summary>
/// Creates a context from detected gates.
/// </summary>
public static BoundaryExtractionContext FromGates(IReadOnlyList<DetectedGate> gates) =>
new() { DetectedGates = gates };
/// <param name="gates">The detected gates.</param>
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
public static BoundaryExtractionContext FromGates(IReadOnlyList<DetectedGate> gates, TimeProvider? timeProvider = null) =>
new() { DetectedGates = gates, Timestamp = (timeProvider ?? TimeProvider.System).GetUtcNow() };
/// <summary>
/// Creates a context with environment hints.
/// </summary>
/// <param name="environmentId">The environment identifier.</param>
/// <param name="isInternetFacing">Whether the service is internet-facing.</param>
/// <param name="networkZone">The network zone.</param>
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
public static BoundaryExtractionContext ForEnvironment(
string environmentId,
bool? isInternetFacing = null,
string? networkZone = null) =>
string? networkZone = null,
TimeProvider? timeProvider = null) =>
new()
{
EnvironmentId = environmentId,
IsInternetFacing = isInternetFacing,
NetworkZone = networkZone
NetworkZone = networkZone,
Timestamp = (timeProvider ?? TimeProvider.System).GetUtcNow()
};
}

View File

@@ -123,19 +123,22 @@ public sealed class IncrementalReachabilityService : IIncrementalReachabilitySer
private readonly IImpactSetCalculator _impactCalculator;
private readonly IStateFlipDetector _stateFlipDetector;
private readonly ILogger<IncrementalReachabilityService> _logger;
private readonly TimeProvider _timeProvider;
public IncrementalReachabilityService(
IReachabilityCache cache,
IGraphDeltaComputer deltaComputer,
IImpactSetCalculator impactCalculator,
IStateFlipDetector stateFlipDetector,
ILogger<IncrementalReachabilityService> logger)
ILogger<IncrementalReachabilityService> logger,
TimeProvider? timeProvider = null)
{
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_deltaComputer = deltaComputer ?? throw new ArgumentNullException(nameof(deltaComputer));
_impactCalculator = impactCalculator ?? throw new ArgumentNullException(nameof(impactCalculator));
_stateFlipDetector = stateFlipDetector ?? throw new ArgumentNullException(nameof(stateFlipDetector));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
@@ -265,7 +268,7 @@ public sealed class IncrementalReachabilityService : IIncrementalReachabilitySer
private List<ReachablePairResult> ComputeFullReachability(IncrementalReachabilityRequest request)
{
var results = new List<ReachablePairResult>();
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
// Build forward adjacency for BFS
var adj = new Dictionary<string, List<string>>();
@@ -323,7 +326,7 @@ public sealed class IncrementalReachabilityService : IIncrementalReachabilitySer
CancellationToken cancellationToken)
{
var results = new Dictionary<(string, string), ReachablePairResult>();
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
// Copy unaffected results from previous
foreach (var prev in previousResults)

View File

@@ -21,13 +21,16 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
{
private readonly string _connectionString;
private readonly ILogger<PostgresReachabilityCache> _logger;
private readonly TimeProvider _timeProvider;
public PostgresReachabilityCache(
string connectionString,
ILogger<PostgresReachabilityCache> logger)
ILogger<PostgresReachabilityCache> logger,
TimeProvider? timeProvider = null)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
@@ -102,7 +105,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
ServiceId = serviceId,
GraphHash = graphHash,
CachedAt = cachedAt,
TimeToLive = expiresAt.HasValue ? expiresAt.Value - DateTimeOffset.UtcNow : null,
TimeToLive = expiresAt.HasValue ? expiresAt.Value - _timeProvider.GetUtcNow() : null,
ReachablePairs = pairs,
EntryPointCount = entryPointCount,
SinkCount = sinkCount
@@ -143,7 +146,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
}
var expiresAt = entry.TimeToLive.HasValue
? (object)DateTimeOffset.UtcNow.Add(entry.TimeToLive.Value)
? (object)_timeProvider.GetUtcNow().Add(entry.TimeToLive.Value)
: DBNull.Value;
const string insertEntrySql = """

View File

@@ -225,8 +225,9 @@ public sealed class EdgeBundleBuilder
return this;
}
public EdgeBundle Build()
public EdgeBundle Build(TimeProvider? timeProvider = null)
{
var tp = timeProvider ?? TimeProvider.System;
var canonical = _edges
.Select(e => e.Trimmed())
.OrderBy(e => e.From, StringComparer.Ordinal)
@@ -241,7 +242,7 @@ public sealed class EdgeBundleBuilder
GraphHash: _graphHash,
BundleReason: _bundleReason,
Edges: canonical,
GeneratedAt: DateTimeOffset.UtcNow,
GeneratedAt: tp.GetUtcNow(),
CustomReason: _customReason);
}

View File

@@ -322,5 +322,5 @@ public sealed record PathExplanationResult
/// When the explanation was generated.
/// </summary>
[JsonPropertyName("generated_at")]
public DateTimeOffset GeneratedAt { get; init; } = DateTimeOffset.UtcNow;
public required DateTimeOffset GeneratedAt { get; init; }
}

View File

@@ -24,7 +24,7 @@ public sealed class FileSystemCodeContentProvider : ICodeContentProvider
return Task.FromResult<string?>(null);
}
return File.ReadAllTextAsync(path, ct);
return File.ReadAllTextAsync(path, ct)!;
}
public async Task<IReadOnlyList<string>?> GetLinesAsync(

View File

@@ -8,7 +8,7 @@ namespace StellaOps.Scanner.Reachability.MiniMap;
public interface IMiniMapExtractor
{
ReachabilityMiniMap Extract(RichGraph graph, string vulnerableComponent, int maxPaths = 10);
ReachabilityMiniMap Extract(RichGraph graph, string vulnerableComponent, int maxPaths = 10, TimeProvider? timeProvider = null);
}
public sealed class MiniMapExtractor : IMiniMapExtractor
@@ -16,16 +16,19 @@ public sealed class MiniMapExtractor : IMiniMapExtractor
public ReachabilityMiniMap Extract(
RichGraph graph,
string vulnerableComponent,
int maxPaths = 10)
int maxPaths = 10,
TimeProvider? timeProvider = null)
{
// Find vulnerable component node
var vulnNode = graph.Nodes.FirstOrDefault(n =>
n.Purl == vulnerableComponent ||
n.SymbolId?.Contains(vulnerableComponent) == true);
var tp = timeProvider ?? TimeProvider.System;
if (vulnNode is null)
{
return CreateNotFoundMap(vulnerableComponent);
return CreateNotFoundMap(vulnerableComponent, tp);
}
// Find all entrypoints
@@ -75,11 +78,11 @@ public sealed class MiniMapExtractor : IMiniMapExtractor
State = state,
Confidence = confidence,
GraphDigest = ComputeGraphDigest(graph),
AnalyzedAt = DateTimeOffset.UtcNow
AnalyzedAt = tp.GetUtcNow()
};
}
private static ReachabilityMiniMap CreateNotFoundMap(string vulnerableComponent)
private static ReachabilityMiniMap CreateNotFoundMap(string vulnerableComponent, TimeProvider timeProvider)
{
return new ReachabilityMiniMap
{
@@ -96,7 +99,7 @@ public sealed class MiniMapExtractor : IMiniMapExtractor
State = ReachabilityState.Unknown,
Confidence = 0m,
GraphDigest = string.Empty,
AnalyzedAt = DateTimeOffset.UtcNow
AnalyzedAt = timeProvider.GetUtcNow()
};
}

View File

@@ -17,6 +17,8 @@ namespace StellaOps.Scanner.Reachability;
/// </summary>
public sealed class ReachabilityUnionWriter
{
private readonly TimeProvider _timeProvider;
private static readonly JsonWriterOptions JsonOptions = new()
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
@@ -24,6 +26,11 @@ public sealed class ReachabilityUnionWriter
SkipValidation = false
};
public ReachabilityUnionWriter(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<ReachabilityUnionWriteResult> WriteAsync(
ReachabilityUnionGraph graph,
string outputRoot,
@@ -57,7 +64,7 @@ public sealed class ReachabilityUnionWriter
File.Delete(factsPath);
}
await WriteMetaAsync(metaPath, nodesInfo, edgesInfo, factsInfo, cancellationToken).ConfigureAwait(false);
await WriteMetaAsync(metaPath, nodesInfo, edgesInfo, factsInfo, _timeProvider, cancellationToken).ConfigureAwait(false);
return new ReachabilityUnionWriteResult(nodesInfo.ToPublic(), edgesInfo.ToPublic(), factsInfo?.ToPublic(), metaPath);
}
@@ -387,6 +394,7 @@ public sealed class ReachabilityUnionWriter
FileHashInfo nodes,
FileHashInfo edges,
FileHashInfo? facts,
TimeProvider timeProvider,
CancellationToken cancellationToken)
{
await using var stream = File.Create(path);
@@ -394,7 +402,7 @@ public sealed class ReachabilityUnionWriter
writer.WriteStartObject();
writer.WriteString("schema", "reachability-union@0.1");
writer.WriteString("generated_at", DateTimeOffset.UtcNow.ToString("O"));
writer.WriteString("generated_at", timeProvider.GetUtcNow().ToString("O"));
writer.WritePropertyName("files");
writer.WriteStartArray();
WriteMetaFile(writer, nodes);

View File

@@ -30,15 +30,17 @@ public sealed class SliceCacheOptions
public sealed class SliceCache : ISliceCache, IDisposable
{
private readonly SliceCacheOptions _options;
private readonly TimeProvider _timeProvider;
private readonly ConcurrentDictionary<string, CacheItem> _cache = new(StringComparer.Ordinal);
private readonly Timer _evictionTimer;
private long _hitCount;
private long _missCount;
private bool _disposed;
public SliceCache(IOptions<SliceCacheOptions> options)
public SliceCache(IOptions<SliceCacheOptions> options, TimeProvider? timeProvider = null)
{
_options = options?.Value ?? new SliceCacheOptions();
_timeProvider = timeProvider ?? TimeProvider.System;
_evictionTimer = new Timer(EvictExpired, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
}
@@ -53,9 +55,10 @@ public sealed class SliceCache : ISliceCache, IDisposable
if (_cache.TryGetValue(cacheKey, out var item))
{
if (item.ExpiresAt > DateTimeOffset.UtcNow)
var now = _timeProvider.GetUtcNow();
if (item.ExpiresAt > now)
{
item.LastAccessed = DateTimeOffset.UtcNow;
item.LastAccessed = now;
Interlocked.Increment(ref _hitCount);
var result = new CachedSliceResult
{
@@ -89,7 +92,7 @@ public sealed class SliceCache : ISliceCache, IDisposable
EvictLru();
}
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
var item = new CacheItem
{
Digest = result.SliceDigest,
@@ -132,7 +135,7 @@ public sealed class SliceCache : ISliceCache, IDisposable
{
if (_disposed) return;
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
var keysToRemove = _cache
.Where(kvp => kvp.Value.ExpiresAt <= now)
.Select(kvp => kvp.Key)

View File

@@ -19,7 +19,8 @@ public interface IReachabilityStackEvaluator
VulnerableSymbol symbol,
ReachabilityLayer1 layer1,
ReachabilityLayer2 layer2,
ReachabilityLayer3 layer3);
ReachabilityLayer3 layer3,
TimeProvider? timeProvider = null);
/// <summary>
/// Derives the verdict from three layers.
@@ -53,8 +54,10 @@ public sealed class ReachabilityStackEvaluator : IReachabilityStackEvaluator
VulnerableSymbol symbol,
ReachabilityLayer1 layer1,
ReachabilityLayer2 layer2,
ReachabilityLayer3 layer3)
ReachabilityLayer3 layer3,
TimeProvider? timeProvider = null)
{
var tp = timeProvider ?? TimeProvider.System;
var verdict = DeriveVerdict(layer1, layer2, layer3);
var explanation = GenerateExplanation(layer1, layer2, layer3, verdict);
@@ -67,7 +70,7 @@ public sealed class ReachabilityStackEvaluator : IReachabilityStackEvaluator
BinaryResolution = layer2,
RuntimeGating = layer3,
Verdict = verdict,
AnalyzedAt = DateTimeOffset.UtcNow,
AnalyzedAt = tp.GetUtcNow(),
Explanation = explanation
};
}

View File

@@ -125,7 +125,7 @@ public sealed class WitnessDsseSigner : IWitnessDsseSigner
return WitnessVerifyResult.Failure($"Signature verification failed: {verifyResult.Error?.Message}");
}
return WitnessVerifyResult.Success(witness, matchingSignature.KeyId);
return WitnessVerifyResult.Success(witness, matchingSignature.KeyId!);
}
catch (Exception ex) when (ex is JsonException or FormatException or InvalidOperationException)
{