Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user