audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories
This commit is contained in:
27
src/Unknowns/StellaOps.Unknowns.WebService/AGENTS.md
Normal file
27
src/Unknowns/StellaOps.Unknowns.WebService/AGENTS.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# AGENTS - Unknowns WebService
|
||||
|
||||
## Roles
|
||||
- Backend engineer: .NET 10 Web API, DI wiring, and repository integration.
|
||||
- QA / test engineer: endpoint and authorization coverage, determinism checks.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/unknowns/architecture.md
|
||||
- src/Unknowns/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/Unknowns/StellaOps.Unknowns.WebService
|
||||
- Test scope: src/Unknowns/__Tests/StellaOps.Unknowns.WebService.Tests
|
||||
- Avoid cross-module edits unless explicitly allowed in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Use TimeProvider/IGuidGenerator for time and IDs.
|
||||
- Validate inputs (tenant id, pagination bounds, confidence thresholds).
|
||||
- Require authorization and derive tenant identity from claims, not headers.
|
||||
|
||||
## Testing
|
||||
- Cover list/query endpoints, pagination bounds, and tenant isolation.
|
||||
- Add health check tests for database connectivity and error handling.
|
||||
@@ -0,0 +1,425 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// UnknownsEndpoints.cs
|
||||
// Sprint: SPRINT_20260106_001_005_UNKNOWNS_provenance_hints
|
||||
// Tasks: WS-004, WS-005, WS-006 - Implement API endpoints
|
||||
// Description: Minimal API endpoints for Unknowns with provenance hints
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Unknowns.Core.Models;
|
||||
using StellaOps.Unknowns.Core.Repositories;
|
||||
|
||||
namespace StellaOps.Unknowns.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal API endpoints for Unknowns service.
|
||||
/// </summary>
|
||||
public static class UnknownsEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps all Unknowns endpoints.
|
||||
/// </summary>
|
||||
public static IEndpointRouteBuilder MapUnknownsEndpoints(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/api/unknowns")
|
||||
.WithTags("Unknowns")
|
||||
.WithOpenApi();
|
||||
|
||||
// WS-004: GET /api/unknowns - List with pagination
|
||||
group.MapGet("/", ListUnknowns)
|
||||
.WithName("ListUnknowns")
|
||||
.WithSummary("List unknowns with pagination")
|
||||
.WithDescription("Returns paginated list of open unknowns. Supports bitemporal query with asOf parameter.");
|
||||
|
||||
// WS-005: GET /api/unknowns/{id} - Single with hints
|
||||
group.MapGet("/{id:guid}", GetUnknownById)
|
||||
.WithName("GetUnknownById")
|
||||
.WithSummary("Get unknown by ID")
|
||||
.WithDescription("Returns a single unknown with full provenance hints.");
|
||||
|
||||
// WS-006: GET /api/unknowns/{id}/hints - Hints only
|
||||
group.MapGet("/{id:guid}/hints", GetUnknownHints)
|
||||
.WithName("GetUnknownHints")
|
||||
.WithSummary("Get provenance hints for unknown")
|
||||
.WithDescription("Returns only the provenance hints for an unknown.");
|
||||
|
||||
// Additional endpoints
|
||||
group.MapGet("/{id:guid}/history", GetUnknownHistory)
|
||||
.WithName("GetUnknownHistory")
|
||||
.WithSummary("Get bitemporal history for unknown")
|
||||
.WithDescription("Returns the bitemporal history of state changes for an unknown.");
|
||||
|
||||
group.MapGet("/triage/{band}", GetByTriageBand)
|
||||
.WithName("GetUnknownsByTriageBand")
|
||||
.WithSummary("Get unknowns by triage band")
|
||||
.WithDescription("Returns unknowns filtered by triage band (hot, warm, cold).");
|
||||
|
||||
group.MapGet("/hot-queue", GetHotQueue)
|
||||
.WithName("GetHotQueue")
|
||||
.WithSummary("Get HOT unknowns for immediate processing")
|
||||
.WithDescription("Returns HOT unknowns ordered by composite score descending.");
|
||||
|
||||
group.MapGet("/high-confidence", GetHighConfidenceHints)
|
||||
.WithName("GetHighConfidenceHints")
|
||||
.WithSummary("Get unknowns with high-confidence hints")
|
||||
.WithDescription("Returns unknowns with provenance hints above confidence threshold.");
|
||||
|
||||
group.MapGet("/summary", GetSummary)
|
||||
.WithName("GetUnknownsSummary")
|
||||
.WithSummary("Get unknowns summary statistics")
|
||||
.WithDescription("Returns summary counts by kind, severity, and triage band.");
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
// WS-004: List unknowns with pagination
|
||||
private static async Task<Ok<UnknownsListResponse>> ListUnknowns(
|
||||
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
|
||||
[FromQuery] int skip = 0,
|
||||
[FromQuery] int take = 50,
|
||||
[FromQuery] DateTimeOffset? asOf = null,
|
||||
[FromQuery] UnknownKind? kind = null,
|
||||
[FromQuery] UnknownSeverity? severity = null,
|
||||
IUnknownRepository repository = null!,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
IReadOnlyList<Unknown> unknowns;
|
||||
long total;
|
||||
|
||||
if (asOf.HasValue)
|
||||
{
|
||||
// Bitemporal query
|
||||
unknowns = await repository.AsOfAsync(tenantId, asOf.Value, null, ct);
|
||||
total = unknowns.Count;
|
||||
unknowns = unknowns.Skip(skip).Take(take).ToList();
|
||||
}
|
||||
else if (kind.HasValue)
|
||||
{
|
||||
unknowns = await repository.GetByKindAsync(tenantId, kind.Value, take, ct);
|
||||
total = unknowns.Count;
|
||||
}
|
||||
else if (severity.HasValue)
|
||||
{
|
||||
unknowns = await repository.GetBySeverityAsync(tenantId, severity.Value, take, ct);
|
||||
total = unknowns.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
unknowns = await repository.GetOpenUnknownsAsync(tenantId, take, skip, ct);
|
||||
total = await repository.CountOpenAsync(tenantId, ct);
|
||||
}
|
||||
|
||||
var response = new UnknownsListResponse
|
||||
{
|
||||
Items = unknowns.Select(u => MapToDto(u)).ToList(),
|
||||
Total = total,
|
||||
Skip = skip,
|
||||
Take = take
|
||||
};
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
// WS-005: Get single unknown with hints
|
||||
private static async Task<Results<Ok<UnknownDto>, NotFound>> GetUnknownById(
|
||||
Guid id,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
|
||||
IUnknownRepository repository = null!,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var unknown = await repository.GetByIdAsync(tenantId, id, ct);
|
||||
|
||||
if (unknown is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
return TypedResults.Ok(MapToDto(unknown));
|
||||
}
|
||||
|
||||
// WS-006: Get hints only
|
||||
private static async Task<Results<Ok<ProvenanceHintsResponse>, NotFound>> GetUnknownHints(
|
||||
Guid id,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
|
||||
IUnknownRepository repository = null!,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var unknown = await repository.GetByIdAsync(tenantId, id, ct);
|
||||
|
||||
if (unknown is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var response = new ProvenanceHintsResponse
|
||||
{
|
||||
UnknownId = unknown.Id,
|
||||
Hints = unknown.ProvenanceHints.Select(h => MapHintToDto(h)).ToList(),
|
||||
BestHypothesis = unknown.BestHypothesis,
|
||||
CombinedConfidence = unknown.CombinedConfidence,
|
||||
PrimarySuggestedAction = unknown.PrimarySuggestedAction
|
||||
};
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
// Get bitemporal history
|
||||
private static async Task<Results<Ok<UnknownHistoryResponse>, NotFound>> GetUnknownHistory(
|
||||
Guid id,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
|
||||
[FromQuery] DateTimeOffset? from = null,
|
||||
[FromQuery] DateTimeOffset? to = null,
|
||||
IUnknownRepository repository = null!,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var unknown = await repository.GetByIdAsync(tenantId, id, ct);
|
||||
|
||||
if (unknown is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
// Note: Full history would require additional repository method
|
||||
// For now, return current state as single history entry
|
||||
var response = new UnknownHistoryResponse
|
||||
{
|
||||
UnknownId = id,
|
||||
History = [
|
||||
new UnknownHistoryEntry
|
||||
{
|
||||
ValidFrom = unknown.ValidFrom,
|
||||
ValidTo = unknown.ValidTo,
|
||||
SysFrom = unknown.SysFrom,
|
||||
SysTo = unknown.SysTo,
|
||||
State = MapToDto(unknown)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
// Get by triage band
|
||||
private static async Task<Ok<UnknownsListResponse>> GetByTriageBand(
|
||||
TriageBand band,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
|
||||
[FromQuery] int limit = 50,
|
||||
[FromQuery] int offset = 0,
|
||||
IUnknownRepository repository = null!,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var unknowns = await repository.GetByTriageBandAsync(tenantId, band, limit, offset, ct);
|
||||
|
||||
var response = new UnknownsListResponse
|
||||
{
|
||||
Items = unknowns.Select(u => MapToDto(u)).ToList(),
|
||||
Total = unknowns.Count,
|
||||
Skip = offset,
|
||||
Take = limit
|
||||
};
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
// Get HOT queue
|
||||
private static async Task<Ok<UnknownsListResponse>> GetHotQueue(
|
||||
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
|
||||
[FromQuery] int limit = 50,
|
||||
IUnknownRepository repository = null!,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var unknowns = await repository.GetHotQueueAsync(tenantId, limit, ct);
|
||||
|
||||
var response = new UnknownsListResponse
|
||||
{
|
||||
Items = unknowns.Select(u => MapToDto(u)).ToList(),
|
||||
Total = unknowns.Count,
|
||||
Skip = 0,
|
||||
Take = limit
|
||||
};
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
// Get high-confidence hints
|
||||
private static async Task<Ok<UnknownsListResponse>> GetHighConfidenceHints(
|
||||
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
|
||||
[FromQuery] double minConfidence = 0.7,
|
||||
[FromQuery] int limit = 50,
|
||||
IUnknownRepository repository = null!,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var unknowns = await repository.GetWithHighConfidenceHintsAsync(
|
||||
tenantId, minConfidence, limit, ct);
|
||||
|
||||
var response = new UnknownsListResponse
|
||||
{
|
||||
Items = unknowns.Select(u => MapToDto(u)).ToList(),
|
||||
Total = unknowns.Count,
|
||||
Skip = 0,
|
||||
Take = limit
|
||||
};
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
// Get summary statistics
|
||||
private static async Task<Ok<UnknownsSummaryResponse>> GetSummary(
|
||||
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
|
||||
IUnknownRepository repository = null!,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var byKind = await repository.CountByKindAsync(tenantId, ct);
|
||||
var bySeverity = await repository.CountBySeverityAsync(tenantId, ct);
|
||||
var byBand = await repository.CountByTriageBandAsync(tenantId, ct);
|
||||
var totalOpen = await repository.CountOpenAsync(tenantId, ct);
|
||||
|
||||
var response = new UnknownsSummaryResponse
|
||||
{
|
||||
TotalOpen = totalOpen,
|
||||
ByKind = byKind.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value),
|
||||
BySeverity = bySeverity.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value),
|
||||
ByTriageBand = byBand.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value)
|
||||
};
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
// Mapping helpers
|
||||
private static UnknownDto MapToDto(Unknown u) => new()
|
||||
{
|
||||
Id = u.Id,
|
||||
TenantId = u.TenantId,
|
||||
SubjectHash = u.SubjectHash,
|
||||
SubjectType = u.SubjectType.ToString(),
|
||||
SubjectRef = u.SubjectRef,
|
||||
Kind = u.Kind.ToString(),
|
||||
Severity = u.Severity?.ToString(),
|
||||
SourceScanId = u.SourceScanId,
|
||||
SourceGraphId = u.SourceGraphId,
|
||||
SourceSbomDigest = u.SourceSbomDigest,
|
||||
ValidFrom = u.ValidFrom,
|
||||
ValidTo = u.ValidTo,
|
||||
ResolvedAt = u.ResolvedAt,
|
||||
ResolutionType = u.ResolutionType?.ToString(),
|
||||
ResolutionRef = u.ResolutionRef,
|
||||
CompositeScore = u.CompositeScore,
|
||||
TriageBand = u.TriageBand.ToString(),
|
||||
IsOpen = u.IsOpen,
|
||||
IsResolved = u.IsResolved,
|
||||
ProvenanceHints = u.ProvenanceHints.Select(h => MapHintToDto(h)).ToList(),
|
||||
BestHypothesis = u.BestHypothesis,
|
||||
CombinedConfidence = u.CombinedConfidence,
|
||||
PrimarySuggestedAction = u.PrimarySuggestedAction,
|
||||
CreatedAt = u.CreatedAt,
|
||||
UpdatedAt = u.UpdatedAt
|
||||
};
|
||||
|
||||
private static ProvenanceHintDto MapHintToDto(ProvenanceHint h) => new()
|
||||
{
|
||||
Id = h.Id,
|
||||
Type = h.Type.ToString(),
|
||||
Confidence = h.Confidence,
|
||||
ConfidenceLevel = h.ConfidenceLevel.ToString(),
|
||||
Hypothesis = h.Hypothesis,
|
||||
SuggestedActions = h.SuggestedActions.Select(a => new SuggestedActionDto
|
||||
{
|
||||
Action = a.Action,
|
||||
Priority = a.Priority,
|
||||
Description = a.Description,
|
||||
Url = a.Url
|
||||
}).ToList(),
|
||||
GeneratedAt = h.GeneratedAt
|
||||
};
|
||||
}
|
||||
|
||||
// DTOs
|
||||
|
||||
public sealed record UnknownsListResponse
|
||||
{
|
||||
public required IReadOnlyList<UnknownDto> Items { get; init; }
|
||||
public required long Total { get; init; }
|
||||
public required int Skip { get; init; }
|
||||
public required int Take { get; init; }
|
||||
}
|
||||
|
||||
public sealed record UnknownDto
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required string SubjectHash { get; init; }
|
||||
public required string SubjectType { get; init; }
|
||||
public required string SubjectRef { get; init; }
|
||||
public required string Kind { get; init; }
|
||||
public string? Severity { get; init; }
|
||||
public Guid? SourceScanId { get; init; }
|
||||
public Guid? SourceGraphId { get; init; }
|
||||
public string? SourceSbomDigest { get; init; }
|
||||
public required DateTimeOffset ValidFrom { get; init; }
|
||||
public DateTimeOffset? ValidTo { get; init; }
|
||||
public DateTimeOffset? ResolvedAt { get; init; }
|
||||
public string? ResolutionType { get; init; }
|
||||
public string? ResolutionRef { get; init; }
|
||||
public required double CompositeScore { get; init; }
|
||||
public required string TriageBand { get; init; }
|
||||
public required bool IsOpen { get; init; }
|
||||
public required bool IsResolved { get; init; }
|
||||
public required IReadOnlyList<ProvenanceHintDto> ProvenanceHints { get; init; }
|
||||
public string? BestHypothesis { get; init; }
|
||||
public double? CombinedConfidence { get; init; }
|
||||
public string? PrimarySuggestedAction { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public required DateTimeOffset UpdatedAt { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ProvenanceHintsResponse
|
||||
{
|
||||
public required Guid UnknownId { get; init; }
|
||||
public required IReadOnlyList<ProvenanceHintDto> Hints { get; init; }
|
||||
public string? BestHypothesis { get; init; }
|
||||
public double? CombinedConfidence { get; init; }
|
||||
public string? PrimarySuggestedAction { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ProvenanceHintDto
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public required double Confidence { get; init; }
|
||||
public required string ConfidenceLevel { get; init; }
|
||||
public required string Hypothesis { get; init; }
|
||||
public required IReadOnlyList<SuggestedActionDto> SuggestedActions { get; init; }
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SuggestedActionDto
|
||||
{
|
||||
public required string Action { get; init; }
|
||||
public required int Priority { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string? Url { get; init; }
|
||||
}
|
||||
|
||||
public sealed record UnknownHistoryResponse
|
||||
{
|
||||
public required Guid UnknownId { get; init; }
|
||||
public required IReadOnlyList<UnknownHistoryEntry> History { get; init; }
|
||||
}
|
||||
|
||||
public sealed record UnknownHistoryEntry
|
||||
{
|
||||
public required DateTimeOffset ValidFrom { get; init; }
|
||||
public DateTimeOffset? ValidTo { get; init; }
|
||||
public required DateTimeOffset SysFrom { get; init; }
|
||||
public DateTimeOffset? SysTo { get; init; }
|
||||
public required UnknownDto State { get; init; }
|
||||
}
|
||||
|
||||
public sealed record UnknownsSummaryResponse
|
||||
{
|
||||
public required long TotalOpen { get; init; }
|
||||
public required IReadOnlyDictionary<string, long> ByKind { get; init; }
|
||||
public required IReadOnlyDictionary<string, long> BySeverity { get; init; }
|
||||
public required IReadOnlyDictionary<string, long> ByTriageBand { get; init; }
|
||||
}
|
||||
56
src/Unknowns/StellaOps.Unknowns.WebService/Program.cs
Normal file
56
src/Unknowns/StellaOps.Unknowns.WebService/Program.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// Program.cs
|
||||
// Sprint: SPRINT_20260106_001_005_UNKNOWNS_provenance_hints
|
||||
// Task: WS-002 - Add Program.cs with minimal hosting
|
||||
// Description: Entry point for Unknowns WebService with OpenAPI, health checks, auth
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.OpenApi.Models;
|
||||
using StellaOps.Unknowns.WebService;
|
||||
using StellaOps.Unknowns.WebService.Endpoints;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services
|
||||
builder.Services.AddUnknownsServices(builder.Configuration);
|
||||
|
||||
// OpenAPI / Swagger
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Title = "StellaOps Unknowns API",
|
||||
Version = "v1",
|
||||
Description = "API for managing unknown components with provenance hints"
|
||||
});
|
||||
});
|
||||
|
||||
// Health checks
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddCheck<DatabaseHealthCheck>("database");
|
||||
|
||||
// Authentication (placeholder - configure based on environment)
|
||||
builder.Services.AddAuthentication();
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure pipeline
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// Map endpoints
|
||||
app.MapUnknownsEndpoints();
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
app.Run();
|
||||
|
||||
// Make Program class accessible for integration tests
|
||||
public partial class Program { }
|
||||
@@ -0,0 +1,68 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ServiceCollectionExtensions.cs
|
||||
// Sprint: SPRINT_20260106_001_005_UNKNOWNS_provenance_hints
|
||||
// Task: WS-003 - Register IUnknownRepository from PostgreSQL library
|
||||
// Description: DI registration for Unknowns services
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using StellaOps.Unknowns.Core.Repositories;
|
||||
using StellaOps.Unknowns.Persistence;
|
||||
|
||||
namespace StellaOps.Unknowns.WebService;
|
||||
|
||||
/// <summary>
|
||||
/// Service collection extensions for Unknowns WebService.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Unknowns services to the service collection.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddUnknownsServices(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// Register repository
|
||||
var connectionString = configuration.GetConnectionString("UnknownsDb")
|
||||
?? throw new InvalidOperationException("UnknownsDb connection string is required");
|
||||
|
||||
services.AddSingleton<IUnknownRepository>(sp =>
|
||||
new PostgresUnknownRepository(connectionString, sp.GetRequiredService<TimeProvider>()));
|
||||
|
||||
// Register TimeProvider
|
||||
services.AddSingleton(TimeProvider.System);
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Health check for database connectivity.
|
||||
/// </summary>
|
||||
public sealed class DatabaseHealthCheck : IHealthCheck
|
||||
{
|
||||
private readonly IUnknownRepository _repository;
|
||||
|
||||
public DatabaseHealthCheck(IUnknownRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||
HealthCheckContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Simple check - try to list with limit 1
|
||||
await _repository.ListAsync(skip: 0, take: 1, asOf: null, cancellationToken);
|
||||
return HealthCheckResult.Healthy("Database connection successful");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy("Database connection failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RootNamespace>StellaOps.Unknowns.WebService</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\__Libraries\StellaOps.Unknowns.Core\StellaOps.Unknowns.Core.csproj" />
|
||||
<ProjectReference Include="..\__Libraries\StellaOps.Unknowns.Persistence\StellaOps.Unknowns.Persistence.csproj" />
|
||||
<ProjectReference Include="..\__Libraries\StellaOps.Unknowns.Persistence.EfCore\StellaOps.Unknowns.Persistence.EfCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,3 +1,5 @@
|
||||
using StellaOps.Unknowns.Core.Models;
|
||||
|
||||
namespace StellaOps.Unknowns.Core.Hints;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -277,6 +277,29 @@ public sealed class UnknownEfRepository : IUnknownRepository
|
||||
throw new NotImplementedException("Scaffold entities first");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Unknown> AttachProvenanceHintsAsync(
|
||||
string tenantId,
|
||||
Guid id,
|
||||
IReadOnlyList<ProvenanceHint> hints,
|
||||
string? bestHypothesis,
|
||||
double? combinedConfidence,
|
||||
string? primarySuggestedAction,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException("Scaffold entities first");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<Unknown>> GetWithHighConfidenceHintsAsync(
|
||||
string tenantId,
|
||||
double minConfidence = 0.7,
|
||||
int? limit = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException("Scaffold entities first");
|
||||
}
|
||||
|
||||
// Helper DTOs for raw SQL queries
|
||||
private sealed record UnknownDto;
|
||||
private sealed record KindCount(string Kind, long Count);
|
||||
|
||||
@@ -231,4 +231,27 @@ public sealed class UnknownEfRepository : IUnknownRepository
|
||||
{
|
||||
throw new NotImplementedException("Scaffold entities first");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Unknown> AttachProvenanceHintsAsync(
|
||||
string tenantId,
|
||||
Guid id,
|
||||
IReadOnlyList<ProvenanceHint> hints,
|
||||
string? bestHypothesis,
|
||||
double? combinedConfidence,
|
||||
string? primarySuggestedAction,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException("Scaffold entities first");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<Unknown>> GetWithHighConfidenceHintsAsync(
|
||||
string tenantId,
|
||||
double minConfidence = 0.7,
|
||||
int? limit = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException("Scaffold entities first");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -776,6 +776,29 @@ public sealed class PostgresUnknownRepository : IUnknownRepository
|
||||
return results;
|
||||
}
|
||||
|
||||
public Task<Unknown> AttachProvenanceHintsAsync(
|
||||
string tenantId,
|
||||
Guid id,
|
||||
IReadOnlyList<ProvenanceHint> hints,
|
||||
string? bestHypothesis,
|
||||
double? combinedConfidence,
|
||||
string? primarySuggestedAction,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: Implement provenance hints storage
|
||||
throw new NotImplementedException("Provenance hints storage not yet implemented");
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<Unknown>> GetWithHighConfidenceHintsAsync(
|
||||
string tenantId,
|
||||
double minConfidence = 0.7,
|
||||
int? limit = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// TODO: Implement provenance hints query
|
||||
throw new NotImplementedException("Provenance hints query not yet implemented");
|
||||
}
|
||||
|
||||
private static async Task SetTenantContextAsync(
|
||||
NpgsqlConnection connection,
|
||||
string tenantId,
|
||||
|
||||
@@ -37,7 +37,7 @@ public sealed class ProvenanceHintBuilderTests
|
||||
hint.Evidence.BuildId.Should().NotBeNull();
|
||||
hint.Evidence.BuildId!.BuildId.Should().Be("abc123");
|
||||
hint.Evidence.BuildId.MatchedPackage.Should().Be("openssl");
|
||||
hint.SuggestedActions.Should().HaveCountGreaterOrEqualTo(1);
|
||||
hint.SuggestedActions.Should().HaveCountGreaterThanOrEqualTo(1);
|
||||
hint.SuggestedActions[0].Action.Should().Be("verify_build_id");
|
||||
hint.HintId.Should().StartWith("hint:sha256:");
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ public sealed class ProvenanceHintSerializationTests
|
||||
|
||||
// Assert
|
||||
deserialized.Should().NotBeNull();
|
||||
deserialized!.SuggestedActions.Should().HaveCountGreaterOrEqualTo(1);
|
||||
deserialized!.SuggestedActions.Should().HaveCountGreaterThanOrEqualTo(1);
|
||||
deserialized.SuggestedActions[0].Action.Should().NotBeNullOrEmpty();
|
||||
deserialized.SuggestedActions[0].Priority.Should().BeGreaterThan(0);
|
||||
deserialized.SuggestedActions[0].Effort.Should().NotBeNullOrEmpty();
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# AGENTS - Unknowns WebService Tests
|
||||
|
||||
## Roles
|
||||
- QA / test engineer: endpoint coverage and deterministic fixtures.
|
||||
- Backend engineer: align API contract and test helpers.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/unknowns/architecture.md
|
||||
- src/Unknowns/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/Unknowns/__Tests/StellaOps.Unknowns.WebService.Tests
|
||||
- Test target: src/Unknowns/StellaOps.Unknowns.WebService
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Use fixed timestamps and deterministic IDs in test fixtures.
|
||||
- Validate tenant handling and reject missing headers.
|
||||
|
||||
## Testing
|
||||
- Cover auth/tenant enforcement, paging bounds, asOf queries, and history endpoint behavior.
|
||||
- Include negative cases for invalid inputs (minConfidence, take/skip).
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
<PackageReference Include="coverlet.collector">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Unknowns.WebService\StellaOps.Unknowns.WebService.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,360 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// UnknownsEndpointsTests.cs
|
||||
// Sprint: SPRINT_20260106_001_005_UNKNOWNS_provenance_hints
|
||||
// Task: WS-007 - Add integration tests for WebService endpoints
|
||||
// Description: Integration tests for Unknowns WebService endpoints
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Unknowns.Core.Models;
|
||||
using StellaOps.Unknowns.Core.Repositories;
|
||||
using StellaOps.Unknowns.WebService.Endpoints;
|
||||
using Xunit;
|
||||
using NSubstitute;
|
||||
|
||||
namespace StellaOps.Unknowns.WebService.Tests;
|
||||
|
||||
public sealed class UnknownsEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly IUnknownRepository _mockRepository;
|
||||
|
||||
public UnknownsEndpointsTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_mockRepository = Substitute.For<IUnknownRepository>();
|
||||
|
||||
_factory = factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Remove existing repository registration
|
||||
var descriptor = services.SingleOrDefault(
|
||||
d => d.ServiceType == typeof(IUnknownRepository));
|
||||
if (descriptor != null)
|
||||
{
|
||||
services.Remove(descriptor);
|
||||
}
|
||||
|
||||
// Add mock repository
|
||||
services.AddSingleton(_mockRepository);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task ListUnknowns_ReturnsEmptyList_WhenNoUnknowns()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
_mockRepository.GetOpenUnknownsAsync(tenantId, Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns([]);
|
||||
_mockRepository.CountOpenAsync(tenantId, Arg.Any<CancellationToken>())
|
||||
.Returns(0);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/unknowns");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownsListResponse>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result.Items);
|
||||
Assert.Equal(0, result.Total);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetUnknownById_ReturnsUnknown_WhenExists()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
var unknownId = Guid.NewGuid();
|
||||
var unknown = CreateTestUnknown(unknownId, tenantId);
|
||||
|
||||
_mockRepository.GetByIdAsync(tenantId, unknownId, Arg.Any<CancellationToken>())
|
||||
.Returns(unknown);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"/api/unknowns/{unknownId}");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownDto>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(unknownId, result.Id);
|
||||
Assert.Equal("AmbiguousPackage", result.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetUnknownById_ReturnsNotFound_WhenDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
var unknownId = Guid.NewGuid();
|
||||
|
||||
_mockRepository.GetByIdAsync(tenantId, unknownId, Arg.Any<CancellationToken>())
|
||||
.Returns((Unknown?)null);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"/api/unknowns/{unknownId}");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetUnknownHints_ReturnsHints_WhenUnknownHasHints()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
var unknownId = Guid.NewGuid();
|
||||
var unknown = CreateTestUnknownWithHints(unknownId, tenantId);
|
||||
|
||||
_mockRepository.GetByIdAsync(tenantId, unknownId, Arg.Any<CancellationToken>())
|
||||
.Returns(unknown);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"/api/unknowns/{unknownId}/hints");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ProvenanceHintsResponse>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(unknownId, result.UnknownId);
|
||||
Assert.NotEmpty(result.Hints);
|
||||
Assert.Equal("Likely debian:bookworm backport", result.BestHypothesis);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetByTriageBand_ReturnsHotUnknowns()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
var unknowns = new List<Unknown>
|
||||
{
|
||||
CreateTestUnknown(Guid.NewGuid(), tenantId, TriageBand.Hot),
|
||||
CreateTestUnknown(Guid.NewGuid(), tenantId, TriageBand.Hot)
|
||||
};
|
||||
|
||||
_mockRepository.GetByTriageBandAsync(tenantId, TriageBand.Hot, Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(unknowns);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/unknowns/triage/Hot");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownsListResponse>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.Items.Count);
|
||||
Assert.All(result.Items, item => Assert.Equal("Hot", item.TriageBand));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetHotQueue_ReturnsHotUnknowns()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
var unknowns = new List<Unknown>
|
||||
{
|
||||
CreateTestUnknown(Guid.NewGuid(), tenantId, TriageBand.Hot)
|
||||
};
|
||||
|
||||
_mockRepository.GetHotQueueAsync(tenantId, Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(unknowns);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/unknowns/hot-queue");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownsListResponse>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result.Items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetHighConfidenceHints_ReturnsFilteredResults()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
var unknowns = new List<Unknown>
|
||||
{
|
||||
CreateTestUnknownWithHints(Guid.NewGuid(), tenantId)
|
||||
};
|
||||
|
||||
_mockRepository.GetWithHighConfidenceHintsAsync(tenantId, 0.7, Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(unknowns);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/unknowns/high-confidence?minConfidence=0.7");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownsListResponse>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result.Items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetSummary_ReturnsSummaryStatistics()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = "test-tenant";
|
||||
|
||||
_mockRepository.CountByKindAsync(tenantId, Arg.Any<CancellationToken>())
|
||||
.Returns(new Dictionary<UnknownKind, long>
|
||||
{
|
||||
{ UnknownKind.AmbiguousPackage, 5 },
|
||||
{ UnknownKind.MissingSbom, 3 }
|
||||
});
|
||||
|
||||
_mockRepository.CountBySeverityAsync(tenantId, Arg.Any<CancellationToken>())
|
||||
.Returns(new Dictionary<UnknownSeverity, long>
|
||||
{
|
||||
{ UnknownSeverity.High, 2 },
|
||||
{ UnknownSeverity.Medium, 6 }
|
||||
});
|
||||
|
||||
_mockRepository.CountByTriageBandAsync(tenantId, Arg.Any<CancellationToken>())
|
||||
.Returns(new Dictionary<TriageBand, long>
|
||||
{
|
||||
{ TriageBand.Hot, 3 },
|
||||
{ TriageBand.Warm, 5 }
|
||||
});
|
||||
|
||||
_mockRepository.CountOpenAsync(tenantId, Arg.Any<CancellationToken>())
|
||||
.Returns(8);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/unknowns/summary");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownsSummaryResponse>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(8, result.TotalOpen);
|
||||
Assert.Equal(5, result.ByKind["AmbiguousPackage"]);
|
||||
Assert.Equal(3, result.ByTriageBand["Hot"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task HealthCheck_ReturnsHealthy_WhenDatabaseIsAvailable()
|
||||
{
|
||||
// Arrange
|
||||
_mockRepository.GetOpenUnknownsAsync(Arg.Any<string>(), 1, Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns([]);
|
||||
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/health");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private static Unknown CreateTestUnknown(
|
||||
Guid id,
|
||||
string tenantId,
|
||||
TriageBand band = TriageBand.Warm)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
return new Unknown
|
||||
{
|
||||
Id = id,
|
||||
TenantId = tenantId,
|
||||
SubjectHash = $"sha256:{Guid.NewGuid():N}",
|
||||
SubjectType = UnknownSubjectType.Package,
|
||||
SubjectRef = "pkg:npm/lodash@4.17.21",
|
||||
Kind = UnknownKind.AmbiguousPackage,
|
||||
Severity = UnknownSeverity.Medium,
|
||||
ValidFrom = now.AddDays(-7),
|
||||
SysFrom = now.AddDays(-7),
|
||||
CompositeScore = band == TriageBand.Hot ? 0.85 : 0.55,
|
||||
TriageBand = band,
|
||||
CreatedAt = now.AddDays(-7),
|
||||
CreatedBy = "system",
|
||||
ProvenanceHints = []
|
||||
};
|
||||
}
|
||||
|
||||
private static Unknown CreateTestUnknownWithHints(Guid id, string tenantId)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
return new Unknown
|
||||
{
|
||||
Id = id,
|
||||
TenantId = tenantId,
|
||||
SubjectHash = $"sha256:{Guid.NewGuid():N}",
|
||||
SubjectType = UnknownSubjectType.Binary,
|
||||
SubjectRef = "/usr/lib/x86_64-linux-gnu/libssl.so.3",
|
||||
Kind = UnknownKind.UnknownBuildId,
|
||||
Severity = UnknownSeverity.High,
|
||||
ValidFrom = now.AddDays(-3),
|
||||
SysFrom = now.AddDays(-3),
|
||||
CompositeScore = 0.72,
|
||||
TriageBand = TriageBand.Hot,
|
||||
CreatedAt = now.AddDays(-3),
|
||||
CreatedBy = "scanner",
|
||||
ProvenanceHints = [
|
||||
new ProvenanceHint
|
||||
{
|
||||
Id = $"hint:{Guid.NewGuid():N}",
|
||||
Type = ProvenanceHintType.BuildIdMatch,
|
||||
Confidence = 0.85,
|
||||
ConfidenceLevel = HintConfidence.High,
|
||||
Hypothesis = "Likely debian:bookworm backport",
|
||||
SuggestedActions = [
|
||||
new SuggestedAction
|
||||
{
|
||||
Action = "Verify debian package version",
|
||||
Priority = 1,
|
||||
Description = "Check against Debian security tracker"
|
||||
}
|
||||
],
|
||||
GeneratedAt = now.AddDays(-3)
|
||||
}
|
||||
],
|
||||
BestHypothesis = "Likely debian:bookworm backport",
|
||||
CombinedConfidence = 0.85,
|
||||
PrimarySuggestedAction = "Verify debian package version"
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user