Add unit tests for PackRunAttestation and SealedInstallEnforcer
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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
Policy Lint & Smoke / policy-lint (push) Has been cancelled
release-manifest-verify / verify (push) Has been cancelled

- Implement comprehensive tests for PackRunAttestationService, covering attestation generation, verification, and event emission.
- Add tests for SealedInstallEnforcer to validate sealed install requirements and enforcement logic.
- Introduce a MonacoLoaderService stub for testing purposes to prevent Monaco workers/styles from loading during Karma runs.
This commit is contained in:
StellaOps Bot
2025-12-06 22:25:30 +02:00
parent dd0067ea0b
commit 4042fc2184
110 changed files with 20084 additions and 639 deletions

View File

@@ -0,0 +1,54 @@
namespace StellaOps.Excititor.Core.RiskFeed;
/// <summary>
/// Service for generating risk-engine ready feeds from VEX linksets.
/// Produces status/justification/provenance without derived severity (aggregation-only).
/// </summary>
public interface IRiskFeedService
{
/// <summary>
/// Generates risk feed items from linksets matching the request criteria.
/// </summary>
/// <param name="request">Filter criteria for the feed.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Risk feed response with items and pagination info.</returns>
Task<RiskFeedResponse> GenerateFeedAsync(RiskFeedRequest request, CancellationToken cancellationToken);
/// <summary>
/// Gets a single risk feed item for a specific advisory/artifact pair.
/// </summary>
/// <param name="tenantId">Tenant identifier.</param>
/// <param name="advisoryKey">Advisory/CVE identifier.</param>
/// <param name="artifact">Package URL or product key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Risk feed item if found.</returns>
Task<RiskFeedItem?> GetItemAsync(
string tenantId,
string advisoryKey,
string artifact,
CancellationToken cancellationToken);
}
/// <summary>
/// Null implementation of <see cref="IRiskFeedService"/> for testing and fallback.
/// </summary>
public sealed class NullRiskFeedService : IRiskFeedService
{
public static readonly NullRiskFeedService Instance = new();
public Task<RiskFeedResponse> GenerateFeedAsync(RiskFeedRequest request, CancellationToken cancellationToken)
{
return Task.FromResult(new RiskFeedResponse(
Enumerable.Empty<RiskFeedItem>(),
DateTimeOffset.UtcNow));
}
public Task<RiskFeedItem?> GetItemAsync(
string tenantId,
string advisoryKey,
string artifact,
CancellationToken cancellationToken)
{
return Task.FromResult<RiskFeedItem?>(null);
}
}

View File

@@ -0,0 +1,312 @@
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.Excititor.Core.Observations;
namespace StellaOps.Excititor.Core.RiskFeed;
/// <summary>
/// Generates risk-engine ready feeds from VEX linksets.
/// Produces status/justification/provenance without derived severity (aggregation-only per AOC baseline).
/// </summary>
public sealed class RiskFeedService : IRiskFeedService
{
private readonly IVexLinksetStore _linksetStore;
public RiskFeedService(IVexLinksetStore linksetStore)
{
_linksetStore = linksetStore ?? throw new ArgumentNullException(nameof(linksetStore));
}
public async Task<RiskFeedResponse> GenerateFeedAsync(
RiskFeedRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var feedItems = new List<RiskFeedItem>();
var generatedAt = DateTimeOffset.UtcNow;
// If specific advisory keys are requested, query by vulnerability
if (!request.AdvisoryKeys.IsDefaultOrEmpty)
{
foreach (var advisoryKey in request.AdvisoryKeys)
{
if (feedItems.Count >= request.Limit)
{
break;
}
var linksets = await _linksetStore.FindByVulnerabilityAsync(
request.TenantId,
advisoryKey,
request.Limit - feedItems.Count,
cancellationToken);
var items = linksets
.Where(ls => PassesSinceFilter(ls, request.Since))
.Select(ls => BuildFeedItem(ls, generatedAt))
.Where(item => item is not null)
.Cast<RiskFeedItem>();
feedItems.AddRange(items);
}
}
// If specific artifacts are requested, query by product key
else if (!request.Artifacts.IsDefaultOrEmpty)
{
foreach (var artifact in request.Artifacts)
{
if (feedItems.Count >= request.Limit)
{
break;
}
var linksets = await _linksetStore.FindByProductKeyAsync(
request.TenantId,
artifact,
request.Limit - feedItems.Count,
cancellationToken);
var items = linksets
.Where(ls => PassesSinceFilter(ls, request.Since))
.Select(ls => BuildFeedItem(ls, generatedAt))
.Where(item => item is not null)
.Cast<RiskFeedItem>();
feedItems.AddRange(items);
}
}
// Otherwise query linksets with conflicts (high-value for risk assessment)
else
{
var linksets = await _linksetStore.FindWithConflictsAsync(
request.TenantId,
request.Limit,
cancellationToken);
var items = linksets
.Where(ls => PassesSinceFilter(ls, request.Since))
.Select(ls => BuildFeedItem(ls, generatedAt))
.Where(item => item is not null)
.Cast<RiskFeedItem>();
feedItems.AddRange(items);
}
// Sort for deterministic output
var sortedItems = feedItems
.OrderBy(item => item.AdvisoryKey, StringComparer.OrdinalIgnoreCase)
.ThenBy(item => item.Artifact, StringComparer.Ordinal)
.Take(request.Limit)
.ToImmutableArray();
return new RiskFeedResponse(sortedItems, generatedAt);
}
public async Task<RiskFeedItem?> GetItemAsync(
string tenantId,
string advisoryKey,
string artifact,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(tenantId))
{
throw new ArgumentException("Tenant ID must be provided.", nameof(tenantId));
}
if (string.IsNullOrWhiteSpace(advisoryKey))
{
throw new ArgumentException("Advisory key must be provided.", nameof(advisoryKey));
}
if (string.IsNullOrWhiteSpace(artifact))
{
throw new ArgumentException("Artifact must be provided.", nameof(artifact));
}
var normalizedTenant = tenantId.Trim().ToLowerInvariant();
var linksetId = VexLinkset.CreateLinksetId(normalizedTenant, advisoryKey.Trim(), artifact.Trim());
var linkset = await _linksetStore.GetByIdAsync(
normalizedTenant,
linksetId,
cancellationToken);
if (linkset is null)
{
return null;
}
return BuildFeedItem(linkset, DateTimeOffset.UtcNow);
}
private static bool PassesSinceFilter(VexLinkset linkset, DateTimeOffset? since)
{
if (!since.HasValue)
{
return true;
}
return linkset.UpdatedAt >= since.Value;
}
private RiskFeedItem? BuildFeedItem(VexLinkset linkset, DateTimeOffset generatedAt)
{
if (linkset.Observations.IsDefaultOrEmpty)
{
return null;
}
// Get the dominant status from observations (most common status)
var statusGroups = linkset.Observations
.GroupBy(obs => obs.Status, StringComparer.OrdinalIgnoreCase)
.OrderByDescending(g => g.Count())
.ThenBy(g => g.Key, StringComparer.OrdinalIgnoreCase)
.ToList();
if (statusGroups.Count == 0)
{
return null;
}
var dominantStatusStr = statusGroups[0].Key;
if (!TryParseStatus(dominantStatusStr, out var status))
{
// Unknown status - skip this linkset
return null;
}
// Try to get justification from disagreements or observation references
VexJustification? justification = null;
foreach (var disagreement in linkset.Disagreements)
{
if (TryParseJustification(disagreement.Justification, out var parsed))
{
justification = parsed;
break;
}
}
// Build provenance
var contentHash = ComputeContentHash(linkset);
var provenance = new RiskFeedProvenance(
tenantId: linkset.Tenant,
linksetId: linkset.LinksetId,
contentHash: contentHash,
confidence: linkset.Confidence,
hasConflicts: linkset.HasConflicts,
generatedAt: generatedAt);
// Build source references
var sources = linkset.Observations
.Select(obs => new RiskFeedObservationSource(
observationId: obs.ObservationId,
providerId: obs.ProviderId,
status: obs.Status,
justification: null,
confidence: obs.Confidence))
.ToImmutableArray();
return new RiskFeedItem(
advisoryKey: linkset.VulnerabilityId,
artifact: linkset.ProductKey,
status: status,
justification: justification,
provenance: provenance,
observedAt: linkset.UpdatedAt,
sources: sources);
}
private static string ComputeContentHash(VexLinkset linkset)
{
var canonical = new
{
linkset.LinksetId,
linkset.Tenant,
linkset.VulnerabilityId,
linkset.ProductKey,
Observations = linkset.Observations
.Select(o => new { o.ObservationId, o.ProviderId, o.Status, o.Confidence })
.OrderBy(o => o.ProviderId)
.ThenBy(o => o.ObservationId)
.ToArray(),
linkset.UpdatedAt
};
var json = JsonSerializer.Serialize(canonical, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
});
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
private static bool TryParseStatus(string? statusStr, out VexClaimStatus status)
{
status = VexClaimStatus.Affected;
if (string.IsNullOrWhiteSpace(statusStr))
{
return false;
}
var normalized = statusStr.Trim().ToLowerInvariant().Replace("_", "");
return normalized switch
{
"affected" => AssignStatus(VexClaimStatus.Affected, out status),
"notaffected" => AssignStatus(VexClaimStatus.NotAffected, out status),
"fixed" => AssignStatus(VexClaimStatus.Fixed, out status),
"underinvestigation" => AssignStatus(VexClaimStatus.UnderInvestigation, out status),
_ => false
};
}
private static bool AssignStatus(VexClaimStatus value, out VexClaimStatus status)
{
status = value;
return true;
}
private static bool TryParseJustification(string? justificationStr, out VexJustification justification)
{
justification = VexJustification.ComponentNotPresent;
if (string.IsNullOrWhiteSpace(justificationStr))
{
return false;
}
var normalized = justificationStr.Trim().ToLowerInvariant().Replace("_", "");
return normalized switch
{
"componentnotpresent" => AssignJustification(VexJustification.ComponentNotPresent, out justification),
"componentnotconfigured" => AssignJustification(VexJustification.ComponentNotConfigured, out justification),
"vulnerablecodenotpresent" => AssignJustification(VexJustification.VulnerableCodeNotPresent, out justification),
"vulnerablecodenotinexecutepath" => AssignJustification(VexJustification.VulnerableCodeNotInExecutePath, out justification),
"vulnerablecodecannotbecontrolledbyadversary" => AssignJustification(VexJustification.VulnerableCodeCannotBeControlledByAdversary, out justification),
"inlinemitigationsalreadyexist" => AssignJustification(VexJustification.InlineMitigationsAlreadyExist, out justification),
"protectedbymitigatingcontrol" => AssignJustification(VexJustification.ProtectedByMitigatingControl, out justification),
"codenotpresent" => AssignJustification(VexJustification.CodeNotPresent, out justification),
"codenotreachable" => AssignJustification(VexJustification.CodeNotReachable, out justification),
"requiresconfiguration" => AssignJustification(VexJustification.RequiresConfiguration, out justification),
"requiresdependency" => AssignJustification(VexJustification.RequiresDependency, out justification),
"requiresenvironment" => AssignJustification(VexJustification.RequiresEnvironment, out justification),
"protectedbycompensatingcontrol" => AssignJustification(VexJustification.ProtectedByCompensatingControl, out justification),
"protectedatperimeter" => AssignJustification(VexJustification.ProtectedAtPerimeter, out justification),
"protectedatruntime" => AssignJustification(VexJustification.ProtectedAtRuntime, out justification),
_ => false
};
}
private static bool AssignJustification(VexJustification value, out VexJustification justification)
{
justification = value;
return true;
}
}