sprints work

This commit is contained in:
StellaOps Bot
2025-12-25 12:19:12 +02:00
parent 223843f1d1
commit 2a06f780cf
224 changed files with 41796 additions and 1515 deletions

View File

@@ -0,0 +1,481 @@
// -----------------------------------------------------------------------------
// Program.cs
// StellaOps Replay Token WebService
// Sprint: SPRINT_5100_0010_0001 - EvidenceLocker + Findings Ledger + Replay Test Implementation
// Task: REPLAY-5100-004 - Replay.WebService for token issuance and verification
// -----------------------------------------------------------------------------
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http.HttpResults;
using Serilog;
using Serilog.Events;
using StellaOps.Audit.ReplayToken;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Configuration;
using StellaOps.Cryptography;
using StellaOps.DependencyInjection;
using StellaOps.Telemetry.Core;
const string ReplayReadPolicy = "replay.token.read";
const string ReplayWritePolicy = "replay.token.write";
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddStellaOpsDefaults(options =>
{
options.BasePath = builder.Environment.ContentRootPath;
options.EnvironmentPrefix = "REPLAY_";
options.ConfigureBuilder = configurationBuilder =>
{
configurationBuilder.AddYamlFile("../etc/replay.yaml", optional: true, reloadOnChange: true);
};
});
builder.Host.UseSerilog((context, services, loggerConfiguration) =>
{
loggerConfiguration
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Console();
});
builder.Services.AddOptions<ReplayServiceOptions>()
.Bind(builder.Configuration.GetSection(ReplayServiceOptions.SectionName))
.ValidateOnStart();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<ICryptoHash, DefaultCryptoHash>();
builder.Services.AddSingleton<IReplayTokenGenerator, Sha256ReplayTokenGenerator>();
builder.Services.AddProblemDetails();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddHealthChecks();
builder.Services.AddStellaOpsTelemetry(builder.Configuration, "replay-webservice");
var authConfig = builder.Configuration.GetSection("Replay:Authority").Get<AuthorityConfig>() ?? new AuthorityConfig();
builder.Services.AddStellaOpsResourceServerAuthentication(
builder.Configuration,
configurationSection: null,
configure: resourceOptions =>
{
resourceOptions.Authority = authConfig.Issuer;
resourceOptions.RequireHttpsMetadata = authConfig.RequireHttpsMetadata;
resourceOptions.MetadataAddress = authConfig.MetadataAddress;
resourceOptions.Audiences.Clear();
foreach (var audience in authConfig.Audiences)
{
resourceOptions.Audiences.Add(audience);
}
resourceOptions.RequiredScopes.Clear();
foreach (var scope in authConfig.RequiredScopes)
{
resourceOptions.RequiredScopes.Add(scope);
}
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(ReplayReadPolicy, policy =>
{
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new StellaOpsScopeRequirement(new[] { StellaOpsScopes.VulnOperate }));
policy.AddAuthenticationSchemes(StellaOpsAuthenticationDefaults.AuthenticationScheme);
});
options.AddPolicy(ReplayWritePolicy, policy =>
{
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new StellaOpsScopeRequirement(new[] { StellaOpsScopes.VulnOperate }));
policy.AddAuthenticationSchemes(StellaOpsAuthenticationDefaults.AuthenticationScheme);
});
});
var app = builder.Build();
app.UseSerilogRequestLogging();
app.UseExceptionHandler(exceptionApp =>
{
exceptionApp.Run(async context =>
{
var feature = context.Features.Get<IExceptionHandlerFeature>();
if (feature?.Error is null)
{
return;
}
var problem = Results.Problem(
statusCode: StatusCodes.Status500InternalServerError,
title: "replay_internal_error",
detail: feature.Error.Message);
await problem.ExecuteAsync(context);
});
});
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz");
// POST /v1/replay/tokens - Generate a new replay token
app.MapPost("/v1/replay/tokens", Task<Results<Created<GenerateTokenResponse>, ProblemHttpResult>> (
HttpContext httpContext,
GenerateTokenRequest request,
IReplayTokenGenerator tokenGenerator,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return Task.FromResult<Results<Created<GenerateTokenResponse>, ProblemHttpResult>>(tenantProblem!);
}
var tokenRequest = new ReplayTokenRequest
{
FeedManifests = request.FeedManifests ?? Array.Empty<string>(),
RulesVersion = request.RulesVersion,
RulesHash = request.RulesHash,
LatticePolicyVersion = request.LatticePolicyVersion,
LatticePolicyHash = request.LatticePolicyHash,
InputHashes = request.InputHashes ?? Array.Empty<string>(),
ScoringConfigVersion = request.ScoringConfigVersion,
EvidenceHashes = request.EvidenceHashes ?? Array.Empty<string>(),
AdditionalContext = request.AdditionalContext ?? new Dictionary<string, string>()
};
var expiration = request.ExpirationMinutes.HasValue
? TimeSpan.FromMinutes(request.ExpirationMinutes.Value)
: ReplayToken.DefaultExpiration;
var token = request.WithExpiration
? tokenGenerator.GenerateWithExpiration(tokenRequest, expiration)
: tokenGenerator.Generate(tokenRequest);
var response = new GenerateTokenResponse(
token.Canonical,
token.Value,
token.GeneratedAt,
token.ExpiresAt,
token.Algorithm,
token.Version);
return Task.FromResult<Results<Created<GenerateTokenResponse>, ProblemHttpResult>>(
TypedResults.Created($"/v1/replay/tokens/{token.Value}", response));
})
.WithName("GenerateReplayToken")
.RequireAuthorization(ReplayWritePolicy)
.Produces(StatusCodes.Status201Created)
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status401Unauthorized)
.ProducesProblem(StatusCodes.Status403Forbidden);
// POST /v1/replay/tokens/verify - Verify a replay token
app.MapPost("/v1/replay/tokens/verify", Task<Results<Ok<VerifyTokenResponse>, ProblemHttpResult>> (
HttpContext httpContext,
VerifyTokenRequest request,
IReplayTokenGenerator tokenGenerator,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return Task.FromResult<Results<Ok<VerifyTokenResponse>, ProblemHttpResult>>(tenantProblem!);
}
if (string.IsNullOrWhiteSpace(request.Token))
{
return Task.FromResult<Results<Ok<VerifyTokenResponse>, ProblemHttpResult>>(
TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "missing_token", detail: "Token is required."));
}
ReplayToken parsedToken;
try
{
parsedToken = ReplayToken.Parse(request.Token);
}
catch (FormatException ex)
{
return Task.FromResult<Results<Ok<VerifyTokenResponse>, ProblemHttpResult>>(
TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "invalid_token_format", detail: ex.Message));
}
var tokenRequest = new ReplayTokenRequest
{
FeedManifests = request.FeedManifests ?? Array.Empty<string>(),
RulesVersion = request.RulesVersion,
RulesHash = request.RulesHash,
LatticePolicyVersion = request.LatticePolicyVersion,
LatticePolicyHash = request.LatticePolicyHash,
InputHashes = request.InputHashes ?? Array.Empty<string>(),
ScoringConfigVersion = request.ScoringConfigVersion,
EvidenceHashes = request.EvidenceHashes ?? Array.Empty<string>(),
AdditionalContext = request.AdditionalContext ?? new Dictionary<string, string>()
};
var result = tokenGenerator.VerifyWithExpiration(parsedToken, tokenRequest);
var response = new VerifyTokenResponse(
Valid: result == ReplayTokenVerificationResult.Valid,
Result: result.ToString(),
TokenValue: parsedToken.Value,
Algorithm: parsedToken.Algorithm,
Version: parsedToken.Version,
GeneratedAt: parsedToken.GeneratedAt,
ExpiresAt: parsedToken.ExpiresAt,
IsExpired: parsedToken.IsExpired(),
TimeToExpiration: parsedToken.GetTimeToExpiration());
return Task.FromResult<Results<Ok<VerifyTokenResponse>, ProblemHttpResult>>(TypedResults.Ok(response));
})
.WithName("VerifyReplayToken")
.RequireAuthorization(ReplayReadPolicy)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status401Unauthorized)
.ProducesProblem(StatusCodes.Status403Forbidden);
// GET /v1/replay/tokens/{tokenValue} - Get token details (parse only)
app.MapGet("/v1/replay/tokens/{tokenCanonical}", Task<Results<Ok<TokenInfoResponse>, NotFound, ProblemHttpResult>> (
HttpContext httpContext,
string tokenCanonical,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return Task.FromResult<Results<Ok<TokenInfoResponse>, NotFound, ProblemHttpResult>>(tenantProblem!);
}
if (!ReplayToken.TryParse(tokenCanonical, out var token) || token is null)
{
return Task.FromResult<Results<Ok<TokenInfoResponse>, NotFound, ProblemHttpResult>>(TypedResults.NotFound());
}
var response = new TokenInfoResponse(
Canonical: token.Canonical,
Value: token.Value,
Algorithm: token.Algorithm,
Version: token.Version,
GeneratedAt: token.GeneratedAt,
ExpiresAt: token.ExpiresAt,
IsExpired: token.IsExpired(),
TimeToExpiration: token.GetTimeToExpiration());
return Task.FromResult<Results<Ok<TokenInfoResponse>, NotFound, ProblemHttpResult>>(TypedResults.Ok(response));
})
.WithName("GetReplayToken")
.RequireAuthorization(ReplayReadPolicy)
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status400BadRequest);
// GET /.well-known/openapi - OpenAPI specification
app.MapGet("/.well-known/openapi", (HttpContext context) =>
{
var spec = """
openapi: 3.1.0
info:
title: StellaOps Replay Token API
version: "1.0"
description: API for generating and verifying deterministic replay tokens
paths:
/v1/replay/tokens:
post:
summary: Generate a replay token
operationId: GenerateReplayToken
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/GenerateTokenRequest'
responses:
'201':
description: Token created
content:
application/json:
schema:
$ref: '#/components/schemas/GenerateTokenResponse'
/v1/replay/tokens/verify:
post:
summary: Verify a replay token
operationId: VerifyReplayToken
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/VerifyTokenRequest'
responses:
'200':
description: Verification result
content:
application/json:
schema:
$ref: '#/components/schemas/VerifyTokenResponse'
components:
schemas:
GenerateTokenRequest:
type: object
properties:
feedManifests:
type: array
items:
type: string
rulesVersion:
type: string
rulesHash:
type: string
inputHashes:
type: array
items:
type: string
withExpiration:
type: boolean
expirationMinutes:
type: integer
GenerateTokenResponse:
type: object
properties:
canonical:
type: string
value:
type: string
generatedAt:
type: string
format: date-time
expiresAt:
type: string
format: date-time
VerifyTokenRequest:
type: object
properties:
token:
type: string
feedManifests:
type: array
items:
type: string
rulesVersion:
type: string
rulesHash:
type: string
inputHashes:
type: array
items:
type: string
VerifyTokenResponse:
type: object
properties:
valid:
type: boolean
result:
type: string
tokenValue:
type: string
isExpired:
type: boolean
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
""";
return Results.Text(spec, "application/yaml");
})
.WithName("ReplayOpenApiDocument")
.Produces(StatusCodes.Status200OK);
app.Run();
static bool TryGetTenant(HttpContext httpContext, out ProblemHttpResult? problem, out string tenantId)
{
tenantId = string.Empty;
if (!httpContext.Request.Headers.TryGetValue("X-Stella-Tenant", out var tenantValues) || string.IsNullOrWhiteSpace(tenantValues))
{
problem = TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "missing_tenant");
return false;
}
tenantId = tenantValues.ToString();
problem = null;
return true;
}
// Request/Response models
public record GenerateTokenRequest(
IReadOnlyList<string>? FeedManifests,
string? RulesVersion,
string? RulesHash,
string? LatticePolicyVersion,
string? LatticePolicyHash,
IReadOnlyList<string>? InputHashes,
string? ScoringConfigVersion,
IReadOnlyList<string>? EvidenceHashes,
IReadOnlyDictionary<string, string>? AdditionalContext,
bool WithExpiration = true,
int? ExpirationMinutes = null);
public record GenerateTokenResponse(
string Canonical,
string Value,
DateTimeOffset GeneratedAt,
DateTimeOffset? ExpiresAt,
string Algorithm,
string Version);
public record VerifyTokenRequest(
string Token,
IReadOnlyList<string>? FeedManifests,
string? RulesVersion,
string? RulesHash,
string? LatticePolicyVersion,
string? LatticePolicyHash,
IReadOnlyList<string>? InputHashes,
string? ScoringConfigVersion,
IReadOnlyList<string>? EvidenceHashes,
IReadOnlyDictionary<string, string>? AdditionalContext);
public record VerifyTokenResponse(
bool Valid,
string Result,
string TokenValue,
string Algorithm,
string Version,
DateTimeOffset GeneratedAt,
DateTimeOffset? ExpiresAt,
bool IsExpired,
TimeSpan? TimeToExpiration);
public record TokenInfoResponse(
string Canonical,
string Value,
string Algorithm,
string Version,
DateTimeOffset GeneratedAt,
DateTimeOffset? ExpiresAt,
bool IsExpired,
TimeSpan? TimeToExpiration);
// Configuration models
public class ReplayServiceOptions
{
public const string SectionName = "Replay";
public AuthorityConfig Authority { get; set; } = new();
}
public class AuthorityConfig
{
public string Issuer { get; set; } = "https://auth.stellaops.local";
public bool RequireHttpsMetadata { get; set; } = true;
public string MetadataAddress { get; set; } = "https://auth.stellaops.local/.well-known/openid-configuration";
public List<string> Audiences { get; set; } = new() { "stellaops-api" };
public List<string> RequiredScopes { get; set; } = new() { "vuln.operate" };
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Audit.ReplayToken\StellaOps.Audit.ReplayToken.csproj" />
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj" />
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Configuration\StellaOps.Configuration.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
<ProjectReference Include="..\..\Telemetry\StellaOps.Telemetry.Core\StellaOps.Telemetry.Core\StellaOps.Telemetry.Core.csproj" />
</ItemGroup>
</Project>