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,303 @@
using System.Collections.Immutable;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.RiskFeed;
using StellaOps.Excititor.Storage.Mongo;
using StellaOps.Excititor.WebService.Services;
namespace StellaOps.Excititor.WebService.Endpoints;
/// <summary>
/// Risk feed API endpoints (EXCITITOR-RISK-66-001).
/// Publishes risk-engine ready feeds with status, justification, and provenance
/// without derived severity (aggregation-only per AOC baseline).
/// </summary>
public static class RiskFeedEndpoints
{
public static void MapRiskFeedEndpoints(this WebApplication app)
{
var group = app.MapGroup("/risk/v1");
// POST /risk/v1/feed - Generate risk feed
group.MapPost("/feed", async (
HttpContext context,
IOptions<VexMongoStorageOptions> storageOptions,
[FromServices] IRiskFeedService riskFeedService,
[FromBody] RiskFeedRequestDto request,
CancellationToken cancellationToken) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
if (scopeResult is not null)
{
return scopeResult;
}
if (!TryResolveTenant(context, storageOptions.Value, out var tenant, out var tenantError))
{
return tenantError;
}
if (request is null)
{
return Results.BadRequest(new
{
error = new { code = "ERR_RISK_PARAMS", message = "Request body is required" }
});
}
var domainRequest = new RiskFeedRequest(
tenantId: tenant,
advisoryKeys: request.AdvisoryKeys,
artifacts: request.Artifacts,
since: request.Since,
limit: request.Limit ?? 1000);
var feedResponse = await riskFeedService
.GenerateFeedAsync(domainRequest, cancellationToken)
.ConfigureAwait(false);
var responseDto = MapToResponse(feedResponse);
return Results.Ok(responseDto);
}).WithName("GenerateRiskFeed");
// GET /risk/v1/feed/item - Get single risk feed item
group.MapGet("/feed/item", async (
HttpContext context,
IOptions<VexMongoStorageOptions> storageOptions,
[FromServices] IRiskFeedService riskFeedService,
[FromQuery] string? advisoryKey,
[FromQuery] string? artifact,
CancellationToken cancellationToken) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
if (scopeResult is not null)
{
return scopeResult;
}
if (!TryResolveTenant(context, storageOptions.Value, out var tenant, out var tenantError))
{
return tenantError;
}
if (string.IsNullOrWhiteSpace(advisoryKey) || string.IsNullOrWhiteSpace(artifact))
{
return Results.BadRequest(new
{
error = new { code = "ERR_RISK_PARAMS", message = "advisoryKey and artifact query parameters are required" }
});
}
var item = await riskFeedService
.GetItemAsync(tenant, advisoryKey, artifact, cancellationToken)
.ConfigureAwait(false);
if (item is null)
{
return Results.NotFound(new
{
error = new { code = "ERR_RISK_NOT_FOUND", message = "No risk feed item found for the specified advisory and artifact" }
});
}
var dto = MapToItemDto(item);
return Results.Ok(dto);
}).WithName("GetRiskFeedItem");
// GET /risk/v1/feed/by-advisory - Get risk feed items by advisory key
group.MapGet("/feed/by-advisory/{advisoryKey}", async (
HttpContext context,
string advisoryKey,
IOptions<VexMongoStorageOptions> storageOptions,
[FromServices] IRiskFeedService riskFeedService,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
if (scopeResult is not null)
{
return scopeResult;
}
if (!TryResolveTenant(context, storageOptions.Value, out var tenant, out var tenantError))
{
return tenantError;
}
if (string.IsNullOrWhiteSpace(advisoryKey))
{
return Results.BadRequest(new
{
error = new { code = "ERR_RISK_PARAMS", message = "advisoryKey is required" }
});
}
var request = new RiskFeedRequest(
tenantId: tenant,
advisoryKeys: [advisoryKey],
limit: limit ?? 100);
var feedResponse = await riskFeedService
.GenerateFeedAsync(request, cancellationToken)
.ConfigureAwait(false);
var responseDto = MapToResponse(feedResponse);
return Results.Ok(responseDto);
}).WithName("GetRiskFeedByAdvisory");
// GET /risk/v1/feed/by-artifact/{artifact} - Get risk feed items by artifact
group.MapGet("/feed/by-artifact/{**artifact}", async (
HttpContext context,
string artifact,
IOptions<VexMongoStorageOptions> storageOptions,
[FromServices] IRiskFeedService riskFeedService,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
if (scopeResult is not null)
{
return scopeResult;
}
if (!TryResolveTenant(context, storageOptions.Value, out var tenant, out var tenantError))
{
return tenantError;
}
if (string.IsNullOrWhiteSpace(artifact))
{
return Results.BadRequest(new
{
error = new { code = "ERR_RISK_PARAMS", message = "artifact is required" }
});
}
var request = new RiskFeedRequest(
tenantId: tenant,
artifacts: [artifact],
limit: limit ?? 100);
var feedResponse = await riskFeedService
.GenerateFeedAsync(request, cancellationToken)
.ConfigureAwait(false);
var responseDto = MapToResponse(feedResponse);
return Results.Ok(responseDto);
}).WithName("GetRiskFeedByArtifact");
}
private static RiskFeedResponseDto MapToResponse(RiskFeedResponse response)
{
var items = response.Items
.Select(MapToItemDto)
.ToList();
return new RiskFeedResponseDto(
Items: items,
GeneratedAt: response.GeneratedAt,
NextPageToken: response.NextPageToken);
}
private static RiskFeedItemDto MapToItemDto(RiskFeedItem item)
{
var provenance = new RiskFeedProvenanceDto(
TenantId: item.Provenance.TenantId,
LinksetId: item.Provenance.LinksetId,
ContentHash: item.Provenance.ContentHash,
Confidence: item.Provenance.Confidence.ToString().ToLowerInvariant(),
HasConflicts: item.Provenance.HasConflicts,
GeneratedAt: item.Provenance.GeneratedAt,
AttestationId: item.Provenance.AttestationId);
var sources = item.Sources
.Select(s => new RiskFeedSourceDto(
ObservationId: s.ObservationId,
ProviderId: s.ProviderId,
Status: s.Status,
Justification: s.Justification,
Confidence: s.Confidence))
.ToList();
return new RiskFeedItemDto(
AdvisoryKey: item.AdvisoryKey,
Artifact: item.Artifact,
Status: item.Status.ToString().ToLowerInvariant(),
Justification: item.Justification?.ToString().ToLowerInvariant(),
Provenance: provenance,
ObservedAt: item.ObservedAt,
Sources: sources);
}
private static bool TryResolveTenant(
HttpContext context,
VexMongoStorageOptions options,
out string tenant,
out IResult? problem)
{
problem = null;
tenant = string.Empty;
var headerTenant = context.Request.Headers["X-Stella-Tenant"].FirstOrDefault();
if (!string.IsNullOrWhiteSpace(headerTenant))
{
tenant = headerTenant.Trim().ToLowerInvariant();
}
else if (!string.IsNullOrWhiteSpace(options.DefaultTenant))
{
tenant = options.DefaultTenant.Trim().ToLowerInvariant();
}
else
{
problem = Results.BadRequest(new
{
error = new { code = "ERR_TENANT", message = "X-Stella-Tenant header is required" }
});
return false;
}
return true;
}
}
// Request DTO
public sealed record RiskFeedRequestDto(
[property: JsonPropertyName("advisoryKeys")] IEnumerable<string>? AdvisoryKeys,
[property: JsonPropertyName("artifacts")] IEnumerable<string>? Artifacts,
[property: JsonPropertyName("since")] DateTimeOffset? Since,
[property: JsonPropertyName("limit")] int? Limit);
// Response DTOs
public sealed record RiskFeedResponseDto(
[property: JsonPropertyName("items")] IReadOnlyList<RiskFeedItemDto> Items,
[property: JsonPropertyName("generatedAt")] DateTimeOffset GeneratedAt,
[property: JsonPropertyName("nextPageToken")] string? NextPageToken);
public sealed record RiskFeedItemDto(
[property: JsonPropertyName("advisoryKey")] string AdvisoryKey,
[property: JsonPropertyName("artifact")] string Artifact,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("justification")] string? Justification,
[property: JsonPropertyName("provenance")] RiskFeedProvenanceDto Provenance,
[property: JsonPropertyName("observedAt")] DateTimeOffset ObservedAt,
[property: JsonPropertyName("sources")] IReadOnlyList<RiskFeedSourceDto> Sources);
public sealed record RiskFeedProvenanceDto(
[property: JsonPropertyName("tenantId")] string TenantId,
[property: JsonPropertyName("linksetId")] string LinksetId,
[property: JsonPropertyName("contentHash")] string ContentHash,
[property: JsonPropertyName("confidence")] string Confidence,
[property: JsonPropertyName("hasConflicts")] bool HasConflicts,
[property: JsonPropertyName("generatedAt")] DateTimeOffset GeneratedAt,
[property: JsonPropertyName("attestationId")] string? AttestationId);
public sealed record RiskFeedSourceDto(
[property: JsonPropertyName("observationId")] string ObservationId,
[property: JsonPropertyName("providerId")] string ProviderId,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("justification")] string? Justification,
[property: JsonPropertyName("confidence")] double? Confidence);

View File

@@ -90,6 +90,9 @@ services.AddSingleton<IVexHashingService>(sp =>
services.AddSingleton<IVexObservationProjectionService, VexObservationProjectionService>();
services.AddScoped<IVexObservationQueryService, VexObservationQueryService>();
// EXCITITOR-RISK-66-001: Risk feed service for Risk Engine integration
services.AddScoped<StellaOps.Excititor.Core.RiskFeed.IRiskFeedService, StellaOps.Excititor.Core.RiskFeed.RiskFeedService>();
var rekorSection = configuration.GetSection("Excititor:Attestation:Rekor");
if (rekorSection.Exists())
{
@@ -2323,6 +2326,9 @@ PolicyEndpoints.MapPolicyEndpoints(app);
ObservationEndpoints.MapObservationEndpoints(app);
LinksetEndpoints.MapLinksetEndpoints(app);
// Risk Feed APIs (EXCITITOR-RISK-66-001)
RiskFeedEndpoints.MapRiskFeedEndpoints(app);
app.Run();
internal sealed record ExcititorTimelineEvent(