audit, advisories and doctors/setup work
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// HotSymbolsController.cs
|
||||
// Sprint: SPRINT_20251226_010_SIGNALS_runtime_stack
|
||||
// Task: STACK-12 — API endpoint: GET /api/v1/signals/hot-symbols?image=<digest>
|
||||
// Task: STACK-12 - API endpoint: GET /api/v1/signals/hot-symbols?image=<digest>
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Collections.Immutable;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.RuntimeAgent;
|
||||
|
||||
namespace StellaOps.Signals.Api;
|
||||
@@ -366,6 +367,7 @@ public sealed class RuntimeAgentController : ControllerBase
|
||||
public sealed class RuntimeFactsController : ControllerBase
|
||||
{
|
||||
private readonly IRuntimeFactsIngest _factsIngestService;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly ILogger<RuntimeFactsController> _logger;
|
||||
|
||||
/// <summary>
|
||||
@@ -373,9 +375,11 @@ public sealed class RuntimeFactsController : ControllerBase
|
||||
/// </summary>
|
||||
public RuntimeFactsController(
|
||||
IRuntimeFactsIngest factsIngestService,
|
||||
IGuidProvider guidProvider,
|
||||
ILogger<RuntimeFactsController> logger)
|
||||
{
|
||||
_factsIngestService = factsIngestService ?? throw new ArgumentNullException(nameof(factsIngestService));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -412,7 +416,7 @@ public sealed class RuntimeFactsController : ControllerBase
|
||||
{
|
||||
var events = request.Events.Select(e => new RuntimeMethodEvent
|
||||
{
|
||||
EventId = e.EventId ?? Guid.NewGuid().ToString("N"),
|
||||
EventId = e.EventId ?? _guidProvider.NewGuid().ToString("N"),
|
||||
SymbolId = e.SymbolId,
|
||||
MethodName = e.MethodName,
|
||||
TypeName = e.TypeName,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
@@ -208,7 +208,7 @@ public sealed record EvidenceWeightPolicy
|
||||
public string? TenantId { get; init; }
|
||||
|
||||
/// <summary>Policy creation timestamp (UTC ISO-8601).</summary>
|
||||
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Default production policy.
|
||||
@@ -224,7 +224,7 @@ public sealed record EvidenceWeightPolicy
|
||||
|
||||
/// <summary>
|
||||
/// Computes a deterministic digest of this policy for versioning.
|
||||
/// Uses canonical JSON serialization → SHA256.
|
||||
/// Uses canonical JSON serialization -> SHA256.
|
||||
/// </summary>
|
||||
public string ComputeDigest()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -191,10 +191,12 @@ public sealed class BucketConfiguration
|
||||
public sealed class OptionsEvidenceWeightPolicyProvider : IEvidenceWeightPolicyProvider
|
||||
{
|
||||
private readonly IOptionsMonitor<EvidenceWeightPolicyOptions> _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public OptionsEvidenceWeightPolicyProvider(IOptionsMonitor<EvidenceWeightPolicyOptions> options)
|
||||
public OptionsEvidenceWeightPolicyProvider(IOptionsMonitor<EvidenceWeightPolicyOptions> options, TimeProvider timeProvider)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public Task<EvidenceWeightPolicy> GetPolicyAsync(
|
||||
@@ -225,7 +227,8 @@ public sealed class OptionsEvidenceWeightPolicyProvider : IEvidenceWeightPolicyP
|
||||
Profile = environment,
|
||||
Weights = weights,
|
||||
Guardrails = options.Guardrails.ToGuardrailConfig(),
|
||||
Buckets = options.Buckets.ToBucketThresholds()
|
||||
Buckets = options.Buckets.ToBucketThresholds(),
|
||||
CreatedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
return Task.FromResult(policy);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
@@ -226,7 +226,7 @@ public sealed class EvidenceWeightedScoreCalculator : IEvidenceWeightedScoreCalc
|
||||
|
||||
// Order matters: caps before floors
|
||||
|
||||
// 1. Speculative cap: if RCH=0 + RTS=0 → cap at configured max (default 45)
|
||||
// 1. Speculative cap: if RCH=0 + RTS=0 -> cap at configured max (default 45)
|
||||
if (config.SpeculativeCap.Enabled &&
|
||||
input.Rch <= config.SpeculativeCap.RequiresRchMax &&
|
||||
input.Rts <= config.SpeculativeCap.RequiresRtsMax)
|
||||
@@ -238,7 +238,7 @@ public sealed class EvidenceWeightedScoreCalculator : IEvidenceWeightedScoreCalc
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Not-affected cap: if BKP>=1 + not_affected + RTS<0.6 → cap at configured max (default 15)
|
||||
// 2. Not-affected cap: if BKP>=1 + not_affected + RTS<0.6 -> cap at configured max (default 15)
|
||||
if (config.NotAffectedCap.Enabled &&
|
||||
input.Bkp >= config.NotAffectedCap.RequiresBkpMin &&
|
||||
input.Rts < config.NotAffectedCap.RequiresRtsMax &&
|
||||
@@ -251,7 +251,7 @@ public sealed class EvidenceWeightedScoreCalculator : IEvidenceWeightedScoreCalc
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Runtime floor: if RTS >= 0.8 → floor at configured min (default 60)
|
||||
// 3. Runtime floor: if RTS >= 0.8 -> floor at configured min (default 60)
|
||||
if (config.RuntimeFloor.Enabled &&
|
||||
input.Rts >= config.RuntimeFloor.RequiresRtsMin)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
@@ -46,7 +46,8 @@ public static class EvidenceWeightedScoringExtensions
|
||||
services.TryAddSingleton<IEvidenceWeightPolicyProvider>(sp =>
|
||||
{
|
||||
var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<EvidenceWeightPolicyOptions>>();
|
||||
return new OptionsEvidenceWeightPolicyProvider(optionsMonitor);
|
||||
var timeProvider = sp.GetRequiredService<TimeProvider>();
|
||||
return new OptionsEvidenceWeightPolicyProvider(optionsMonitor, timeProvider);
|
||||
});
|
||||
|
||||
// Register TimeProvider if not already registered
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
@@ -44,13 +44,25 @@ public sealed class InMemoryEvidenceWeightPolicyProvider : IEvidenceWeightPolicy
|
||||
{
|
||||
private readonly Dictionary<string, EvidenceWeightPolicy> _policies = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly object _lock = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryEvidenceWeightPolicyProvider(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds or updates a policy.
|
||||
/// </summary>
|
||||
public void SetPolicy(EvidenceWeightPolicy policy)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(policy);
|
||||
|
||||
var key = GetPolicyKey(policy.TenantId, policy.Profile);
|
||||
if (policy.CreatedAt == default)
|
||||
{
|
||||
policy = policy with { CreatedAt = _timeProvider.GetUtcNow() };
|
||||
}
|
||||
lock (_lock)
|
||||
{
|
||||
_policies[key] = policy;
|
||||
@@ -132,7 +144,7 @@ public sealed class InMemoryEvidenceWeightPolicyProvider : IEvidenceWeightPolicy
|
||||
: $"{tenantId}:{environment}";
|
||||
}
|
||||
|
||||
private static EvidenceWeightPolicy CreateDefaultPolicy(string environment)
|
||||
private EvidenceWeightPolicy CreateDefaultPolicy(string environment)
|
||||
{
|
||||
var weights = environment.Equals("production", StringComparison.OrdinalIgnoreCase)
|
||||
? new EvidenceWeights
|
||||
@@ -160,7 +172,8 @@ public sealed class InMemoryEvidenceWeightPolicyProvider : IEvidenceWeightPolicy
|
||||
{
|
||||
Version = "ews.v1",
|
||||
Profile = environment,
|
||||
Weights = weights
|
||||
Weights = weights,
|
||||
CreatedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
@@ -125,7 +125,7 @@ public sealed record MitigationInput
|
||||
.ThenBy(m => m.Name ?? "", StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
// Diminishing returns: combined = 1 - Π(1 - e_i)
|
||||
// Diminishing returns: combined = 1 - prod(1 - e_i)
|
||||
// Each mitigation reduces remaining risk multiplicatively
|
||||
var remainingRisk = 1.0;
|
||||
foreach (var mitigation in sorted)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -39,6 +39,9 @@ public static class EvidenceNormalizersServiceCollectionExtensions
|
||||
services.AddOptions<NormalizerOptions>()
|
||||
.Configure(configure);
|
||||
|
||||
// Register TimeProvider if not already registered
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
|
||||
// Register individual normalizers
|
||||
services.TryAddSingleton<IEvidenceNormalizer<ReachabilityInput>, ReachabilityNormalizer>();
|
||||
services.TryAddSingleton<IEvidenceNormalizer<RuntimeInput>, RuntimeSignalNormalizer>();
|
||||
@@ -74,6 +77,9 @@ public static class EvidenceNormalizersServiceCollectionExtensions
|
||||
.Bind(section)
|
||||
.ValidateOnStart();
|
||||
|
||||
// Register TimeProvider if not already registered
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
|
||||
// Register individual normalizers
|
||||
services.TryAddSingleton<IEvidenceNormalizer<ReachabilityInput>, ReachabilityNormalizer>();
|
||||
services.TryAddSingleton<IEvidenceNormalizer<RuntimeInput>, RuntimeSignalNormalizer>();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -13,10 +13,10 @@ namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
/// Scoring logic:
|
||||
/// - KEV presence establishes a floor (default 0.40) - actively exploited vulnerabilities are high risk
|
||||
/// - EPSS percentile maps to score bands:
|
||||
/// - Top 1% (≥99th percentile): 0.90–1.00
|
||||
/// - Top 5% (≥95th percentile): 0.70–0.89
|
||||
/// - Top 25% (≥75th percentile): 0.40–0.69
|
||||
/// - Below 75th percentile: 0.20–0.39
|
||||
/// - Top 1% (>=99th percentile): 0.90-1.00
|
||||
/// - Top 5% (>=95th percentile): 0.70-0.89
|
||||
/// - Top 25% (>=75th percentile): 0.40-0.69
|
||||
/// - Below 75th percentile: 0.20-0.39
|
||||
/// - Missing EPSS data: neutral score (default 0.30)
|
||||
/// - Public exploit availability adds a bonus
|
||||
/// - Final score is max(KEV floor, EPSS-based score)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -166,7 +166,7 @@ public sealed class MitigationNormalizer : IEvidenceNormalizer<MitigationInput>
|
||||
private static string FormatMitigation(ActiveMitigation mitigation)
|
||||
{
|
||||
var name = !string.IsNullOrEmpty(mitigation.Name) ? mitigation.Name : mitigation.Type.ToString();
|
||||
var verified = mitigation.Verified ? " ✓" : "";
|
||||
var verified = mitigation.Verified ? " [verified]" : "";
|
||||
return $"{name} ({mitigation.Effectiveness:P0}{verified})";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -18,23 +18,25 @@ public sealed class NormalizerAggregator : INormalizerAggregator
|
||||
private readonly IEvidenceNormalizer<SourceTrustInput> _sourceTrustNormalizer;
|
||||
private readonly IEvidenceNormalizer<MitigationInput> _mitigationNormalizer;
|
||||
private readonly NormalizerOptions _options;
|
||||
private readonly TimeProvider _timeProvider = TimeProvider.System;
|
||||
|
||||
/// <summary>
|
||||
/// Create an aggregator with default normalizers and options.
|
||||
/// </summary>
|
||||
public NormalizerAggregator()
|
||||
: this(new NormalizerOptions())
|
||||
: this(new NormalizerOptions(), TimeProvider.System)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an aggregator with specific options.
|
||||
/// </summary>
|
||||
public NormalizerAggregator(NormalizerOptions options)
|
||||
public NormalizerAggregator(NormalizerOptions options, TimeProvider? timeProvider = null)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_reachabilityNormalizer = new ReachabilityNormalizer(_options.Reachability);
|
||||
_runtimeNormalizer = new RuntimeSignalNormalizer(_options.Runtime);
|
||||
_runtimeNormalizer = new RuntimeSignalNormalizer(_options.Runtime, _timeProvider);
|
||||
_backportNormalizer = new BackportEvidenceNormalizer(_options.Backport);
|
||||
_exploitNormalizer = new ExploitLikelihoodNormalizer(_options.Exploit);
|
||||
_sourceTrustNormalizer = new SourceTrustNormalizer(_options.SourceTrust);
|
||||
@@ -65,8 +67,8 @@ public sealed class NormalizerAggregator : INormalizerAggregator
|
||||
/// <summary>
|
||||
/// Create an aggregator with DI-provided options.
|
||||
/// </summary>
|
||||
public NormalizerAggregator(IOptionsMonitor<NormalizerOptions> optionsMonitor)
|
||||
: this(optionsMonitor?.CurrentValue ?? new NormalizerOptions())
|
||||
public NormalizerAggregator(IOptionsMonitor<NormalizerOptions> optionsMonitor, TimeProvider timeProvider)
|
||||
: this(optionsMonitor?.CurrentValue ?? new NormalizerOptions(), timeProvider)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -210,7 +210,7 @@ public sealed class ReachabilityNormalizer : IEvidenceNormalizer<ReachabilityInp
|
||||
if (!string.IsNullOrEmpty(input.EvidenceSource))
|
||||
sb.Append($" from {input.EvidenceSource}");
|
||||
|
||||
sb.Append($" → RCH={score:F2}");
|
||||
sb.Append($" -> RCH={score:F2}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -21,28 +21,30 @@ namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
public sealed class RuntimeSignalNormalizer : IEvidenceNormalizer<RuntimeInput>
|
||||
{
|
||||
private readonly RuntimeNormalizerOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Create a normalizer with default options.
|
||||
/// </summary>
|
||||
public RuntimeSignalNormalizer()
|
||||
: this(new RuntimeNormalizerOptions())
|
||||
: this(new RuntimeNormalizerOptions(), TimeProvider.System)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a normalizer with specific options.
|
||||
/// </summary>
|
||||
public RuntimeSignalNormalizer(RuntimeNormalizerOptions options)
|
||||
public RuntimeSignalNormalizer(RuntimeNormalizerOptions options, TimeProvider timeProvider)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a normalizer with DI-provided options.
|
||||
/// </summary>
|
||||
public RuntimeSignalNormalizer(IOptionsMonitor<NormalizerOptions> optionsMonitor)
|
||||
: this(optionsMonitor?.CurrentValue?.Runtime ?? new RuntimeNormalizerOptions())
|
||||
public RuntimeSignalNormalizer(IOptionsMonitor<NormalizerOptions> optionsMonitor, TimeProvider timeProvider)
|
||||
: this(optionsMonitor?.CurrentValue?.Runtime ?? new RuntimeNormalizerOptions(), timeProvider)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -126,7 +128,7 @@ public sealed class RuntimeSignalNormalizer : IEvidenceNormalizer<RuntimeInput>
|
||||
// Fall back to LastObservation timestamp if available
|
||||
if (input.LastObservation.HasValue)
|
||||
{
|
||||
var hoursSince = (DateTimeOffset.UtcNow - input.LastObservation.Value).TotalHours;
|
||||
var hoursSince = (_timeProvider.GetUtcNow() - input.LastObservation.Value).TotalHours;
|
||||
return hoursSince switch
|
||||
{
|
||||
< 1.0 when hoursSince < _options.VeryRecentHours => _options.VeryRecentBonus,
|
||||
@@ -181,7 +183,7 @@ public sealed class RuntimeSignalNormalizer : IEvidenceNormalizer<RuntimeInput>
|
||||
if (input.Posture == RuntimePosture.None || input.ObservationCount == 0)
|
||||
{
|
||||
sb.Append("No runtime observations available");
|
||||
sb.Append($" → RTS={score:F2}");
|
||||
sb.Append($" -> RTS={score:F2}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
@@ -215,7 +217,7 @@ public sealed class RuntimeSignalNormalizer : IEvidenceNormalizer<RuntimeInput>
|
||||
if (!string.IsNullOrEmpty(input.EvidenceSource))
|
||||
sb.Append($" from {input.EvidenceSource}");
|
||||
|
||||
sb.Append($" → RTS={score:F2}");
|
||||
sb.Append($" -> RTS={score:F2}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -218,7 +218,7 @@ public sealed class SourceTrustNormalizer : IEvidenceNormalizer<SourceTrustInput
|
||||
if (input.HistoricalAccuracy.HasValue)
|
||||
sb.Append($", {input.HistoricalAccuracy.Value:P0} historical accuracy");
|
||||
|
||||
sb.Append($" → SRC={score:F2}");
|
||||
sb.Append($" -> SRC={score:F2}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@ public sealed class ProvenanceFeed
|
||||
public int SchemaVersion { get; init; } = CurrentSchemaVersion;
|
||||
|
||||
[JsonPropertyName("feedId")]
|
||||
public string FeedId { get; init; } = Guid.NewGuid().ToString("D");
|
||||
public string FeedId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("feedType")]
|
||||
public ProvenanceFeedType FeedType { get; init; } = ProvenanceFeedType.RuntimeFacts;
|
||||
|
||||
[JsonPropertyName("generatedAt")]
|
||||
public DateTimeOffset GeneratedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset GeneratedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("sourceService")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
@@ -73,7 +73,7 @@ public enum ProvenanceFeedType
|
||||
public sealed class ProvenanceRecord
|
||||
{
|
||||
[JsonPropertyName("recordId")]
|
||||
public string RecordId { get; init; } = Guid.NewGuid().ToString("D");
|
||||
public string RecordId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("recordType")]
|
||||
public string RecordType { get; init; } = string.Empty;
|
||||
|
||||
@@ -60,7 +60,7 @@ public sealed class CallgraphDocument
|
||||
// ===== LEGACY FIELDS FOR BACKWARD COMPATIBILITY =====
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Legacy language field (string). Use LanguageType for new code.
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace StellaOps.Signals.Models;
|
||||
/// </summary>
|
||||
public sealed class EdgeBundleDocument
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Bundle identifier from the DSSE envelope.
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace StellaOps.Signals.Models;
|
||||
|
||||
public sealed class ReachabilityFactDocument
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
public string CallgraphId { get; set; } = string.Empty;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace StellaOps.Signals.Models;
|
||||
/// </summary>
|
||||
public sealed class UnknownSymbolDocument
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
public string SubjectKey { get; set; } = string.Empty;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace StellaOps.Signals.Options;
|
||||
public sealed class ScoreExplanationWeights
|
||||
{
|
||||
/// <summary>
|
||||
/// Multiplier for CVSS base score (10.0 CVSS × 5.0 = 50 points max).
|
||||
/// Multiplier for CVSS base score (10.0 CVSS x 5.0 = 50 points max).
|
||||
/// </summary>
|
||||
public double CvssMultiplier { get; set; } = 5.0;
|
||||
|
||||
@@ -85,7 +85,7 @@ public sealed class ScoreExplanationWeights
|
||||
public double NonDefaultConfigDiscount { get; set; } = -2.0;
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier for EPSS probability (0.0-1.0 → 0-10 points).
|
||||
/// Multiplier for EPSS probability (0.0-1.0 -> 0-10 points).
|
||||
/// </summary>
|
||||
public double EpssMultiplier { get; set; } = 10.0;
|
||||
|
||||
|
||||
@@ -50,6 +50,11 @@ public sealed class SignalsOptions
|
||||
/// </summary>
|
||||
public SignalsRetentionOptions Retention { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// SCM webhook signature validation settings.
|
||||
/// </summary>
|
||||
public SignalsScmWebhookOptions Scm { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Validates configured options.
|
||||
/// </summary>
|
||||
@@ -63,5 +68,6 @@ public sealed class SignalsOptions
|
||||
Events.Validate();
|
||||
OpenApi.Validate();
|
||||
Retention.Validate();
|
||||
Scm.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Signals.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for SCM webhook signature validation.
|
||||
/// </summary>
|
||||
public sealed class SignalsScmWebhookOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows unsigned webhooks when no secret is configured (development only).
|
||||
/// </summary>
|
||||
public bool AllowUnsignedWebhooks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default shared secret used when provider/integration-specific secrets are not set.
|
||||
/// </summary>
|
||||
public string? DefaultSecret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Provider-level secrets keyed by provider name (github/gitlab/gitea).
|
||||
/// </summary>
|
||||
public Dictionary<string, string> ProviderSecrets { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Integration-level secrets keyed by integration ID.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> IntegrationSecrets { get; set; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Validates configured values.
|
||||
/// </summary>
|
||||
public void Validate()
|
||||
{
|
||||
ProviderSecrets ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
IntegrationSecrets ??= new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
ValidateSecrets(ProviderSecrets, "provider");
|
||||
ValidateSecrets(IntegrationSecrets, "integration");
|
||||
}
|
||||
|
||||
private static void ValidateSecrets(Dictionary<string, string> secrets, string scopeName)
|
||||
{
|
||||
foreach (var pair in secrets)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pair.Key))
|
||||
{
|
||||
throw new InvalidOperationException($"Signals SCM {scopeName} secret keys must be non-empty.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pair.Value))
|
||||
{
|
||||
throw new InvalidOperationException($"Signals SCM {scopeName} secret values must be non-empty.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,12 @@ public sealed class InMemoryCallGraphProjectionRepository : ICallGraphProjection
|
||||
private readonly ConcurrentDictionary<(Guid ScanId, string NodeId), NodeRecord> _nodes = new();
|
||||
private readonly ConcurrentDictionary<(Guid ScanId, string FromId, string ToId), EdgeRecord> _edges = new();
|
||||
private readonly ConcurrentDictionary<(Guid ScanId, string NodeId, string Kind), EntrypointRecord> _entrypoints = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryCallGraphProjectionRepository(TimeProvider timeProvider)
|
||||
{
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public Task<bool> UpsertScanAsync(
|
||||
Guid scanId,
|
||||
@@ -35,7 +41,7 @@ public sealed class InMemoryCallGraphProjectionRepository : ICallGraphProjection
|
||||
{
|
||||
if (_scans.TryGetValue(scanId, out var scan))
|
||||
{
|
||||
_scans[scanId] = scan with { Status = "completed", CompletedAt = DateTimeOffset.UtcNow };
|
||||
_scans[scanId] = scan with { Status = "completed", CompletedAt = _timeProvider.GetUtcNow() };
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -44,7 +50,7 @@ public sealed class InMemoryCallGraphProjectionRepository : ICallGraphProjection
|
||||
{
|
||||
if (_scans.TryGetValue(scanId, out var scan))
|
||||
{
|
||||
_scans[scanId] = scan with { Status = "failed", ErrorMessage = errorMessage, CompletedAt = DateTimeOffset.UtcNow };
|
||||
_scans[scanId] = scan with { Status = "failed", ErrorMessage = errorMessage, CompletedAt = _timeProvider.GetUtcNow() };
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
|
||||
namespace StellaOps.Signals.Persistence;
|
||||
@@ -8,6 +9,12 @@ namespace StellaOps.Signals.Persistence;
|
||||
internal sealed class InMemoryCallgraphRepository : ICallgraphRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, CallgraphDocument> _store = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public InMemoryCallgraphRepository(IGuidProvider guidProvider)
|
||||
{
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
}
|
||||
|
||||
public Task<CallgraphDocument> UpsertAsync(CallgraphDocument document, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -15,7 +22,7 @@ internal sealed class InMemoryCallgraphRepository : ICallgraphRepository
|
||||
|
||||
if (string.IsNullOrWhiteSpace(document.Id))
|
||||
{
|
||||
document.Id = Guid.NewGuid().ToString("N");
|
||||
document.Id = _guidProvider.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
_store[document.Id] = Clone(document);
|
||||
|
||||
@@ -79,17 +79,15 @@ public sealed class InMemoryDeploymentRefsRepository : IDeploymentRefsRepository
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task BulkUpsertAsync(IEnumerable<DeploymentRef> deployments, CancellationToken cancellationToken = default)
|
||||
public async Task BulkUpsertAsync(IEnumerable<DeploymentRef> deployments, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ArgumentNullException.ThrowIfNull(deployments);
|
||||
|
||||
foreach (var deployment in deployments)
|
||||
{
|
||||
UpsertAsync(deployment, cancellationToken).GetAwaiter().GetResult();
|
||||
await UpsertAsync(deployment, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<DeploymentSummary?> GetSummaryAsync(string purl, CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -10,6 +10,12 @@ namespace StellaOps.Signals.Persistence;
|
||||
public sealed class InMemoryGraphMetricsRepository : IGraphMetricsRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, GraphMetrics> _metrics = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryGraphMetricsRepository(TimeProvider timeProvider)
|
||||
{
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public void SetMetrics(string symbolId, string callgraphId, GraphMetrics metrics)
|
||||
{
|
||||
@@ -61,7 +67,7 @@ public sealed class InMemoryGraphMetricsRepository : IGraphMetricsRepository
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var cutoff = DateTimeOffset.UtcNow - maxAge;
|
||||
var cutoff = _timeProvider.GetUtcNow() - maxAge;
|
||||
var staleGraphs = _metrics.Values
|
||||
.Where(m => m.ComputedAt < cutoff)
|
||||
.Select(m => m.CallgraphId)
|
||||
|
||||
@@ -8,6 +8,7 @@ using NetEscapades.Configuration.Yaml;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Authentication;
|
||||
using StellaOps.Signals.Hosting;
|
||||
using StellaOps.Signals.Models;
|
||||
@@ -79,7 +80,7 @@ builder.Services.AddOptions<SignalsOptions>()
|
||||
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<SignalsOptions>>().Value);
|
||||
builder.Services.AddSingleton<SignalsStartupState>();
|
||||
builder.Services.AddSingleton(TimeProvider.System);
|
||||
builder.Services.AddDeterminismDefaults();
|
||||
builder.Services.AddSingleton<SignalsSealedModeMonitor>();
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddHealthChecks();
|
||||
@@ -532,6 +533,7 @@ signalsGroup.MapPost("/reachability/union", async Task<IResult> (
|
||||
SignalsOptions options,
|
||||
[FromHeader(Name = "X-Analysis-Id")] string? analysisId,
|
||||
IReachabilityUnionIngestionService ingestionService,
|
||||
IGuidProvider guidProvider,
|
||||
SignalsSealedModeMonitor sealedModeMonitor,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
@@ -545,7 +547,7 @@ signalsGroup.MapPost("/reachability/union", async Task<IResult> (
|
||||
return sealedFailure ?? Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
var id = string.IsNullOrWhiteSpace(analysisId) ? Guid.NewGuid().ToString("N") : analysisId.Trim();
|
||||
var id = string.IsNullOrWhiteSpace(analysisId) ? guidProvider.NewGuid().ToString("N") : analysisId.Trim();
|
||||
|
||||
if (!string.Equals(context.Request.ContentType, "application/zip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Hosting;
|
||||
using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Scm.Models;
|
||||
@@ -48,6 +49,7 @@ public static class ScmWebhookEndpoints
|
||||
HttpContext context,
|
||||
IScmWebhookService webhookService,
|
||||
SignalsSealedModeMonitor sealedModeMonitor,
|
||||
IGuidProvider guidProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure))
|
||||
@@ -57,7 +59,7 @@ public static class ScmWebhookEndpoints
|
||||
|
||||
// Extract GitHub-specific headers
|
||||
var eventType = context.Request.Headers["X-GitHub-Event"].FirstOrDefault() ?? "unknown";
|
||||
var deliveryId = context.Request.Headers["X-GitHub-Delivery"].FirstOrDefault() ?? Guid.NewGuid().ToString("N");
|
||||
var deliveryId = context.Request.Headers["X-GitHub-Delivery"].FirstOrDefault() ?? guidProvider.NewGuid().ToString("N");
|
||||
var signature = context.Request.Headers["X-Hub-Signature-256"].FirstOrDefault();
|
||||
var integrationId = context.Request.Headers["X-Integration-Id"].FirstOrDefault();
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
|
||||
@@ -84,6 +86,7 @@ public static class ScmWebhookEndpoints
|
||||
HttpContext context,
|
||||
IScmWebhookService webhookService,
|
||||
SignalsSealedModeMonitor sealedModeMonitor,
|
||||
IGuidProvider guidProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure))
|
||||
@@ -93,7 +96,7 @@ public static class ScmWebhookEndpoints
|
||||
|
||||
// Extract GitLab-specific headers
|
||||
var eventType = context.Request.Headers["X-Gitlab-Event"].FirstOrDefault() ?? "unknown";
|
||||
var deliveryId = context.Request.Headers["X-Gitlab-Event-UUID"].FirstOrDefault() ?? Guid.NewGuid().ToString("N");
|
||||
var deliveryId = context.Request.Headers["X-Gitlab-Event-UUID"].FirstOrDefault() ?? guidProvider.NewGuid().ToString("N");
|
||||
var signature = context.Request.Headers["X-Gitlab-Token"].FirstOrDefault();
|
||||
var integrationId = context.Request.Headers["X-Integration-Id"].FirstOrDefault();
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
|
||||
@@ -120,6 +123,7 @@ public static class ScmWebhookEndpoints
|
||||
HttpContext context,
|
||||
IScmWebhookService webhookService,
|
||||
SignalsSealedModeMonitor sealedModeMonitor,
|
||||
IGuidProvider guidProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure))
|
||||
@@ -129,7 +133,7 @@ public static class ScmWebhookEndpoints
|
||||
|
||||
// Extract Gitea-specific headers (similar to GitHub)
|
||||
var eventType = context.Request.Headers["X-Gitea-Event"].FirstOrDefault() ?? "unknown";
|
||||
var deliveryId = context.Request.Headers["X-Gitea-Delivery"].FirstOrDefault() ?? Guid.NewGuid().ToString("N");
|
||||
var deliveryId = context.Request.Headers["X-Gitea-Delivery"].FirstOrDefault() ?? guidProvider.NewGuid().ToString("N");
|
||||
var signature = context.Request.Headers["X-Hub-Signature-256"].FirstOrDefault()
|
||||
?? context.Request.Headers["X-Hub-Signature"].FirstOrDefault();
|
||||
var integrationId = context.Request.Headers["X-Integration-Id"].FirstOrDefault();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Scm.Models;
|
||||
|
||||
namespace StellaOps.Signals.Scm.Services;
|
||||
@@ -10,13 +11,16 @@ public sealed class ScmTriggerService : IScmTriggerService
|
||||
{
|
||||
private readonly ILogger<ScmTriggerService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public ScmTriggerService(
|
||||
ILogger<ScmTriggerService> logger,
|
||||
TimeProvider timeProvider)
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider;
|
||||
_guidProvider = guidProvider;
|
||||
}
|
||||
|
||||
public async Task<ScmTriggerResult> ProcessEventAsync(
|
||||
@@ -118,7 +122,7 @@ public sealed class ScmTriggerService : IScmTriggerService
|
||||
private Task<string?> TriggerScanAsync(NormalizedScmEvent scmEvent, CancellationToken cancellationToken)
|
||||
{
|
||||
// Generate a scan ID for tracking
|
||||
var scanId = $"scm-{scmEvent.Provider.ToString().ToLowerInvariant()}-{_timeProvider.GetUtcNow():yyyyMMddHHmmss}-{Guid.NewGuid():N}";
|
||||
var scanId = $"scm-{scmEvent.Provider.ToString().ToLowerInvariant()}-{_timeProvider.GetUtcNow():yyyyMMddHHmmss}-{_guidProvider.NewGuid():N}";
|
||||
|
||||
// In a full implementation, this would:
|
||||
// 1. Call Orchestrator API to enqueue a scan job
|
||||
|
||||
@@ -65,7 +65,19 @@ public sealed class ScmWebhookService : IScmWebhookService
|
||||
|
||||
// Validate signature if webhook secret is configured
|
||||
var secret = GetWebhookSecret(provider, integrationId);
|
||||
if (!string.IsNullOrEmpty(secret))
|
||||
if (string.IsNullOrWhiteSpace(secret))
|
||||
{
|
||||
if (!_options.Scm.AllowUnsignedWebhooks)
|
||||
{
|
||||
_logger.LogWarning("Webhook secret not configured for provider {Provider}, delivery {DeliveryId}", provider, deliveryId);
|
||||
return ScmWebhookResult.Unauthorized("Webhook secret is not configured");
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"Webhook secret not configured for provider {Provider}, accepting unsigned webhook per configuration",
|
||||
provider);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_validators.TryGetValue(provider, out var validator))
|
||||
{
|
||||
@@ -73,16 +85,18 @@ public sealed class ScmWebhookService : IScmWebhookService
|
||||
return ScmWebhookResult.BadRequest($"Unsupported provider: {provider}");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(signature))
|
||||
{
|
||||
_logger.LogWarning("Missing webhook signature for delivery {DeliveryId}", deliveryId);
|
||||
return ScmWebhookResult.Unauthorized("Missing webhook signature");
|
||||
}
|
||||
|
||||
if (!validator.IsValid(payload, signature, secret))
|
||||
{
|
||||
_logger.LogWarning("Invalid webhook signature for delivery {DeliveryId}", deliveryId);
|
||||
return ScmWebhookResult.Unauthorized("Invalid webhook signature");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("No webhook secret configured, skipping signature validation");
|
||||
}
|
||||
|
||||
// Parse payload
|
||||
JsonElement payloadJson;
|
||||
@@ -132,12 +146,27 @@ public sealed class ScmWebhookService : IScmWebhookService
|
||||
|
||||
private string? GetWebhookSecret(ScmProvider provider, string? integrationId)
|
||||
{
|
||||
// In a full implementation, this would look up the secret from:
|
||||
// 1. Integration-specific AuthRef credentials
|
||||
// 2. Provider-specific configuration
|
||||
// 3. Global fallback configuration
|
||||
var scmOptions = _options.Scm;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(integrationId)
|
||||
&& scmOptions.IntegrationSecrets.TryGetValue(integrationId, out var integrationSecret)
|
||||
&& !string.IsNullOrWhiteSpace(integrationSecret))
|
||||
{
|
||||
return integrationSecret;
|
||||
}
|
||||
|
||||
var providerKey = provider.ToString().ToLowerInvariant();
|
||||
if (scmOptions.ProviderSecrets.TryGetValue(providerKey, out var providerSecret)
|
||||
&& !string.IsNullOrWhiteSpace(providerSecret))
|
||||
{
|
||||
return providerSecret;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(scmOptions.DefaultSecret))
|
||||
{
|
||||
return scmOptions.DefaultSecret;
|
||||
}
|
||||
|
||||
// For now, return null to skip validation (development mode)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Parsing;
|
||||
@@ -36,6 +37,7 @@ internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
||||
private readonly ILogger<CallgraphIngestionService> logger;
|
||||
private readonly SignalsOptions options;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly IGuidProvider guidProvider;
|
||||
private static readonly JsonSerializerOptions ManifestSerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public CallgraphIngestionService(
|
||||
@@ -47,6 +49,7 @@ internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
||||
ICallGraphSyncService callGraphSyncService,
|
||||
IOptions<SignalsOptions> options,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
ILogger<CallgraphIngestionService> logger)
|
||||
{
|
||||
this.parserResolver = parserResolver ?? throw new ArgumentNullException(nameof(parserResolver));
|
||||
@@ -57,6 +60,7 @@ internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
||||
this.callGraphSyncService = callGraphSyncService ?? throw new ArgumentNullException(nameof(callGraphSyncService));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
this.guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
this.options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
@@ -168,7 +172,7 @@ internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
||||
// This is triggered post-upsert per SPRINT_3104 requirements
|
||||
var scanId = Guid.TryParse(document.Id, out var parsedScanId)
|
||||
? parsedScanId
|
||||
: Guid.NewGuid();
|
||||
: guidProvider.NewGuid();
|
||||
var artifactDigest = document.Artifact.Hash ?? document.GraphHash ?? document.Id;
|
||||
|
||||
try
|
||||
|
||||
@@ -6,10 +6,12 @@ using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Options;
|
||||
|
||||
@@ -22,6 +24,8 @@ public sealed class EdgeBundleIngestionService : IEdgeBundleIngestionService
|
||||
{
|
||||
private readonly ILogger<EdgeBundleIngestionService> _logger;
|
||||
private readonly SignalsOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
// In-memory storage (in production, would use repository)
|
||||
private readonly ConcurrentDictionary<string, List<EdgeBundleDocument>> _bundlesByGraphHash = new();
|
||||
@@ -34,10 +38,14 @@ public sealed class EdgeBundleIngestionService : IEdgeBundleIngestionService
|
||||
|
||||
public EdgeBundleIngestionService(
|
||||
ILogger<EdgeBundleIngestionService> logger,
|
||||
IOptions<SignalsOptions> options)
|
||||
IOptions<SignalsOptions> options,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
}
|
||||
|
||||
public async Task<EdgeBundleIngestResponse> IngestAsync(
|
||||
@@ -58,7 +66,7 @@ public sealed class EdgeBundleIngestionService : IEdgeBundleIngestionService
|
||||
var root = bundleJson.RootElement;
|
||||
|
||||
// Extract bundle fields
|
||||
var bundleId = GetStringOrDefault(root, "bundleId", $"bundle:{Guid.NewGuid():N}");
|
||||
var bundleId = GetStringOrDefault(root, "bundleId", $"bundle:{_guidProvider.NewGuid():N}");
|
||||
var graphHash = GetStringOrDefault(root, "graphHash", string.Empty);
|
||||
var bundleReason = GetStringOrDefault(root, "bundleReason", "Custom");
|
||||
var customReason = GetStringOrDefault(root, "customReason", null);
|
||||
@@ -69,9 +77,16 @@ public sealed class EdgeBundleIngestionService : IEdgeBundleIngestionService
|
||||
throw new InvalidOperationException("Edge bundle missing required 'graphHash' field");
|
||||
}
|
||||
|
||||
var generatedAt = !string.IsNullOrWhiteSpace(generatedAtStr)
|
||||
? DateTimeOffset.Parse(generatedAtStr)
|
||||
: DateTimeOffset.UtcNow;
|
||||
var generatedAt = _timeProvider.GetUtcNow();
|
||||
if (!string.IsNullOrWhiteSpace(generatedAtStr)
|
||||
&& DateTimeOffset.TryParse(
|
||||
generatedAtStr,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.RoundtripKind,
|
||||
out var parsedGeneratedAt))
|
||||
{
|
||||
generatedAt = parsedGeneratedAt;
|
||||
}
|
||||
|
||||
// Parse edges
|
||||
var edges = new List<EdgeBundleEdgeDocument>();
|
||||
@@ -125,6 +140,7 @@ public sealed class EdgeBundleIngestionService : IEdgeBundleIngestionService
|
||||
// Create document
|
||||
var document = new EdgeBundleDocument
|
||||
{
|
||||
Id = $"edge-bundle:{_guidProvider.NewGuid():N}",
|
||||
BundleId = bundleId,
|
||||
GraphHash = graphHash,
|
||||
TenantId = tenantId,
|
||||
@@ -137,7 +153,7 @@ public sealed class EdgeBundleIngestionService : IEdgeBundleIngestionService
|
||||
DsseCasUri = dsseCasUri,
|
||||
Verified = dsseStream is not null, // Simple verification - in production would verify signature
|
||||
RevokedCount = revokedCount,
|
||||
IngestedAt = DateTimeOffset.UtcNow,
|
||||
IngestedAt = _timeProvider.GetUtcNow(),
|
||||
GeneratedAt = generatedAt
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IFuncProofLinkingService.cs
|
||||
// Sprint: SPRINT_20251226_010_SIGNALS_runtime_stack
|
||||
// Task: STACK-14 — Link to FuncProof: verify observed symbol exists in funcproof
|
||||
// Task: STACK-14 - Link to FuncProof: verify observed symbol exists in funcproof
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.Signals.Services;
|
||||
@@ -276,7 +276,7 @@ public sealed record FuncProofSummary
|
||||
public required int SinkCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of entry→sink traces.
|
||||
/// Number of entry->sink traces.
|
||||
/// </summary>
|
||||
public required int TraceCount { get; init; }
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ISbomCorrelationService.cs
|
||||
// Sprint: SPRINT_20251226_010_SIGNALS_runtime_stack
|
||||
// Task: STACK-13 — Correlate stacks with SBOM: (image-digest, Build-ID, function) → purl
|
||||
// Task: STACK-13 - Correlate stacks with SBOM: (image-digest, Build-ID, function) -> purl
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.Signals.Services;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.Signals.Services;
|
||||
|
||||
@@ -9,13 +10,16 @@ namespace StellaOps.Signals.Services;
|
||||
public sealed class NullSchedulerJobClient : ISchedulerJobClient
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly ILogger<NullSchedulerJobClient> _logger;
|
||||
|
||||
public NullSchedulerJobClient(
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
ILogger<NullSchedulerJobClient> logger)
|
||||
{
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -31,7 +35,7 @@ public sealed class NullSchedulerJobClient : ISchedulerJobClient
|
||||
request.PackageUrl);
|
||||
|
||||
// Generate a fake job ID for testing/development
|
||||
var jobId = $"null-job-{Guid.NewGuid():N}";
|
||||
var jobId = $"null-job-{_guidProvider.NewGuid():N}";
|
||||
var runId = $"null-run-{_timeProvider.GetUtcNow():yyyyMMddHHmmss}";
|
||||
|
||||
return Task.FromResult(SchedulerJobResult.Succeeded(jobId, runId));
|
||||
@@ -50,7 +54,7 @@ public sealed class NullSchedulerJobClient : ISchedulerJobClient
|
||||
var results = requests
|
||||
.Select(r =>
|
||||
{
|
||||
var jobId = $"null-job-{Guid.NewGuid():N}";
|
||||
var jobId = $"null-job-{_guidProvider.NewGuid():N}";
|
||||
var runId = $"null-run-{_timeProvider.GetUtcNow():yyyyMMddHHmmss}";
|
||||
return SchedulerJobResult.Succeeded(jobId, runId);
|
||||
})
|
||||
|
||||
@@ -3,18 +3,13 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Canonicalization.Json;
|
||||
using StellaOps.Signals.Models;
|
||||
|
||||
namespace StellaOps.Signals.Services;
|
||||
|
||||
internal static class ReachabilityFactDigestCalculator
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
public static string Compute(ReachabilityFactDocument fact)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fact);
|
||||
@@ -38,7 +33,7 @@ internal static class ReachabilityFactDigestCalculator
|
||||
UnknownsPressure: fact.UnknownsPressure,
|
||||
ComputedAt: fact.ComputedAt);
|
||||
|
||||
var json = JsonSerializer.Serialize(canonical, SerializerOptions);
|
||||
var json = CanonicalJsonSerializer.Serialize(canonical);
|
||||
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
|
||||
SHA256.HashData(Encoding.UTF8.GetBytes(json), hash);
|
||||
return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant();
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Options;
|
||||
|
||||
@@ -12,11 +13,13 @@ internal sealed class ReachabilityFactEventBuilder
|
||||
{
|
||||
private readonly SignalsOptions options;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly IGuidProvider guidProvider;
|
||||
|
||||
public ReachabilityFactEventBuilder(SignalsOptions options, TimeProvider timeProvider)
|
||||
public ReachabilityFactEventBuilder(SignalsOptions options, TimeProvider timeProvider, IGuidProvider guidProvider)
|
||||
{
|
||||
this.options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
this.guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
}
|
||||
|
||||
public ReachabilityFactUpdatedEnvelope Build(ReachabilityFactDocument fact)
|
||||
@@ -29,7 +32,7 @@ internal sealed class ReachabilityFactEventBuilder
|
||||
|
||||
return new ReachabilityFactUpdatedEnvelope(
|
||||
Topic: ResolveTopic(),
|
||||
EventId: Guid.NewGuid().ToString("n"),
|
||||
EventId: guidProvider.NewGuid().ToString("n"),
|
||||
Version: "signals.fact.updated@v1",
|
||||
EmittedAtUtc: timeProvider.GetUtcNow(),
|
||||
Tenant: ResolveTenant(fact),
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Options;
|
||||
@@ -18,6 +19,7 @@ public sealed class ReachabilityScoringService : IReachabilityScoringService
|
||||
private readonly ICallgraphRepository callgraphRepository;
|
||||
private readonly IReachabilityFactRepository factRepository;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly IGuidProvider guidProvider;
|
||||
private readonly SignalsScoringOptions scoringOptions;
|
||||
private readonly IReachabilityCache cache;
|
||||
private readonly IUnknownsRepository unknownsRepository;
|
||||
@@ -28,6 +30,7 @@ public sealed class ReachabilityScoringService : IReachabilityScoringService
|
||||
ICallgraphRepository callgraphRepository,
|
||||
IReachabilityFactRepository factRepository,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
IOptions<SignalsOptions> options,
|
||||
IReachabilityCache cache,
|
||||
IUnknownsRepository unknownsRepository,
|
||||
@@ -37,6 +40,7 @@ public sealed class ReachabilityScoringService : IReachabilityScoringService
|
||||
this.callgraphRepository = callgraphRepository ?? throw new ArgumentNullException(nameof(callgraphRepository));
|
||||
this.factRepository = factRepository ?? throw new ArgumentNullException(nameof(factRepository));
|
||||
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
this.guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
this.scoringOptions = options?.Value?.Scoring ?? throw new ArgumentNullException(nameof(options));
|
||||
this.cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||
this.unknownsRepository = unknownsRepository ?? throw new ArgumentNullException(nameof(unknownsRepository));
|
||||
@@ -168,8 +172,15 @@ public sealed class ReachabilityScoringService : IReachabilityScoringService
|
||||
scoringOptions.UncertaintyEntropyMultiplier,
|
||||
scoringOptions.UncertaintyBoostCeiling);
|
||||
|
||||
var factId = existingFact?.Id;
|
||||
if (string.IsNullOrWhiteSpace(factId))
|
||||
{
|
||||
factId = guidProvider.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
var document = new ReachabilityFactDocument
|
||||
{
|
||||
Id = factId,
|
||||
CallgraphId = request.CallgraphId,
|
||||
Subject = request.Subject!,
|
||||
EntryPoints = entryPoints,
|
||||
|
||||
@@ -6,14 +6,9 @@ namespace StellaOps.Signals.Services;
|
||||
|
||||
internal sealed class RedisConnectionFactory : IRedisConnectionFactory
|
||||
{
|
||||
public Task<IConnectionMultiplexer> ConnectAsync(ConfigurationOptions options, CancellationToken cancellationToken)
|
||||
public async Task<IConnectionMultiplexer> ConnectAsync(ConfigurationOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return ConnectionMultiplexer.ConnectAsync(options)
|
||||
.ContinueWith(
|
||||
t => (IConnectionMultiplexer)t.Result,
|
||||
cancellationToken,
|
||||
TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously,
|
||||
TaskScheduler.Current);
|
||||
return await ConnectionMultiplexer.ConnectAsync(options).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Storage;
|
||||
@@ -15,6 +16,7 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
|
||||
{
|
||||
private readonly IReachabilityFactRepository factRepository;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly IGuidProvider guidProvider;
|
||||
private readonly IReachabilityCache cache;
|
||||
private readonly IEventsPublisher eventsPublisher;
|
||||
private readonly IReachabilityScoringService scoringService;
|
||||
@@ -26,6 +28,7 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
|
||||
public RuntimeFactsIngestionService(
|
||||
IReachabilityFactRepository factRepository,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
IReachabilityCache cache,
|
||||
IEventsPublisher eventsPublisher,
|
||||
IReachabilityScoringService scoringService,
|
||||
@@ -36,6 +39,7 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
|
||||
{
|
||||
this.factRepository = factRepository ?? throw new ArgumentNullException(nameof(factRepository));
|
||||
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
this.guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
this.cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||
this.eventsPublisher = eventsPublisher ?? throw new ArgumentNullException(nameof(eventsPublisher));
|
||||
this.scoringService = scoringService ?? throw new ArgumentNullException(nameof(scoringService));
|
||||
@@ -56,10 +60,16 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
|
||||
var existing = await factRepository.GetBySubjectAsync(subjectKey, cancellationToken).ConfigureAwait(false);
|
||||
var document = existing ?? new ReachabilityFactDocument
|
||||
{
|
||||
Id = guidProvider.NewGuid().ToString("N"),
|
||||
Subject = request.Subject,
|
||||
SubjectKey = subjectKey,
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(document.Id))
|
||||
{
|
||||
document.Id = guidProvider.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
document.CallgraphId = request.CallgraphId;
|
||||
document.Subject = request.Subject;
|
||||
document.SubjectKey = subjectKey;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
|
||||
namespace StellaOps.Signals.Services;
|
||||
@@ -39,6 +40,12 @@ public sealed class RuntimeFactsProvenanceNormalizer : IRuntimeFactsProvenanceNo
|
||||
{
|
||||
private const string SourceService = "signals-runtime-ingestion";
|
||||
private const double DefaultConfidence = 0.95;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public RuntimeFactsProvenanceNormalizer(IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
public ProvenanceFeed NormalizeToFeed(
|
||||
IEnumerable<RuntimeFactEvent> events,
|
||||
@@ -81,7 +88,7 @@ public sealed class RuntimeFactsProvenanceNormalizer : IRuntimeFactsProvenanceNo
|
||||
return new ProvenanceFeed
|
||||
{
|
||||
SchemaVersion = ProvenanceFeed.CurrentSchemaVersion,
|
||||
FeedId = Guid.NewGuid().ToString("D"),
|
||||
FeedId = _guidProvider.NewGuid().ToString("D"),
|
||||
FeedType = ProvenanceFeedType.RuntimeFacts,
|
||||
GeneratedAt = generatedAt,
|
||||
SourceService = SourceService,
|
||||
@@ -108,7 +115,7 @@ public sealed class RuntimeFactsProvenanceNormalizer : IRuntimeFactsProvenanceNo
|
||||
};
|
||||
}
|
||||
|
||||
private static ProvenanceRecord? NormalizeEvent(
|
||||
private ProvenanceRecord? NormalizeEvent(
|
||||
RuntimeFactEvent evt,
|
||||
ReachabilitySubject subject,
|
||||
string callgraphId,
|
||||
@@ -149,7 +156,7 @@ public sealed class RuntimeFactsProvenanceNormalizer : IRuntimeFactsProvenanceNo
|
||||
|
||||
return new ProvenanceRecord
|
||||
{
|
||||
RecordId = Guid.NewGuid().ToString("D"),
|
||||
RecordId = _guidProvider.NewGuid().ToString("D"),
|
||||
RecordType = recordType,
|
||||
Subject = provenanceSubject,
|
||||
OccurredAt = evt.ObservedAt ?? generatedAt,
|
||||
|
||||
@@ -63,7 +63,7 @@ public sealed class ScoreExplanationService : IScoreExplanationService
|
||||
Weight = weights.CvssMultiplier,
|
||||
RawValue = request.CvssScore.Value,
|
||||
Contribution = cvssContribution,
|
||||
Explanation = $"CVSS base score {request.CvssScore.Value:F1} × {weights.CvssMultiplier:F1} weight",
|
||||
Explanation = $"CVSS base score {request.CvssScore.Value:F1} x {weights.CvssMultiplier:F1} weight",
|
||||
Source = "nvd"
|
||||
});
|
||||
runningTotal += cvssContribution;
|
||||
|
||||
@@ -19,6 +19,7 @@ public sealed class SlimSymbolCache : IDisposable
|
||||
private readonly string? _persistencePath;
|
||||
private readonly SlimSymbolCacheOptions _options;
|
||||
private readonly Timer? _cleanupTimer;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private long _hitCount;
|
||||
private long _missCount;
|
||||
private bool _disposed;
|
||||
@@ -26,9 +27,10 @@ public sealed class SlimSymbolCache : IDisposable
|
||||
/// <summary>
|
||||
/// Creates a new slim symbol cache.
|
||||
/// </summary>
|
||||
public SlimSymbolCache(SlimSymbolCacheOptions options)
|
||||
public SlimSymbolCache(SlimSymbolCacheOptions options, TimeProvider? timeProvider = null)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_persistencePath = options.PersistencePath;
|
||||
|
||||
if (options.EnableAutoCleanup)
|
||||
@@ -75,7 +77,7 @@ public sealed class SlimSymbolCache : IDisposable
|
||||
if (address >= sym.StartAddress && address < sym.StartAddress + sym.Size)
|
||||
{
|
||||
Interlocked.Increment(ref _hitCount);
|
||||
entry.LastAccess = DateTime.UtcNow;
|
||||
entry.LastAccess = _timeProvider.GetUtcNow().UtcDateTime;
|
||||
|
||||
symbol = new CanonicalSymbol
|
||||
{
|
||||
@@ -120,13 +122,14 @@ public sealed class SlimSymbolCache : IDisposable
|
||||
// Sort symbols by start address for binary search
|
||||
var sorted = symbols.OrderBy(s => s.StartAddress).ToList();
|
||||
|
||||
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||
var entry = new SymbolTableEntry
|
||||
{
|
||||
BuildId = buildId,
|
||||
ModuleName = moduleName,
|
||||
Symbols = sorted,
|
||||
LoadedAt = DateTime.UtcNow,
|
||||
LastAccess = DateTime.UtcNow,
|
||||
LoadedAt = now,
|
||||
LastAccess = now,
|
||||
IsTrusted = isTrusted,
|
||||
};
|
||||
|
||||
@@ -291,7 +294,7 @@ public sealed class SlimSymbolCache : IDisposable
|
||||
|
||||
private void Cleanup()
|
||||
{
|
||||
var cutoff = DateTime.UtcNow - _options.EntryTtl;
|
||||
var cutoff = _timeProvider.GetUtcNow().UtcDateTime - _options.EntryTtl;
|
||||
var toRemove = new List<string>();
|
||||
|
||||
foreach (var (buildId, entry) in _cache)
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence;
|
||||
|
||||
@@ -13,12 +14,18 @@ internal sealed class UnknownsIngestionService : IUnknownsIngestionService
|
||||
{
|
||||
private readonly IUnknownsRepository repository;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly IGuidProvider guidProvider;
|
||||
private readonly ILogger<UnknownsIngestionService> logger;
|
||||
|
||||
public UnknownsIngestionService(IUnknownsRepository repository, TimeProvider timeProvider, ILogger<UnknownsIngestionService> logger)
|
||||
public UnknownsIngestionService(
|
||||
IUnknownsRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
ILogger<UnknownsIngestionService> logger)
|
||||
{
|
||||
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
this.guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -70,6 +77,7 @@ internal sealed class UnknownsIngestionService : IUnknownsIngestionService
|
||||
|
||||
normalized.Add(new UnknownSymbolDocument
|
||||
{
|
||||
Id = guidProvider.NewGuid().ToString("N"),
|
||||
SubjectKey = subjectKey,
|
||||
CallgraphId = request.CallgraphId,
|
||||
SymbolId = entry.SymbolId?.Trim(),
|
||||
|
||||
@@ -158,7 +158,7 @@ public sealed class UnknownsScoringService : IUnknownsScoringService
|
||||
unknown.UpdatedAt = now;
|
||||
|
||||
_logger.LogDebug(
|
||||
"Scored unknown {UnknownId}: P={P:F2} E={E:F2} U={U:F2} C={C:F2} S={S:F2} → Score={Score:F2} Band={Band}",
|
||||
"Scored unknown {UnknownId}: P={P:F2} E={E:F2} U={U:F2} C={C:F2} S={S:F2} -> Score={Score:F2} Band={Band}",
|
||||
unknown.Id,
|
||||
unknown.PopularityScore,
|
||||
unknown.ExploitPotentialScore,
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Canonicalization/StellaOps.Canonicalization.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Determinism.Abstractions/StellaOps.Determinism.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
|
||||
<ProjectReference Include="../../Router/__Libraries/StellaOps.Messaging/StellaOps.Messaging.csproj" />
|
||||
|
||||
@@ -13,11 +13,13 @@ public class PoECasStore : IPoECasStore
|
||||
{
|
||||
private readonly string _casRoot;
|
||||
private readonly ILogger<PoECasStore> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public PoECasStore(string casRoot, ILogger<PoECasStore> logger)
|
||||
public PoECasStore(string casRoot, ILogger<PoECasStore> logger, TimeProvider? timeProvider = null)
|
||||
{
|
||||
_casRoot = casRoot ?? throw new ArgumentNullException(nameof(casRoot));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
|
||||
// Ensure CAS root exists
|
||||
if (!Directory.Exists(_casRoot))
|
||||
@@ -57,7 +59,7 @@ public class PoECasStore : IPoECasStore
|
||||
// Write metadata
|
||||
var metadata = new PoEMetadata(
|
||||
PoeHash: poeHash,
|
||||
CreatedAt: DateTime.UtcNow,
|
||||
CreatedAt: _timeProvider.GetUtcNow().UtcDateTime,
|
||||
Size: poeBytes.Length
|
||||
);
|
||||
await WriteMetadataAsync(metaPath, metadata, cancellationToken);
|
||||
|
||||
8
src/Signals/StellaOps.Signals/TASKS.md
Normal file
8
src/Signals/StellaOps.Signals/TASKS.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Signals Task Board
|
||||
|
||||
This board tracks audit remediation tasks for Signals.
|
||||
Source of truth: `docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_apply.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-HOTLIST-SIGNALS-0001 | DONE | Hotlist apply for `src/Signals/StellaOps.Signals/StellaOps.Signals.csproj`; audit tracker updated. |
|
||||
@@ -21,7 +21,7 @@ public sealed class CallGraphSyncServiceTests
|
||||
public async Task SyncAsync_WithValidDocument_ReturnsSuccessResult()
|
||||
{
|
||||
// Arrange
|
||||
var repository = new InMemoryCallGraphProjectionRepository();
|
||||
var repository = new InMemoryCallGraphProjectionRepository(TimeProvider.System);
|
||||
var service = new CallGraphSyncService(
|
||||
repository,
|
||||
TimeProvider.System,
|
||||
@@ -47,7 +47,7 @@ public sealed class CallGraphSyncServiceTests
|
||||
public async Task SyncAsync_ProjectsToRepository()
|
||||
{
|
||||
// Arrange
|
||||
var repository = new InMemoryCallGraphProjectionRepository();
|
||||
var repository = new InMemoryCallGraphProjectionRepository(TimeProvider.System);
|
||||
var service = new CallGraphSyncService(
|
||||
repository,
|
||||
TimeProvider.System,
|
||||
@@ -71,7 +71,7 @@ public sealed class CallGraphSyncServiceTests
|
||||
public async Task SyncAsync_SetsScanStatusToCompleted()
|
||||
{
|
||||
// Arrange
|
||||
var repository = new InMemoryCallGraphProjectionRepository();
|
||||
var repository = new InMemoryCallGraphProjectionRepository(TimeProvider.System);
|
||||
var service = new CallGraphSyncService(
|
||||
repository,
|
||||
TimeProvider.System,
|
||||
@@ -93,7 +93,7 @@ public sealed class CallGraphSyncServiceTests
|
||||
public async Task SyncAsync_WithEmptyDocument_ReturnsZeroCounts()
|
||||
{
|
||||
// Arrange
|
||||
var repository = new InMemoryCallGraphProjectionRepository();
|
||||
var repository = new InMemoryCallGraphProjectionRepository(TimeProvider.System);
|
||||
var service = new CallGraphSyncService(
|
||||
repository,
|
||||
TimeProvider.System,
|
||||
@@ -125,7 +125,7 @@ public sealed class CallGraphSyncServiceTests
|
||||
public async Task SyncAsync_WithNullDocument_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var repository = new InMemoryCallGraphProjectionRepository();
|
||||
var repository = new InMemoryCallGraphProjectionRepository(TimeProvider.System);
|
||||
var service = new CallGraphSyncService(
|
||||
repository,
|
||||
TimeProvider.System,
|
||||
@@ -141,7 +141,7 @@ public sealed class CallGraphSyncServiceTests
|
||||
public async Task SyncAsync_WithEmptyArtifactDigest_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
var repository = new InMemoryCallGraphProjectionRepository();
|
||||
var repository = new InMemoryCallGraphProjectionRepository(TimeProvider.System);
|
||||
var service = new CallGraphSyncService(
|
||||
repository,
|
||||
TimeProvider.System,
|
||||
@@ -159,7 +159,7 @@ public sealed class CallGraphSyncServiceTests
|
||||
public async Task DeleteByScanAsync_RemovesScanFromRepository()
|
||||
{
|
||||
// Arrange
|
||||
var repository = new InMemoryCallGraphProjectionRepository();
|
||||
var repository = new InMemoryCallGraphProjectionRepository(TimeProvider.System);
|
||||
var service = new CallGraphSyncService(
|
||||
repository,
|
||||
TimeProvider.System,
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Parsing;
|
||||
@@ -46,6 +47,7 @@ public class CallgraphIngestionServiceTests
|
||||
callGraphSyncService,
|
||||
options,
|
||||
_timeProvider,
|
||||
SystemGuidProvider.Instance,
|
||||
NullLogger<CallgraphIngestionService>.Instance);
|
||||
|
||||
var artifactJson = @"{""nodes"":[{""id"":""com/example/Foo.bar:(I)V"",""kind"":""fn""}],
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
@@ -24,7 +25,11 @@ public class EdgeBundleIngestionServiceTests
|
||||
var opts = new SignalsOptions();
|
||||
opts.Storage.RootPath = Path.GetTempPath();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(opts);
|
||||
_service = new EdgeBundleIngestionService(NullLogger<EdgeBundleIngestionService>.Instance, options);
|
||||
_service = new EdgeBundleIngestionService(
|
||||
NullLogger<EdgeBundleIngestionService>.Instance,
|
||||
options,
|
||||
TimeProvider.System,
|
||||
SystemGuidProvider.Instance);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Signals.EvidenceWeightedScore;
|
||||
@@ -30,7 +30,7 @@ public class EvidenceWeightedScoreCalculatorTests
|
||||
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
|
||||
// Without MIT, sum of weights = 0.95 (default) → 95%
|
||||
// Without MIT, sum of weights = 0.95 (default) -> 95%
|
||||
result.Score.Should().BeGreaterThanOrEqualTo(90);
|
||||
result.Bucket.Should().Be(ScoreBucket.ActNow);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -290,7 +290,7 @@ public class NormalizerAggregatorTests
|
||||
};
|
||||
var optionsMonitor = new TestOptionsMonitor(options);
|
||||
|
||||
var aggregator = new NormalizerAggregator(optionsMonitor);
|
||||
var aggregator = new NormalizerAggregator(optionsMonitor, TimeProvider.System);
|
||||
|
||||
var evidence = new FindingEvidence
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -15,7 +15,7 @@ namespace StellaOps.Signals.Tests.EvidenceWeightedScore.Normalizers;
|
||||
/// </summary>
|
||||
public class NormalizerIntegrationTests
|
||||
{
|
||||
#region Backport Evidence → BKP Score Tests
|
||||
#region Backport Evidence -> BKP Score Tests
|
||||
|
||||
[Fact]
|
||||
public void BackportEvidence_PatchSignatureFixed_ProducesHighBkpScore()
|
||||
@@ -80,7 +80,7 @@ public class NormalizerIntegrationTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region EPSS + KEV → XPL Score Tests
|
||||
#region EPSS + KEV -> XPL Score Tests
|
||||
|
||||
[Fact]
|
||||
public void ExploitEvidence_HighEpssAndKev_ProducesHighXplScore()
|
||||
@@ -147,7 +147,7 @@ public class NormalizerIntegrationTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region Full Evidence Pipeline → Score Input Tests
|
||||
#region Full Evidence Pipeline -> Score Input Tests
|
||||
|
||||
[Fact]
|
||||
public void FullEvidence_AllDimensions_ProducesValidScoreInput()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -19,7 +19,7 @@ public class RuntimeSignalNormalizerTests
|
||||
|
||||
public RuntimeSignalNormalizerTests()
|
||||
{
|
||||
_sut = new RuntimeSignalNormalizer(_defaultOptions);
|
||||
_sut = new RuntimeSignalNormalizer(_defaultOptions, TimeProvider.System);
|
||||
}
|
||||
|
||||
#region Dimension Property Tests
|
||||
@@ -517,7 +517,7 @@ public class RuntimeSignalNormalizerTests
|
||||
};
|
||||
var optionsMonitor = new TestOptionsMonitor(options);
|
||||
|
||||
var normalizer = new RuntimeSignalNormalizer(optionsMonitor);
|
||||
var normalizer = new RuntimeSignalNormalizer(optionsMonitor, TimeProvider.System);
|
||||
|
||||
var input = new RuntimeInput
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Services;
|
||||
@@ -22,7 +23,7 @@ public class InMemoryEventsPublisherTests
|
||||
options.Events.Stream = "signals.fact.updated.v1";
|
||||
options.Events.DefaultTenant = "tenant-default";
|
||||
|
||||
var builder = new ReachabilityFactEventBuilder(options, TimeProvider.System);
|
||||
var builder = new ReachabilityFactEventBuilder(options, TimeProvider.System, SystemGuidProvider.Instance);
|
||||
var publisher = new InMemoryEventsPublisher(logger, builder);
|
||||
|
||||
var fact = new ReachabilityFactDocument
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Persistence;
|
||||
@@ -64,6 +65,7 @@ public class ReachabilityScoringServiceTests
|
||||
callgraphRepository,
|
||||
factRepository,
|
||||
TimeProvider.System,
|
||||
SystemGuidProvider.Instance,
|
||||
Options.Create(options),
|
||||
cache,
|
||||
unknowns,
|
||||
@@ -135,6 +137,7 @@ public class ReachabilityScoringServiceTests
|
||||
callgraphRepository,
|
||||
factRepository,
|
||||
TimeProvider.System,
|
||||
SystemGuidProvider.Instance,
|
||||
Options.Create(options),
|
||||
cache,
|
||||
unknowns,
|
||||
@@ -220,6 +223,7 @@ public class ReachabilityScoringServiceTests
|
||||
callgraphRepository,
|
||||
factRepository,
|
||||
TimeProvider.System,
|
||||
SystemGuidProvider.Instance,
|
||||
Options.Create(options),
|
||||
cache,
|
||||
unknowns,
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Services;
|
||||
@@ -25,7 +26,7 @@ public class RouterEventsPublisherTests
|
||||
var handler = new StubHandler(HttpStatusCode.Accepted);
|
||||
using var httpClient = new HttpClient(handler) { BaseAddress = new Uri(options.Events.Router.BaseUrl) };
|
||||
var logger = new ListLogger<RouterEventsPublisher>();
|
||||
var builder = new ReachabilityFactEventBuilder(options, TimeProvider.System);
|
||||
var builder = new ReachabilityFactEventBuilder(options, TimeProvider.System, SystemGuidProvider.Instance);
|
||||
var publisher = new RouterEventsPublisher(builder, options, httpClient, logger);
|
||||
|
||||
await publisher.PublishFactUpdatedAsync(CreateFact(), CancellationToken.None);
|
||||
@@ -49,7 +50,7 @@ public class RouterEventsPublisherTests
|
||||
var handler = new StubHandler(HttpStatusCode.InternalServerError, "boom");
|
||||
using var httpClient = new HttpClient(handler) { BaseAddress = new Uri(options.Events.Router.BaseUrl) };
|
||||
var logger = new ListLogger<RouterEventsPublisher>();
|
||||
var builder = new ReachabilityFactEventBuilder(options, TimeProvider.System);
|
||||
var builder = new ReachabilityFactEventBuilder(options, TimeProvider.System, SystemGuidProvider.Instance);
|
||||
var publisher = new RouterEventsPublisher(builder, options, httpClient, logger);
|
||||
|
||||
await publisher.PublishFactUpdatedAsync(CreateFact(), CancellationToken.None);
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Services;
|
||||
@@ -207,6 +208,7 @@ public class RuntimeFactsBatchIngestionTests
|
||||
return new RuntimeFactsIngestionService(
|
||||
repository,
|
||||
TimeProvider.System,
|
||||
SystemGuidProvider.Instance,
|
||||
cache,
|
||||
eventsPublisher,
|
||||
scoringService,
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Services;
|
||||
@@ -25,6 +26,7 @@ public class RuntimeFactsIngestionServiceTests
|
||||
var service = new RuntimeFactsIngestionService(
|
||||
factRepository,
|
||||
TimeProvider.System,
|
||||
SystemGuidProvider.Instance,
|
||||
cache,
|
||||
eventsPublisher,
|
||||
scoringService,
|
||||
@@ -446,6 +448,7 @@ public class RuntimeFactsIngestionServiceTests
|
||||
return new RuntimeFactsIngestionService(
|
||||
factRepository,
|
||||
TimeProvider.System,
|
||||
SystemGuidProvider.Instance,
|
||||
new InMemoryReachabilityCache(),
|
||||
new RecordingEventsPublisher(),
|
||||
new RecordingScoringService(),
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
// <copyright file="ScmWebhookServiceTests.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Scm.Models;
|
||||
using StellaOps.Signals.Scm.Services;
|
||||
using StellaOps.Signals.Scm.Webhooks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Tests.Scm;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SCM webhook processing.
|
||||
/// </summary>
|
||||
public sealed class ScmWebhookServiceTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedTimestamp =
|
||||
DateTimeOffset.Parse("2025-01-01T00:00:00Z", CultureInfo.InvariantCulture);
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_RejectsWhenSecretMissingAndUnsignedNotAllowed()
|
||||
{
|
||||
var options = new SignalsOptions();
|
||||
options.Scm.AllowUnsignedWebhooks = false;
|
||||
|
||||
var triggerService = new TestScmTriggerService();
|
||||
var service = CreateService(options, triggerService);
|
||||
|
||||
var payload = Encoding.UTF8.GetBytes("{}");
|
||||
|
||||
var result = await service.ProcessAsync(
|
||||
ScmProvider.GitHub,
|
||||
eventType: "push",
|
||||
deliveryId: "delivery-1",
|
||||
signature: null,
|
||||
payload: payload,
|
||||
integrationId: null,
|
||||
tenantId: null);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(401, result.StatusCode);
|
||||
Assert.Equal(0, triggerService.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_AllowsUnsignedWhenConfigured()
|
||||
{
|
||||
var options = new SignalsOptions();
|
||||
options.Scm.AllowUnsignedWebhooks = true;
|
||||
|
||||
var triggerService = new TestScmTriggerService();
|
||||
var service = CreateService(options, triggerService);
|
||||
|
||||
var payload = Encoding.UTF8.GetBytes("{}");
|
||||
|
||||
var result = await service.ProcessAsync(
|
||||
ScmProvider.GitHub,
|
||||
eventType: "push",
|
||||
deliveryId: "delivery-2",
|
||||
signature: null,
|
||||
payload: payload,
|
||||
integrationId: "integration-1",
|
||||
tenantId: "tenant-1");
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(202, result.StatusCode);
|
||||
Assert.Equal(1, triggerService.Calls);
|
||||
Assert.Equal("integration-1", result.Event?.IntegrationId);
|
||||
Assert.Equal("tenant-1", result.Event?.TenantId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_ValidSignature_AcceptsAndDispatches()
|
||||
{
|
||||
var options = new SignalsOptions();
|
||||
options.Scm.DefaultSecret = "test-secret";
|
||||
|
||||
var triggerService = new TestScmTriggerService();
|
||||
var service = CreateService(options, triggerService);
|
||||
|
||||
var payload = Encoding.UTF8.GetBytes("{}");
|
||||
var signature = ComputeGitHubSignature(payload, options.Scm.DefaultSecret);
|
||||
|
||||
var result = await service.ProcessAsync(
|
||||
ScmProvider.GitHub,
|
||||
eventType: "push",
|
||||
deliveryId: "delivery-3",
|
||||
signature: signature,
|
||||
payload: payload,
|
||||
integrationId: null,
|
||||
tenantId: null);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(202, result.StatusCode);
|
||||
Assert.Equal(1, triggerService.Calls);
|
||||
Assert.Equal("delivery-3", result.Event?.EventId);
|
||||
}
|
||||
|
||||
private static ScmWebhookService CreateService(SignalsOptions options, TestScmTriggerService triggerService)
|
||||
{
|
||||
return new ScmWebhookService(
|
||||
NullLogger<ScmWebhookService>.Instance,
|
||||
Options.Create(options),
|
||||
triggerService,
|
||||
new IWebhookSignatureValidator[] { new GitHubWebhookValidator() },
|
||||
new IScmEventMapper[] { new TestScmEventMapper(ScmProvider.GitHub) });
|
||||
}
|
||||
|
||||
private static string ComputeGitHubSignature(byte[] payload, string secret)
|
||||
{
|
||||
var secretBytes = Encoding.UTF8.GetBytes(secret);
|
||||
var hash = HMACSHA256.HashData(secretBytes, payload);
|
||||
return $"sha256={Convert.ToHexStringLower(hash)}";
|
||||
}
|
||||
|
||||
private sealed class TestScmEventMapper : IScmEventMapper
|
||||
{
|
||||
public TestScmEventMapper(ScmProvider provider)
|
||||
{
|
||||
Provider = provider;
|
||||
}
|
||||
|
||||
public ScmProvider Provider { get; }
|
||||
|
||||
public NormalizedScmEvent? Map(string eventType, string deliveryId, JsonElement payload)
|
||||
{
|
||||
return new NormalizedScmEvent
|
||||
{
|
||||
EventId = deliveryId,
|
||||
Provider = Provider,
|
||||
EventType = ScmEventType.Push,
|
||||
Timestamp = FixedTimestamp,
|
||||
Repository = new ScmRepository
|
||||
{
|
||||
FullName = "stellaops/signals"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestScmTriggerService : IScmTriggerService
|
||||
{
|
||||
public int Calls { get; private set; }
|
||||
|
||||
public Task<ScmTriggerResult> ProcessEventAsync(NormalizedScmEvent scmEvent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Calls++;
|
||||
return Task.FromResult(new ScmTriggerResult
|
||||
{
|
||||
TriggersDispatched = true,
|
||||
ScanTriggersCount = 1,
|
||||
SbomTriggersCount = 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ public class UnknownsDecayServiceTests
|
||||
_timeProvider = new MockTimeProvider(new DateTimeOffset(2025, 12, 15, 12, 0, 0, TimeSpan.Zero));
|
||||
_unknownsRepo = new InMemoryUnknownsRepository();
|
||||
_deploymentRefs = new InMemoryDeploymentRefsRepository();
|
||||
_graphMetrics = new InMemoryGraphMetricsRepository();
|
||||
_graphMetrics = new InMemoryGraphMetricsRepository(TimeProvider.System);
|
||||
_scoringOptions = new UnknownsScoringOptions();
|
||||
_decayOptions = new UnknownsDecayOptions();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Services;
|
||||
@@ -17,7 +18,7 @@ public class UnknownsIngestionServiceTests
|
||||
public async Task IngestAsync_StoresNormalizedUnknowns()
|
||||
{
|
||||
var repo = new InMemoryUnknownsRepository();
|
||||
var service = new UnknownsIngestionService(repo, TimeProvider.System, NullLogger<UnknownsIngestionService>.Instance);
|
||||
var service = new UnknownsIngestionService(repo, TimeProvider.System, SystemGuidProvider.Instance, NullLogger<UnknownsIngestionService>.Instance);
|
||||
|
||||
var request = new UnknownsIngestRequest
|
||||
{
|
||||
@@ -49,7 +50,7 @@ public class UnknownsIngestionServiceTests
|
||||
public async Task IngestAsync_ThrowsWhenEmpty()
|
||||
{
|
||||
var repo = new InMemoryUnknownsRepository();
|
||||
var service = new UnknownsIngestionService(repo, TimeProvider.System, NullLogger<UnknownsIngestionService>.Instance);
|
||||
var service = new UnknownsIngestionService(repo, TimeProvider.System, SystemGuidProvider.Instance, NullLogger<UnknownsIngestionService>.Instance);
|
||||
|
||||
var request = new UnknownsIngestRequest
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace StellaOps.Signals.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the unknowns scoring system.
|
||||
/// Tests end-to-end flow: ingest → score → persist → query.
|
||||
/// Tests end-to-end flow: ingest -> score -> persist -> query.
|
||||
/// </summary>
|
||||
public sealed class UnknownsScoringIntegrationTests
|
||||
{
|
||||
@@ -32,7 +32,7 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
_timeProvider = new MockTimeProvider(new DateTimeOffset(2025, 12, 15, 12, 0, 0, TimeSpan.Zero));
|
||||
_unknownsRepo = new FullInMemoryUnknownsRepository(_timeProvider);
|
||||
_deploymentRefs = new InMemoryDeploymentRefsRepository();
|
||||
_graphMetrics = new InMemoryGraphMetricsRepository();
|
||||
_graphMetrics = new InMemoryGraphMetricsRepository(TimeProvider.System);
|
||||
_defaultOptions = new UnknownsScoringOptions();
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ public class UnknownsScoringServiceTests
|
||||
_timeProvider = new MockTimeProvider(new DateTimeOffset(2025, 12, 15, 12, 0, 0, TimeSpan.Zero));
|
||||
_unknownsRepo = new InMemoryUnknownsRepository();
|
||||
_deploymentRefs = new InMemoryDeploymentRefsRepository();
|
||||
_graphMetrics = new InMemoryGraphMetricsRepository();
|
||||
_graphMetrics = new InMemoryGraphMetricsRepository(TimeProvider.System);
|
||||
_defaultOptions = new UnknownsScoringOptions();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user