feat: Add documentation and task tracking for Sprints 508 to 514 in Ops & Offline

- Created detailed markdown files for Sprints 508 (Ops Offline Kit), 509 (Samples), 510 (AirGap), 511 (Api), 512 (Bench), 513 (Provenance), and 514 (Sovereign Crypto Enablement) outlining tasks, dependencies, and owners.
- Introduced a comprehensive Reachability Evidence Delivery Guide to streamline the reachability signal process.
- Implemented unit tests for Advisory AI to block known injection patterns and redact secrets.
- Added AuthoritySenderConstraintHelper to manage sender constraints in OpenIddict transactions.
This commit is contained in:
master
2025-11-08 23:18:28 +02:00
parent 536f6249a6
commit ae69b1a8a1
187 changed files with 4326 additions and 3196 deletions

View File

@@ -5,6 +5,8 @@ using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -16,6 +18,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Concelier.Core.Events;
@@ -101,6 +104,7 @@ builder.Services.AddConcelierAocGuards();
builder.Services.AddConcelierLinksetMappers();
builder.Services.AddAdvisoryRawServices();
builder.Services.AddSingleton<IAdvisoryObservationQueryService, AdvisoryObservationQueryService>();
builder.Services.AddSingleton<AdvisoryChunkBuilder>();
var features = concelierOptions.Features ?? new ConcelierOptions.FeaturesOptions();
@@ -139,6 +143,7 @@ builder.Services.AddAocGuard();
var authorityConfigured = concelierOptions.Authority is { Enabled: true };
if (authorityConfigured)
{
builder.Services.AddStellaOpsAuthClient(clientOptions =>
@@ -180,36 +185,61 @@ if (authorityConfigured)
}
});
builder.Services.AddStellaOpsResourceServerAuthentication(
builder.Configuration,
configurationSection: null,
configure: resourceOptions =>
{
resourceOptions.Authority = concelierOptions.Authority.Issuer;
resourceOptions.RequireHttpsMetadata = concelierOptions.Authority.RequireHttpsMetadata;
resourceOptions.BackchannelTimeout = TimeSpan.FromSeconds(concelierOptions.Authority.BackchannelTimeoutSeconds);
resourceOptions.TokenClockSkew = TimeSpan.FromSeconds(concelierOptions.Authority.TokenClockSkewSeconds);
if (!string.IsNullOrWhiteSpace(concelierOptions.Authority.MetadataAddress))
if (string.IsNullOrWhiteSpace(concelierOptions.Authority.TestSigningSecret))
{
builder.Services.AddStellaOpsResourceServerAuthentication(
builder.Configuration,
configurationSection: null,
configure: resourceOptions =>
{
resourceOptions.MetadataAddress = concelierOptions.Authority.MetadataAddress;
}
resourceOptions.Authority = concelierOptions.Authority.Issuer;
resourceOptions.RequireHttpsMetadata = concelierOptions.Authority.RequireHttpsMetadata;
resourceOptions.BackchannelTimeout = TimeSpan.FromSeconds(concelierOptions.Authority.BackchannelTimeoutSeconds);
resourceOptions.TokenClockSkew = TimeSpan.FromSeconds(concelierOptions.Authority.TokenClockSkewSeconds);
foreach (var audience in concelierOptions.Authority.Audiences)
{
resourceOptions.Audiences.Add(audience);
}
if (!string.IsNullOrWhiteSpace(concelierOptions.Authority.MetadataAddress))
{
resourceOptions.MetadataAddress = concelierOptions.Authority.MetadataAddress;
}
foreach (var scope in concelierOptions.Authority.RequiredScopes)
{
resourceOptions.RequiredScopes.Add(scope);
}
foreach (var audience in concelierOptions.Authority.Audiences)
{
resourceOptions.Audiences.Add(audience);
}
foreach (var network in concelierOptions.Authority.BypassNetworks)
foreach (var scope in concelierOptions.Authority.RequiredScopes)
{
resourceOptions.RequiredScopes.Add(scope);
}
foreach (var network in concelierOptions.Authority.BypassNetworks)
{
resourceOptions.BypassNetworks.Add(network);
}
});
}
else
{
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
resourceOptions.BypassNetworks.Add(network);
}
});
options.RequireHttpsMetadata = concelierOptions.Authority.RequireHttpsMetadata;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(concelierOptions.Authority.TestSigningSecret!)),
ValidateIssuer = true,
ValidIssuer = concelierOptions.Authority.Issuer,
ValidateAudience = concelierOptions.Authority.Audiences.Count > 0,
ValidAudiences = concelierOptions.Authority.Audiences,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(concelierOptions.Authority.TokenClockSkewSeconds),
NameClaimType = StellaOpsClaimTypes.Subject,
RoleClaimType = ClaimTypes.Role
};
});
}
}
builder.Services.AddAuthorization(options =>
{
@@ -250,6 +280,12 @@ if (resolvedAuthority.Enabled && resolvedAuthority.AllowAnonymousFallback)
"Authority authentication is configured but anonymous fallback remains enabled. Set authority.allowAnonymousFallback to false before 2025-12-31 to complete the rollout.");
}
if (authorityConfigured)
{
app.UseAuthentication();
app.UseAuthorization();
}
app.MapConcelierMirrorEndpoints(authorityConfigured, enforceAuthority);
app.MapGet("/.well-known/openapi", ([FromServices] OpenApiDiscoveryDocumentProvider provider, HttpContext context) =>
@@ -689,16 +725,18 @@ var advisoryEvidenceEndpoint = app.MapGet("/vuln/evidence/advisories/{advisoryKe
return Problem(context, "advisoryKey is required", StatusCodes.Status400BadRequest, ProblemTypes.Validation, "Provide an advisory identifier.");
}
var normalizedKey = advisoryKey.Trim();
var canonicalKey = normalizedKey.ToUpperInvariant();
var vendorFilter = AdvisoryRawRequestMapper.NormalizeStrings(context.Request.Query["vendor"]);
var records = await rawService.FindByAdvisoryKeyAsync(
tenant,
advisoryKey,
canonicalKey,
vendorFilter,
cancellationToken).ConfigureAwait(false);
if (records.Count == 0)
{
return Results.NotFound();
return Problem(context, "Advisory not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, $"No evidence available for {normalizedKey}.");
}
var recordResponses = records
@@ -710,7 +748,8 @@ var advisoryEvidenceEndpoint = app.MapGet("/vuln/evidence/advisories/{advisoryKe
record.Document))
.ToArray();
var response = new AdvisoryEvidenceResponse(recordResponses[0].Document.AdvisoryKey, recordResponses);
var responseKey = recordResponses[0].Document.AdvisoryKey ?? canonicalKey;
var response = new AdvisoryEvidenceResponse(responseKey, recordResponses);
return JsonResult(response);
});
if (authorityConfigured)
@@ -718,6 +757,67 @@ if (authorityConfigured)
advisoryEvidenceEndpoint.RequireAuthorization(AdvisoryReadPolicyName);
}
var advisoryChunksEndpoint = app.MapGet("/advisories/{advisoryKey}/chunks", async (
string advisoryKey,
HttpContext context,
[FromServices] IAdvisoryObservationQueryService observationService,
[FromServices] AdvisoryChunkBuilder chunkBuilder,
CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
if (!TryResolveTenant(context, requireHeader: false, out var tenant, out var tenantError))
{
return tenantError;
}
var authorizationError = EnsureTenantAuthorized(context, tenant);
if (authorizationError is not null)
{
return authorizationError;
}
if (string.IsNullOrWhiteSpace(advisoryKey))
{
return Problem(context, "advisoryKey is required", StatusCodes.Status400BadRequest, ProblemTypes.Validation, "Provide an advisory identifier.");
}
var normalizedKey = advisoryKey.Trim();
var chunkSettings = resolvedConcelierOptions.AdvisoryChunks ?? new ConcelierOptions.AdvisoryChunkOptions();
var chunkLimit = ResolveBoundedInt(context.Request.Query["limit"], chunkSettings.DefaultChunkLimit, 1, chunkSettings.MaxChunkLimit);
var observationLimit = ResolveBoundedInt(context.Request.Query["observations"], chunkSettings.DefaultObservationLimit, 1, chunkSettings.MaxObservationLimit);
var minimumLength = ResolveBoundedInt(context.Request.Query["minLength"], chunkSettings.DefaultMinimumLength, 16, chunkSettings.MaxMinimumLength);
var sectionFilter = BuildFilterSet(context.Request.Query["section"]);
var formatFilter = BuildFilterSet(context.Request.Query["format"]);
var queryOptions = new AdvisoryObservationQueryOptions(
tenant,
aliases: new[] { normalizedKey },
limit: observationLimit);
var observationResult = await observationService.QueryAsync(queryOptions, cancellationToken).ConfigureAwait(false);
if (observationResult.Observations.IsDefaultOrEmpty || observationResult.Observations.Length == 0)
{
return Problem(context, "Advisory not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, $"No observations available for {normalizedKey}.");
}
var buildOptions = new AdvisoryChunkBuildOptions(
normalizedKey,
chunkLimit,
observationLimit,
sectionFilter,
formatFilter,
minimumLength);
var response = chunkBuilder.Build(buildOptions, observationResult.Observations.ToArray());
return JsonResult(response);
});
if (authorityConfigured)
{
advisoryChunksEndpoint.RequireAuthorization(AdvisoryReadPolicyName);
}
var aocVerifyEndpoint = app.MapPost("/aoc/verify", async (
HttpContext context,
AocVerifyRequest request,
@@ -932,12 +1032,6 @@ if (authorityConfigured)
});
}
if (authorityConfigured)
{
app.UseAuthentication();
app.UseAuthorization();
}
IResult JsonResult<T>(T value, int? statusCode = null)
{
var payload = JsonSerializer.Serialize(value, jsonOptions);
@@ -1049,6 +1143,53 @@ IResult? EnsureTenantAuthorized(HttpContext context, string tenant)
return null;
}
ImmutableHashSet<string> BuildFilterSet(StringValues values)
{
if (values.Count == 0)
{
return ImmutableHashSet<string>.Empty;
}
var builder = ImmutableHashSet.CreateBuilder<string>(StringComparer.OrdinalIgnoreCase);
foreach (var value in values)
{
if (string.IsNullOrWhiteSpace(value))
{
continue;
}
var segments = value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments.Length == 0)
{
builder.Add(value.Trim());
continue;
}
foreach (var segment in segments)
{
if (!string.IsNullOrWhiteSpace(segment))
{
builder.Add(segment.Trim());
}
}
}
return builder.ToImmutable();
}
int ResolveBoundedInt(StringValues values, int fallback, int minValue, int maxValue)
{
foreach (var value in values)
{
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
{
return Math.Clamp(parsed, minValue, maxValue);
}
}
return Math.Clamp(fallback, minValue, maxValue);
}
static DateTimeOffset? ParseDateTime(string? value)
{
if (string.IsNullOrWhiteSpace(value))
@@ -1474,3 +1615,4 @@ static async Task InitializeMongoAsync(WebApplication app)
}
public partial class Program;