audit, advisories and doctors/setup work

This commit is contained in:
master
2026-01-13 18:53:39 +02:00
parent 9ca7cb183e
commit d7be6ba34b
811 changed files with 54242 additions and 4056 deletions

View File

@@ -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;

View File

@@ -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,

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
// Copyright (c) 2025 StellaOps
namespace StellaOps.Signals.EvidenceWeightedScore;

View File

@@ -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()
{

View File

@@ -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);

View File

@@ -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)
{

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
// Copyright (c) 2025 StellaOps
namespace StellaOps.Signals.EvidenceWeightedScore;

View File

@@ -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

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
// Copyright (c) 2025 StellaOps
namespace StellaOps.Signals.EvidenceWeightedScore;

View File

@@ -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()
};
}
}

View File

@@ -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)

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
// Copyright (c) 2025 StellaOps
using Microsoft.Extensions.Options;

View File

@@ -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>();

View File

@@ -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.901.00
/// - Top 5% (95th percentile): 0.700.89
/// - Top 25% (75th percentile): 0.400.69
/// - Below 75th percentile: 0.200.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)

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
// Copyright (c) 2025 StellaOps
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
// Copyright (c) 2025 StellaOps
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;

View File

@@ -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})";
}

View File

@@ -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)
{
}

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
// Copyright (c) 2025 StellaOps
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
// Copyright (c) 2025 StellaOps
namespace StellaOps.Signals.EvidenceWeightedScore;

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
// Copyright (c) 2025 StellaOps
namespace StellaOps.Signals.EvidenceWeightedScore;

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
// Copyright (c) 2025 StellaOps
namespace StellaOps.Signals.EvidenceWeightedScore;

View File

@@ -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;

View File

@@ -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.

View File

@@ -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.

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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.");
}
}
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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)

View File

@@ -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)

View File

@@ -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))
{

View File

@@ -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();

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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
};

View File

@@ -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 entrysink traces.
/// Number of entry->sink traces.
/// </summary>
public required int TraceCount { get; init; }

View File

@@ -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;

View File

@@ -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);
})

View File

@@ -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();

View File

@@ -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),

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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)

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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" />

View File

@@ -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);

View 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. |

View File

@@ -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,

View File

@@ -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""}],

View File

@@ -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)]

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
{

View File

@@ -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()

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
{

View File

@@ -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;

View File

@@ -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

View File

@@ -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,

View File

@@ -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);

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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
});
}
}
}

View File

@@ -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();
}

View File

@@ -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
{

View File

@@ -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();
}

View File

@@ -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();
}