up
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
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,6 +1,8 @@
using System.Collections.Generic;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
@@ -17,10 +19,20 @@ builder.Services.AddSwaggerGen(options =>
{
Title = "StellaOps Vuln Explorer API",
Version = "v1",
Description = "Deterministic vulnerability listing/detail endpoints"
Description = "Deterministic vulnerability listing/detail and VEX decision endpoints"
});
});
// Configure JSON serialization with enum string converter
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
// Register VEX decision store
builder.Services.AddSingleton<VexDecisionStore>();
var app = builder.Build();
app.UseSwagger();
@@ -61,6 +73,103 @@ app.MapGet("/v1/vulns/{id}", ([FromHeader(Name = "x-stella-tenant")] string? ten
})
.WithOpenApi();
// ============================================================================
// VEX Decision Endpoints (API-VEX-06-001, API-VEX-06-002, API-VEX-06-003)
// ============================================================================
app.MapPost("/v1/vex-decisions", (
[FromHeader(Name = "x-stella-tenant")] string? tenant,
[FromHeader(Name = "x-stella-user-id")] string? userId,
[FromHeader(Name = "x-stella-user-name")] string? userName,
[FromBody] CreateVexDecisionRequest request,
VexDecisionStore store) =>
{
if (string.IsNullOrWhiteSpace(tenant))
{
return Results.BadRequest(new { error = "x-stella-tenant required" });
}
if (string.IsNullOrWhiteSpace(request.VulnerabilityId))
{
return Results.BadRequest(new { error = "vulnerabilityId is required" });
}
if (request.Subject is null)
{
return Results.BadRequest(new { error = "subject is required" });
}
var effectiveUserId = userId ?? "anonymous";
var effectiveUserName = userName ?? "Anonymous User";
var decision = store.Create(request, effectiveUserId, effectiveUserName);
return Results.Created($"/v1/vex-decisions/{decision.Id}", decision);
})
.WithName("CreateVexDecision")
.WithOpenApi();
app.MapPatch("/v1/vex-decisions/{id:guid}", (
[FromHeader(Name = "x-stella-tenant")] string? tenant,
Guid id,
[FromBody] UpdateVexDecisionRequest request,
VexDecisionStore store) =>
{
if (string.IsNullOrWhiteSpace(tenant))
{
return Results.BadRequest(new { error = "x-stella-tenant required" });
}
var updated = store.Update(id, request);
return updated is not null
? Results.Ok(updated)
: Results.NotFound(new { error = $"VEX decision {id} not found" });
})
.WithName("UpdateVexDecision")
.WithOpenApi();
app.MapGet("/v1/vex-decisions", ([AsParameters] VexDecisionFilter filter, VexDecisionStore store) =>
{
if (string.IsNullOrWhiteSpace(filter.Tenant))
{
return Results.BadRequest(new { error = "x-stella-tenant required" });
}
var pageSize = Math.Clamp(filter.PageSize ?? 50, 1, 200);
var offset = ParsePageToken(filter.PageToken);
var decisions = store.Query(
vulnerabilityId: filter.VulnerabilityId,
subjectName: filter.Subject,
status: filter.Status,
skip: offset,
take: pageSize);
var nextOffset = offset + decisions.Count;
var next = nextOffset < store.Count() ? nextOffset.ToString(CultureInfo.InvariantCulture) : null;
return Results.Ok(new VexDecisionListResponse(decisions, next));
})
.WithName("ListVexDecisions")
.WithOpenApi();
app.MapGet("/v1/vex-decisions/{id:guid}", (
[FromHeader(Name = "x-stella-tenant")] string? tenant,
Guid id,
VexDecisionStore store) =>
{
if (string.IsNullOrWhiteSpace(tenant))
{
return Results.BadRequest(new { error = "x-stella-tenant required" });
}
var decision = store.Get(id);
return decision is not null
? Results.Ok(decision)
: Results.NotFound(new { error = $"VEX decision {id} not found" });
})
.WithName("GetVexDecision")
.WithOpenApi();
app.Run();
static int ParsePageToken(string? token) =>
@@ -117,6 +226,12 @@ public record VulnFilter(
[FromQuery(Name = "exploitability")] string? Exploitability,
[FromQuery(Name = "fixAvailable")] bool? FixAvailable);
public partial class Program { }
public record VexDecisionFilter(
[FromHeader(Name = "x-stella-tenant")] string? Tenant,
[FromQuery(Name = "vulnerabilityId")] string? VulnerabilityId,
[FromQuery(Name = "subject")] string? Subject,
[FromQuery(Name = "status")] VexStatus? Status,
[FromQuery(Name = "pageSize")] int? PageSize,
[FromQuery(Name = "pageToken")] string? PageToken);
public partial class Program { }