Refactor code structure for improved readability and maintainability
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-06 21:48:12 +02:00
parent f6c22854a4
commit dd0067ea0b
105 changed files with 12662 additions and 427 deletions

View File

@@ -0,0 +1,293 @@
using System.Collections.Immutable;
using System.Runtime.Serialization;
using StellaOps.Excititor.Core.Observations;
namespace StellaOps.Excititor.Core.RiskFeed;
/// <summary>
/// Risk-engine ready feed item containing VEX status, justification, and provenance
/// WITHOUT derived severity (aggregation-only contract per AOC baseline).
/// Aligns with docs/schemas/risk-scoring.schema.json.
/// </summary>
public sealed record RiskFeedItem
{
public RiskFeedItem(
string advisoryKey,
string artifact,
VexClaimStatus status,
VexJustification? justification,
RiskFeedProvenance provenance,
DateTimeOffset observedAt,
ImmutableArray<RiskFeedObservationSource> sources)
{
AdvisoryKey = EnsureNotNullOrWhiteSpace(advisoryKey, nameof(advisoryKey));
Artifact = EnsureNotNullOrWhiteSpace(artifact, nameof(artifact));
Status = status;
Justification = justification;
Provenance = provenance ?? throw new ArgumentNullException(nameof(provenance));
ObservedAt = observedAt.ToUniversalTime();
Sources = sources.IsDefault ? ImmutableArray<RiskFeedObservationSource>.Empty : sources;
}
/// <summary>
/// Advisory/CVE identifier (e.g., "CVE-2025-13579").
/// </summary>
public string AdvisoryKey { get; }
/// <summary>
/// Package URL or product key of affected artifact.
/// </summary>
public string Artifact { get; }
/// <summary>
/// VEX status (affected, not_affected, fixed, under_investigation).
/// No derived severity - status is passed through unchanged.
/// </summary>
public VexClaimStatus Status { get; }
/// <summary>
/// Justification for not_affected status.
/// </summary>
public VexJustification? Justification { get; }
/// <summary>
/// Provenance chain for auditability.
/// </summary>
public RiskFeedProvenance Provenance { get; }
/// <summary>
/// When this observation was made (UTC).
/// </summary>
public DateTimeOffset ObservedAt { get; }
/// <summary>
/// Source observations contributing to this feed item.
/// </summary>
public ImmutableArray<RiskFeedObservationSource> Sources { get; }
private static string EnsureNotNullOrWhiteSpace(string value, string name)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException($"{name} must be provided.", name);
}
return value.Trim();
}
}
/// <summary>
/// Provenance metadata for risk feed items - tracks origin and chain of custody.
/// </summary>
public sealed record RiskFeedProvenance
{
public RiskFeedProvenance(
string tenantId,
string linksetId,
string contentHash,
VexLinksetConfidence confidence,
bool hasConflicts,
DateTimeOffset generatedAt,
string? attestationId = null)
{
TenantId = EnsureNotNullOrWhiteSpace(tenantId, nameof(tenantId)).ToLowerInvariant();
LinksetId = EnsureNotNullOrWhiteSpace(linksetId, nameof(linksetId));
ContentHash = EnsureNotNullOrWhiteSpace(contentHash, nameof(contentHash));
Confidence = confidence;
HasConflicts = hasConflicts;
GeneratedAt = generatedAt.ToUniversalTime();
AttestationId = string.IsNullOrWhiteSpace(attestationId) ? null : attestationId.Trim();
}
public string TenantId { get; }
public string LinksetId { get; }
public string ContentHash { get; }
public VexLinksetConfidence Confidence { get; }
public bool HasConflicts { get; }
public DateTimeOffset GeneratedAt { get; }
public string? AttestationId { get; }
private static string EnsureNotNullOrWhiteSpace(string value, string name)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException($"{name} must be provided.", name);
}
return value.Trim();
}
}
/// <summary>
/// Source observation reference for risk feed provenance.
/// </summary>
public sealed record RiskFeedObservationSource
{
public RiskFeedObservationSource(
string observationId,
string providerId,
string status,
string? justification = null,
double? confidence = null)
{
ObservationId = EnsureNotNullOrWhiteSpace(observationId, nameof(observationId));
ProviderId = EnsureNotNullOrWhiteSpace(providerId, nameof(providerId));
Status = EnsureNotNullOrWhiteSpace(status, nameof(status));
Justification = string.IsNullOrWhiteSpace(justification) ? null : justification.Trim();
Confidence = confidence is null ? null : Math.Clamp(confidence.Value, 0.0, 1.0);
}
public string ObservationId { get; }
public string ProviderId { get; }
public string Status { get; }
public string? Justification { get; }
public double? Confidence { get; }
private static string EnsureNotNullOrWhiteSpace(string value, string name)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException($"{name} must be provided.", name);
}
return value.Trim();
}
}
/// <summary>
/// Request to generate risk feed for specified artifacts.
/// </summary>
public sealed record RiskFeedRequest
{
public RiskFeedRequest(
string tenantId,
IEnumerable<string>? advisoryKeys = null,
IEnumerable<string>? artifacts = null,
DateTimeOffset? since = null,
int limit = 1000)
{
TenantId = EnsureNotNullOrWhiteSpace(tenantId, nameof(tenantId)).ToLowerInvariant();
AdvisoryKeys = NormalizeSet(advisoryKeys);
Artifacts = NormalizeSet(artifacts);
Since = since?.ToUniversalTime();
Limit = Math.Clamp(limit, 1, 10000);
}
public string TenantId { get; }
public ImmutableArray<string> AdvisoryKeys { get; }
public ImmutableArray<string> Artifacts { get; }
public DateTimeOffset? Since { get; }
public int Limit { get; }
private static string EnsureNotNullOrWhiteSpace(string value, string name)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException($"{name} must be provided.", name);
}
return value.Trim();
}
private static ImmutableArray<string> NormalizeSet(IEnumerable<string>? values)
{
if (values is null)
{
return ImmutableArray<string>.Empty;
}
var set = new SortedSet<string>(StringComparer.Ordinal);
foreach (var value in values)
{
var trimmed = string.IsNullOrWhiteSpace(value) ? null : value.Trim();
if (trimmed is not null)
{
set.Add(trimmed);
}
}
return set.Count == 0 ? ImmutableArray<string>.Empty : set.ToImmutableArray();
}
}
/// <summary>
/// Response containing risk feed items.
/// </summary>
public sealed record RiskFeedResponse
{
public RiskFeedResponse(
IEnumerable<RiskFeedItem> items,
DateTimeOffset generatedAt,
string? nextPageToken = null)
{
Items = NormalizeItems(items);
GeneratedAt = generatedAt.ToUniversalTime();
NextPageToken = string.IsNullOrWhiteSpace(nextPageToken) ? null : nextPageToken.Trim();
}
public ImmutableArray<RiskFeedItem> Items { get; }
public DateTimeOffset GeneratedAt { get; }
public string? NextPageToken { get; }
private static ImmutableArray<RiskFeedItem> NormalizeItems(IEnumerable<RiskFeedItem>? items)
{
if (items is null)
{
return ImmutableArray<RiskFeedItem>.Empty;
}
var list = items.Where(i => i is not null).ToList();
return list.Count == 0 ? ImmutableArray<RiskFeedItem>.Empty : list.ToImmutableArray();
}
}
/// <summary>
/// Event published when risk feed is generated.
/// </summary>
public sealed record RiskFeedGeneratedEvent
{
public const string EventType = "excititor.risk_feed.generated";
public RiskFeedGeneratedEvent(
string tenantId,
string feedId,
int itemCount,
DateTimeOffset generatedAt,
string? correlationId = null)
{
Type = EventType;
TenantId = tenantId.ToLowerInvariant();
FeedId = feedId;
ItemCount = itemCount;
GeneratedAt = generatedAt.ToUniversalTime();
CorrelationId = string.IsNullOrWhiteSpace(correlationId) ? null : correlationId.Trim();
}
public string Type { get; }
public string TenantId { get; }
public string FeedId { get; }
public int ItemCount { get; }
public DateTimeOffset GeneratedAt { get; }
public string? CorrelationId { get; }
}