work
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-25 08:01:23 +02:00
parent d92973d6fd
commit 6bee1fdcf5
207 changed files with 12816 additions and 2295 deletions

View File

@@ -1,6 +0,0 @@
namespace StellaOps.RiskEngine.Core;
public class Class1
{
}

View File

@@ -0,0 +1,14 @@
namespace StellaOps.RiskEngine.Core.Contracts;
/// <summary>
/// Result of a risk score computation.
/// </summary>
public sealed record RiskScoreResult(
Guid JobId,
string Provider,
string Subject,
double Score,
bool Success,
string? Error,
IReadOnlyDictionary<string, double> Signals,
DateTimeOffset CompletedAtUtc);

View File

@@ -0,0 +1,14 @@
namespace StellaOps.RiskEngine.Core.Contracts;
/// <summary>
/// Input for a risk score computation. Subject is an opaque asset/id; Signals are deterministic numeric factors.
/// </summary>
public sealed record ScoreRequest(
string Provider,
string Subject,
IReadOnlyDictionary<string, double> Signals);
/// <summary>
/// Job envelope carried through the queue.
/// </summary>
public sealed record RiskScoreJob(Guid JobId, ScoreRequest Request);

View File

@@ -0,0 +1,37 @@
using StellaOps.RiskEngine.Core.Contracts;
namespace StellaOps.RiskEngine.Core.Providers;
/// <summary>
/// Risk provider that derives score from CVSS base score and KEV flag.
/// Score formula: clamp01((cvss/10) + kevBonus), where kevBonus = 0.2 if KEV, else 0.
/// </summary>
public sealed class CvssKevProvider : IRiskScoreProvider
{
public const string ProviderName = "cvss-kev";
private readonly ICvssSource cvss;
private readonly IKevSource kev;
public CvssKevProvider(ICvssSource cvss, IKevSource kev)
{
this.cvss = cvss ?? throw new ArgumentNullException(nameof(cvss));
this.kev = kev ?? throw new ArgumentNullException(nameof(kev));
}
public string Name => ProviderName;
public async Task<double> ScoreAsync(ScoreRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var cvssScore = await cvss.GetCvssAsync(request.Subject, cancellationToken).ConfigureAwait(false) ?? 0d;
cvssScore = Math.Clamp(cvssScore, 0d, 10d);
var kevFlag = await kev.IsKevAsync(request.Subject, cancellationToken).ConfigureAwait(false) ?? false;
var kevBonus = kevFlag ? 0.2d : 0d;
var raw = (cvssScore / 10d) + kevBonus;
return Math.Round(Math.Min(1d, raw), 6, MidpointRounding.ToEven);
}
}

View File

@@ -0,0 +1,40 @@
using StellaOps.RiskEngine.Core.Contracts;
namespace StellaOps.RiskEngine.Core.Providers;
/// <summary>
/// Default provider that clamps each signal to [0,1] and averages the result.
/// Deterministic and side-effect free.
/// </summary>
public sealed class DefaultTransformsProvider : IRiskScoreProvider
{
public const string ProviderName = "default-transforms";
public string Name => ProviderName;
public Task<double> ScoreAsync(ScoreRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
if (request.Signals.Count == 0)
{
return Task.FromResult(0d);
}
var sum = 0d;
foreach (var kvp in request.Signals.OrderBy(k => k.Key, StringComparer.Ordinal))
{
sum += Clamp01(kvp.Value);
}
var average = sum / request.Signals.Count;
return Task.FromResult(Math.Round(average, 6, MidpointRounding.ToEven));
}
private static double Clamp01(double value) =>
value switch
{
< 0d => 0d,
> 1d => 1d,
_ => value
};
}

View File

@@ -0,0 +1,33 @@
using StellaOps.RiskEngine.Core.Contracts;
namespace StellaOps.RiskEngine.Core.Providers;
/// <summary>
/// Combines fix availability, asset criticality, and internet exposure into a bounded score.
/// Formula: clamp01(0.5 * FixAvailability + 0.3 * Criticality + 0.2 * Exposure).
/// Inputs are expected in [0,1]; missing keys default to 0.
/// </summary>
public sealed class FixExposureProvider : IRiskScoreProvider
{
public const string ProviderName = "fix-exposure";
public string Name => ProviderName;
public Task<double> ScoreAsync(ScoreRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var fix = Get(request, "FixAvailability");
var crit = Get(request, "Criticality");
var exposure = Get(request, "Exposure");
var weighted = (0.5 * fix) + (0.3 * crit) + (0.2 * exposure);
var score = Math.Round(Math.Clamp(weighted, 0d, 1d), 6, MidpointRounding.ToEven);
return Task.FromResult(score);
}
private static double Get(ScoreRequest request, string key) =>
request.Signals.TryGetValue(key, out var value)
? Math.Clamp(value, 0d, 1d)
: 0d;
}

View File

@@ -0,0 +1,35 @@
namespace StellaOps.RiskEngine.Core.Providers;
public interface ICvssSource
{
/// <summary>
/// Returns CVSS base score (0-10) for the subject, or null if unknown.
/// </summary>
Task<double?> GetCvssAsync(string subject, CancellationToken cancellationToken);
}
public interface IKevSource
{
/// <summary>
/// Returns true if the subject is marked as known exploited (KEV), false otherwise, null if unknown.
/// </summary>
Task<bool?> IsKevAsync(string subject, CancellationToken cancellationToken);
}
/// <summary>
/// Null-object CVSS source returning no score (treat as unknown).
/// </summary>
public sealed class NullCvssSource : ICvssSource
{
public Task<double?> GetCvssAsync(string subject, CancellationToken cancellationToken) =>
Task.FromResult<double?>(null);
}
/// <summary>
/// Null-object KEV source returning false.
/// </summary>
public sealed class NullKevSource : IKevSource
{
public Task<bool?> IsKevAsync(string subject, CancellationToken cancellationToken) =>
Task.FromResult<bool?>(false);
}

View File

@@ -0,0 +1,44 @@
using StellaOps.RiskEngine.Core.Contracts;
namespace StellaOps.RiskEngine.Core.Providers;
/// <summary>
/// Computes a risk score for a request. Implementations must be deterministic for identical inputs.
/// </summary>
public interface IRiskScoreProvider
{
string Name { get; }
Task<double> ScoreAsync(ScoreRequest request, CancellationToken cancellationToken);
}
public interface IRiskScoreProviderRegistry
{
bool TryGet(string name, out IRiskScoreProvider provider);
IReadOnlyCollection<string> ProviderNames { get; }
}
/// <summary>
/// Simple in-memory provider registry.
/// </summary>
public sealed class RiskScoreProviderRegistry : IRiskScoreProviderRegistry
{
private readonly IReadOnlyDictionary<string, IRiskScoreProvider> providers;
public RiskScoreProviderRegistry(IEnumerable<IRiskScoreProvider> providers)
{
var map = new Dictionary<string, IRiskScoreProvider>(StringComparer.OrdinalIgnoreCase);
foreach (var provider in providers)
{
map[provider.Name] = provider;
}
this.providers = map;
}
public bool TryGet(string name, out IRiskScoreProvider provider) =>
providers.TryGetValue(name, out provider!);
public IReadOnlyCollection<string> ProviderNames => providers.Keys.ToArray();
}

View File

@@ -0,0 +1,35 @@
using StellaOps.RiskEngine.Core.Contracts;
namespace StellaOps.RiskEngine.Core.Providers;
/// <summary>
/// VEX gate provider that short-circuits scoring when a denial is present.
/// Signals are ignored when <c>HasDenial</c> is true.
/// </summary>
public sealed class VexGateProvider : IRiskScoreProvider
{
public const string ProviderName = "vex-gate";
public string Name => ProviderName;
public Task<double> ScoreAsync(ScoreRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var hasDenial = request.Signals.TryGetValue("HasDenial", out var denialFlag) && denialFlag >= 1;
if (hasDenial)
{
return Task.FromResult(0d);
}
// Fall back to simple max of remaining signals (if any)
var max = request.Signals
.Where(kvp => !string.Equals(kvp.Key, "HasDenial", StringComparison.Ordinal))
.Select(kvp => kvp.Value)
.DefaultIfEmpty(0d)
.Max();
var score = Math.Clamp(max, 0d, 1d);
return Task.FromResult(Math.Round(score, 6, MidpointRounding.ToEven));
}
}

View File

@@ -0,0 +1,12 @@
using StellaOps.RiskEngine.Core.Contracts;
namespace StellaOps.RiskEngine.Core.Services;
/// <summary>
/// Persists risk score results for later retrieval/ledger projection.
/// Implementations must be deterministic and sideeffect free for identical inputs.
/// </summary>
public interface IRiskScoreResultStore
{
Task SaveAsync(RiskScoreResult result, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,52 @@
using System.Threading.Channels;
using StellaOps.RiskEngine.Core.Contracts;
using System.Diagnostics.CodeAnalysis;
namespace StellaOps.RiskEngine.Core.Services;
/// <summary>
/// Deterministic FIFO queue for risk score jobs.
/// </summary>
public sealed class RiskScoreQueue
{
private readonly Channel<RiskScoreJob> channel;
public RiskScoreQueue(int? capacity = null)
{
if (capacity.HasValue)
{
var options = new BoundedChannelOptions(capacity.Value)
{
AllowSynchronousContinuations = false,
SingleReader = true,
SingleWriter = false,
FullMode = BoundedChannelFullMode.Wait
};
channel = Channel.CreateBounded<RiskScoreJob>(options);
}
else
{
var options = new UnboundedChannelOptions
{
AllowSynchronousContinuations = false,
SingleReader = true,
SingleWriter = false
};
channel = Channel.CreateUnbounded<RiskScoreJob>(options);
}
}
public ValueTask EnqueueAsync(ScoreRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var job = new RiskScoreJob(Guid.NewGuid(), request);
return channel.Writer.WriteAsync(job, cancellationToken);
}
public ValueTask<RiskScoreJob> DequeueAsync(CancellationToken cancellationToken) =>
channel.Reader.ReadAsync(cancellationToken);
public bool TryDequeue([NotNullWhen(true)] out RiskScoreJob? job) => channel.Reader.TryRead(out job);
}

View File

@@ -0,0 +1,86 @@
using StellaOps.RiskEngine.Core.Contracts;
using StellaOps.RiskEngine.Core.Providers;
namespace StellaOps.RiskEngine.Core.Services;
/// <summary>
/// Single-reader worker that pulls jobs from the queue and executes providers deterministically.
/// </summary>
public sealed class RiskScoreWorker
{
private readonly RiskScoreQueue queue;
private readonly IRiskScoreProviderRegistry registry;
private readonly IRiskScoreResultStore? resultStore;
private readonly TimeProvider timeProvider;
public RiskScoreWorker(
RiskScoreQueue queue,
IRiskScoreProviderRegistry registry,
IRiskScoreResultStore? resultStore = null,
TimeProvider? timeProvider = null)
{
this.queue = queue ?? throw new ArgumentNullException(nameof(queue));
this.registry = registry ?? throw new ArgumentNullException(nameof(registry));
this.resultStore = resultStore;
this.timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<RiskScoreResult> ProcessNextAsync(CancellationToken cancellationToken)
{
var job = await queue.DequeueAsync(cancellationToken).ConfigureAwait(false);
var request = job.Request;
RiskScoreResult Build(double score, bool success, string? error) =>
new(
job.JobId,
request.Provider,
request.Subject,
Score: score,
Success: success,
Error: error,
request.Signals,
CompletedAtUtc: timeProvider.GetUtcNow());
if (!registry.TryGet(request.Provider, out var provider))
{
var missing = Build(0d, false, "Provider not registered");
await PersistAsync(missing, cancellationToken).ConfigureAwait(false);
return missing;
}
try
{
var score = await provider.ScoreAsync(request, cancellationToken).ConfigureAwait(false);
var success = Build(score, true, null);
await PersistAsync(success, cancellationToken).ConfigureAwait(false);
return success;
}
catch (Exception ex)
{
var failure = Build(0d, false, ex.Message);
await PersistAsync(failure, cancellationToken).ConfigureAwait(false);
return failure;
}
}
public async Task<IReadOnlyList<RiskScoreResult>> ProcessBatchAsync(int expectedCount, CancellationToken cancellationToken)
{
if (expectedCount < 0)
{
throw new ArgumentOutOfRangeException(nameof(expectedCount));
}
var results = new List<RiskScoreResult>(expectedCount);
for (var i = 0; i < expectedCount; i++)
{
results.Add(await ProcessNextAsync(cancellationToken).ConfigureAwait(false));
}
return results;
}
private Task PersistAsync(RiskScoreResult result, CancellationToken cancellationToken) =>
resultStore is null
? Task.CompletedTask
: resultStore.SaveAsync(result, cancellationToken);
}