feat: Implement Runtime Facts ingestion service and NDJSON reader
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added RuntimeFactsNdjsonReader for reading NDJSON formatted runtime facts.
- Introduced IRuntimeFactsIngestionService interface and its implementation.
- Enhanced Program.cs to register new services and endpoints for runtime facts.
- Updated CallgraphIngestionService to include CAS URI in stored artifacts.
- Created RuntimeFactsValidationException for validation errors during ingestion.
- Added tests for RuntimeFactsIngestionService and RuntimeFactsNdjsonReader.
- Implemented SignalsSealedModeMonitor for compliance checks in sealed mode.
- Updated project dependencies for testing utilities.
This commit is contained in:
master
2025-11-10 07:56:15 +02:00
parent 9df52d84aa
commit 69c59defdc
132 changed files with 19718 additions and 9334 deletions

View File

@@ -1,6 +1,7 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using NetEscapades.Configuration.Yaml;
@@ -12,10 +13,10 @@ using StellaOps.Signals.Hosting;
using StellaOps.Signals.Models;
using StellaOps.Signals.Options;
using StellaOps.Signals.Parsing;
using StellaOps.Signals.Persistence;
using StellaOps.Signals.Routing;
using StellaOps.Signals.Services;
using StellaOps.Signals.Storage;
using StellaOps.Signals.Persistence;
using StellaOps.Signals.Routing;
using StellaOps.Signals.Services;
using StellaOps.Signals.Storage;
var builder = WebApplication.CreateBuilder(args);
@@ -73,9 +74,10 @@ builder.Services.AddOptions<SignalsOptions>()
.ValidateOnStart();
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<SignalsOptions>>().Value);
builder.Services.AddSingleton<SignalsStartupState>();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddProblemDetails();
builder.Services.AddSingleton<SignalsStartupState>();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<SignalsSealedModeMonitor>();
builder.Services.AddProblemDetails();
builder.Services.AddHealthChecks();
builder.Services.AddRouting(options => options.LowercaseUrls = true);
@@ -122,6 +124,7 @@ builder.Services.AddSingleton<ICallgraphParserResolver, CallgraphParserResolver>
builder.Services.AddSingleton<ICallgraphIngestionService, CallgraphIngestionService>();
builder.Services.AddSingleton<IReachabilityFactRepository, MongoReachabilityFactRepository>();
builder.Services.AddSingleton<IReachabilityScoringService, ReachabilityScoringService>();
builder.Services.AddSingleton<IRuntimeFactsIngestionService, RuntimeFactsIngestionService>();
if (bootstrap.Authority.Enabled)
{
@@ -189,39 +192,64 @@ app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").AllowAnonymous();
app.MapGet("/readyz", static (SignalsStartupState state) =>
state.IsReady ? Results.Ok(new { status = "ready" }) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable))
.AllowAnonymous();
app.MapGet("/readyz", (SignalsStartupState state, SignalsSealedModeMonitor sealedModeMonitor) =>
{
if (!sealedModeMonitor.IsCompliant(out var reason))
{
return Results.Json(
new { status = "sealed-mode-blocked", reason },
statusCode: StatusCodes.Status503ServiceUnavailable);
}
return state.IsReady
? Results.Ok(new { status = "ready" })
: Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}).AllowAnonymous();
var fallbackAllowed = !bootstrap.Authority.Enabled || bootstrap.Authority.AllowAnonymousFallback;
var fallbackAllowed = !bootstrap.Authority.Enabled || bootstrap.Authority.AllowAnonymousFallback;
var signalsGroup = app.MapGroup("/signals");
signalsGroup.MapGet("/ping", (HttpContext context, SignalsOptions options, SignalsSealedModeMonitor sealedModeMonitor) =>
Program.TryAuthorize(context, requiredScope: SignalsPolicies.Read, fallbackAllowed: options.Authority.AllowAnonymousFallback, out var authFailure) &&
Program.TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure)
? Results.NoContent()
: authFailure ?? sealedFailure ?? Results.Unauthorized()).WithName("SignalsPing");
var signalsGroup = app.MapGroup("/signals");
signalsGroup.MapGet("/status", (HttpContext context, SignalsOptions options, SignalsSealedModeMonitor sealedModeMonitor) =>
{
if (!Program.TryAuthorize(context, SignalsPolicies.Read, options.Authority.AllowAnonymousFallback, out var failure))
{
return failure ?? Results.Unauthorized();
}
var sealedCompliant = sealedModeMonitor.IsCompliant(out var sealedReason);
return Results.Ok(new
{
service = "signals",
version = typeof(Program).Assembly.GetName().Version?.ToString() ?? "unknown",
sealedMode = new
{
enforced = sealedModeMonitor.EnforcementEnabled,
compliant = sealedCompliant,
reason = sealedCompliant ? null : sealedReason
}
});
}).WithName("SignalsStatus");
signalsGroup.MapGet("/ping", (HttpContext context, SignalsOptions options) =>
Program.TryAuthorize(context, requiredScope: SignalsPolicies.Read, fallbackAllowed: options.Authority.AllowAnonymousFallback, out var failure)
? Results.NoContent()
: failure ?? Results.Unauthorized()).WithName("SignalsPing");
signalsGroup.MapGet("/status", (HttpContext context, SignalsOptions options) =>
Program.TryAuthorize(context, SignalsPolicies.Read, options.Authority.AllowAnonymousFallback, out var failure)
? Results.Ok(new
{
service = "signals",
version = typeof(Program).Assembly.GetName().Version?.ToString() ?? "unknown"
})
: failure ?? Results.Unauthorized()).WithName("SignalsStatus");
signalsGroup.MapPost("/callgraphs", async Task<IResult> (
HttpContext context,
SignalsOptions options,
CallgraphIngestRequest request,
ICallgraphIngestionService ingestionService,
CancellationToken cancellationToken) =>
{
if (!Program.TryAuthorize(context, SignalsPolicies.Write, options.Authority.AllowAnonymousFallback, out var failure))
{
return failure ?? Results.Unauthorized();
}
signalsGroup.MapPost("/callgraphs", async Task<IResult> (
HttpContext context,
SignalsOptions options,
CallgraphIngestRequest request,
ICallgraphIngestionService ingestionService,
SignalsSealedModeMonitor sealedModeMonitor,
CancellationToken cancellationToken) =>
{
if (!Program.TryAuthorize(context, SignalsPolicies.Write, options.Authority.AllowAnonymousFallback, out var authFailure) ||
!Program.TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure))
{
return authFailure ?? sealedFailure ?? Results.Unauthorized();
}
try
{
@@ -244,23 +272,143 @@ signalsGroup.MapPost("/callgraphs", async Task<IResult> (
{
return Results.BadRequest(new { error = ex.Message });
}
}).WithName("SignalsCallgraphIngest");
signalsGroup.MapPost("/runtime-facts", (HttpContext context, SignalsOptions options) =>
Program.TryAuthorize(context, SignalsPolicies.Write, options.Authority.AllowAnonymousFallback, out var failure)
? Results.StatusCode(StatusCodes.Status501NotImplemented)
: failure ?? Results.Unauthorized()).WithName("SignalsRuntimeIngest");
}).WithName("SignalsCallgraphIngest");
signalsGroup.MapGet("/callgraphs/{callgraphId}", async Task<IResult> (
HttpContext context,
SignalsOptions options,
string callgraphId,
ICallgraphRepository callgraphRepository,
SignalsSealedModeMonitor sealedModeMonitor,
CancellationToken cancellationToken) =>
{
if (!Program.TryAuthorize(context, SignalsPolicies.Read, options.Authority.AllowAnonymousFallback, out var authFailure) ||
!Program.TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure))
{
return authFailure ?? sealedFailure ?? Results.Unauthorized();
}
if (string.IsNullOrWhiteSpace(callgraphId))
{
return Results.BadRequest(new { error = "callgraphId is required." });
}
var document = await callgraphRepository.GetByIdAsync(callgraphId.Trim(), cancellationToken).ConfigureAwait(false);
return document is null ? Results.NotFound() : Results.Ok(document);
}).WithName("SignalsCallgraphGet");
signalsGroup.MapPost("/runtime-facts", async Task<IResult> (
HttpContext context,
SignalsOptions options,
RuntimeFactsIngestRequest request,
IRuntimeFactsIngestionService ingestionService,
SignalsSealedModeMonitor sealedModeMonitor,
CancellationToken cancellationToken) =>
{
if (!Program.TryAuthorize(context, SignalsPolicies.Write, options.Authority.AllowAnonymousFallback, out var authFailure) ||
!Program.TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure))
{
return authFailure ?? sealedFailure ?? Results.Unauthorized();
}
try
{
var response = await ingestionService.IngestAsync(request, cancellationToken).ConfigureAwait(false);
return Results.Accepted($"/signals/runtime-facts/{response.SubjectKey}", response);
}
catch (RuntimeFactsValidationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}).WithName("SignalsRuntimeIngest");
signalsGroup.MapPost("/runtime-facts/ndjson", async Task<IResult> (
HttpContext context,
SignalsOptions options,
RuntimeFactsStreamMetadata metadata,
IRuntimeFactsIngestionService ingestionService,
SignalsSealedModeMonitor sealedModeMonitor,
CancellationToken cancellationToken) =>
{
if (!Program.TryAuthorize(context, SignalsPolicies.Write, options.Authority.AllowAnonymousFallback, out var authFailure) ||
!Program.TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure))
{
return authFailure ?? sealedFailure ?? Results.Unauthorized();
}
if (metadata is null || string.IsNullOrWhiteSpace(metadata.CallgraphId))
{
return Results.BadRequest(new { error = "callgraphId is required." });
}
var subject = new ReachabilitySubject
{
ScanId = metadata.ScanId,
ImageDigest = metadata.ImageDigest,
Component = metadata.Component,
Version = metadata.Version
};
var isGzip = string.Equals(context.Request.Headers.ContentEncoding, "gzip", StringComparison.OrdinalIgnoreCase);
var events = await RuntimeFactsNdjsonReader.ReadAsync(context.Request.Body, isGzip, cancellationToken).ConfigureAwait(false);
if (events.Count == 0)
{
return Results.BadRequest(new { error = "runtime fact stream was empty." });
}
var request = new RuntimeFactsIngestRequest
{
Subject = subject,
CallgraphId = metadata.CallgraphId,
Events = events
};
try
{
var response = await ingestionService.IngestAsync(request, cancellationToken).ConfigureAwait(false);
return Results.Accepted($"/signals/runtime-facts/{response.SubjectKey}", response);
}
catch (RuntimeFactsValidationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}).WithName("SignalsRuntimeIngestNdjson");
signalsGroup.MapGet("/facts/{subjectKey}", async Task<IResult> (
HttpContext context,
SignalsOptions options,
string subjectKey,
IReachabilityFactRepository factRepository,
SignalsSealedModeMonitor sealedModeMonitor,
CancellationToken cancellationToken) =>
{
if (!Program.TryAuthorize(context, SignalsPolicies.Read, options.Authority.AllowAnonymousFallback, out var authFailure) ||
!Program.TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure))
{
return authFailure ?? sealedFailure ?? Results.Unauthorized();
}
if (string.IsNullOrWhiteSpace(subjectKey))
{
return Results.BadRequest(new { error = "subjectKey is required." });
}
var fact = await factRepository.GetBySubjectAsync(subjectKey.Trim(), cancellationToken).ConfigureAwait(false);
return fact is null ? Results.NotFound() : Results.Ok(fact);
}).WithName("SignalsFactsGet");
signalsGroup.MapPost("/reachability/recompute", async Task<IResult> (
HttpContext context,
SignalsOptions options,
ReachabilityRecomputeRequest request,
IReachabilityScoringService scoringService,
SignalsSealedModeMonitor sealedModeMonitor,
CancellationToken cancellationToken) =>
{
if (!Program.TryAuthorize(context, SignalsPolicies.Admin, options.Authority.AllowAnonymousFallback, out var failure))
if (!Program.TryAuthorize(context, SignalsPolicies.Admin, options.Authority.AllowAnonymousFallback, out var authFailure) ||
!Program.TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure))
{
return failure ?? Results.Unauthorized();
return authFailure ?? sealedFailure ?? Results.Unauthorized();
}
try
@@ -285,6 +433,27 @@ signalsGroup.MapPost("/reachability/recompute", async Task<IResult> (
return Results.NotFound(new { error = ex.Message });
}
}).WithName("SignalsReachabilityRecompute");
signalsGroup.MapGet("/facts/{subjectKey}", async Task<IResult> (
HttpContext context,
SignalsOptions options,
string subjectKey,
IReachabilityFactRepository factRepository,
CancellationToken cancellationToken) =>
{
if (!Program.TryAuthorize(context, SignalsPolicies.Read, options.Authority.AllowAnonymousFallback, out var failure))
{
return failure ?? Results.Unauthorized();
}
if (string.IsNullOrWhiteSpace(subjectKey))
{
return Results.BadRequest(new { error = "subjectKey is required." });
}
var fact = await factRepository.GetBySubjectAsync(subjectKey.Trim(), cancellationToken).ConfigureAwait(false);
return fact is null ? Results.NotFound() : Results.Ok(fact);
}).WithName("SignalsFactsGet");
app.Run();
@@ -376,4 +545,24 @@ public partial class Program
// Ignore when indexes already exist with different options to keep startup idempotent.
}
}
internal static bool TryEnsureSealedMode(SignalsSealedModeMonitor monitor, out IResult? failure)
{
if (!monitor.EnforcementEnabled)
{
failure = null;
return true;
}
if (monitor.IsCompliant(out var reason))
{
failure = null;
return true;
}
failure = Results.Json(
new { error = "sealed-mode evidence invalid", reason },
statusCode: StatusCodes.Status503ServiceUnavailable);
return false;
}
}