feat(api): Implement Console Export Client and Models
Some checks failed
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
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled

- Added ConsoleExportClient for managing export requests and responses.
- Introduced ConsoleExportRequest and ConsoleExportResponse models.
- Implemented methods for creating and retrieving exports with appropriate headers.

feat(crypto): Add Software SM2/SM3 Cryptography Provider

- Implemented SmSoftCryptoProvider for software-only SM2/SM3 cryptography.
- Added support for signing and verification using SM2 algorithm.
- Included hashing functionality with SM3 algorithm.
- Configured options for loading keys from files and environment gate checks.

test(crypto): Add unit tests for SmSoftCryptoProvider

- Created comprehensive tests for signing, verifying, and hashing functionalities.
- Ensured correct behavior for key management and error handling.

feat(api): Enhance Console Export Models

- Expanded ConsoleExport models to include detailed status and event types.
- Added support for various export formats and notification options.

test(time): Implement TimeAnchorPolicyService tests

- Developed tests for TimeAnchorPolicyService to validate time anchors.
- Covered scenarios for anchor validation, drift calculation, and policy enforcement.
This commit is contained in:
StellaOps Bot
2025-12-07 00:27:33 +02:00
parent 9bd6a73926
commit 0de92144d2
229 changed files with 32351 additions and 1481 deletions

View File

@@ -62,8 +62,9 @@ using StellaOps.Concelier.Storage.Mongo.Aliases;
using StellaOps.Concelier.Storage.Postgres;
using StellaOps.Provenance.Mongo;
using StellaOps.Concelier.Core.Attestation;
using StellaOps.Concelier.Core.Signals;
using AttestationClaims = StellaOps.Concelier.Core.Attestation.AttestationClaims;
using StellaOps.Concelier.Storage.Mongo.Orchestrator;
using StellaOps.Concelier.Core.Orchestration;
using System.Diagnostics.Metrics;
using StellaOps.Concelier.Models.Observations;
@@ -261,6 +262,12 @@ builder.Services.AddSingleton<IAdvisoryChunkCache, AdvisoryChunkCache>();
builder.Services.AddSingleton<IAdvisoryAiTelemetry, AdvisoryAiTelemetry>();
builder.Services.AddSingleton<EvidenceBundleAttestationBuilder>();
// Register signals services (CONCELIER-SIG-26-001)
builder.Services.AddConcelierSignalsServices();
// Register orchestration services (CONCELIER-ORCH-32-001)
builder.Services.AddConcelierOrchestrationServices();
var features = concelierOptions.Features ?? new ConcelierOptions.FeaturesOptions();
if (!features.NoMergeEnabled)
@@ -3698,6 +3705,220 @@ var concelierTimelineEndpoint = app.MapGet("/obs/concelier/timeline", async (
return Results.Empty;
});
// ==========================================
// Signals Endpoints (CONCELIER-SIG-26-001)
// Expose affected symbol/function lists for reachability scoring
// ==========================================
app.MapGet("/v1/signals/symbols", async (
HttpContext context,
[FromQuery(Name = "advisoryId")] string? advisoryId,
[FromQuery(Name = "purl")] string? purl,
[FromQuery(Name = "symbolType")] string? symbolType,
[FromQuery(Name = "source")] string? source,
[FromQuery(Name = "withLocation")] bool? withLocation,
[FromQuery(Name = "limit")] int? limit,
[FromQuery(Name = "offset")] int? offset,
[FromServices] IAffectedSymbolProvider symbolProvider,
CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
{
return tenantError;
}
var authorizationError = EnsureTenantAuthorized(context, tenant);
if (authorizationError is not null)
{
return authorizationError;
}
// Parse symbol types if provided
ImmutableArray<AffectedSymbolType>? symbolTypes = null;
if (!string.IsNullOrWhiteSpace(symbolType))
{
var types = symbolType.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var parsed = new List<AffectedSymbolType>();
foreach (var t in types)
{
if (Enum.TryParse<AffectedSymbolType>(t, ignoreCase: true, out var parsedType))
{
parsed.Add(parsedType);
}
}
if (parsed.Count > 0)
{
symbolTypes = parsed.ToImmutableArray();
}
}
// Parse sources if provided
ImmutableArray<string>? sources = null;
if (!string.IsNullOrWhiteSpace(source))
{
sources = source.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.ToImmutableArray();
}
var options = new AffectedSymbolQueryOptions(
TenantId: tenant!,
AdvisoryId: advisoryId?.Trim(),
Purl: purl?.Trim(),
SymbolTypes: symbolTypes,
Sources: sources,
WithLocationOnly: withLocation,
Limit: Math.Clamp(limit ?? 100, 1, 500),
Offset: Math.Max(offset ?? 0, 0));
var result = await symbolProvider.QueryAsync(options, cancellationToken);
return Results.Ok(new SignalsSymbolQueryResponse(
Symbols: result.Symbols.Select(s => ToSymbolResponse(s)).ToList(),
TotalCount: result.TotalCount,
HasMore: result.HasMore,
ComputedAt: result.ComputedAt.ToString("O", CultureInfo.InvariantCulture)));
}).WithName("QueryAffectedSymbols");
app.MapGet("/v1/signals/symbols/advisory/{advisoryId}", async (
HttpContext context,
string advisoryId,
[FromServices] IAffectedSymbolProvider symbolProvider,
CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
{
return tenantError;
}
var authorizationError = EnsureTenantAuthorized(context, tenant);
if (authorizationError is not null)
{
return authorizationError;
}
if (string.IsNullOrWhiteSpace(advisoryId))
{
return ConcelierProblemResultFactory.AdvisoryIdRequired(context);
}
var symbolSet = await symbolProvider.GetByAdvisoryAsync(tenant!, advisoryId.Trim(), cancellationToken);
return Results.Ok(ToSymbolSetResponse(symbolSet));
}).WithName("GetAffectedSymbolsByAdvisory");
app.MapGet("/v1/signals/symbols/package/{*purl}", async (
HttpContext context,
string purl,
[FromServices] IAffectedSymbolProvider symbolProvider,
CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
{
return tenantError;
}
var authorizationError = EnsureTenantAuthorized(context, tenant);
if (authorizationError is not null)
{
return authorizationError;
}
if (string.IsNullOrWhiteSpace(purl))
{
return Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "Package URL required",
detail: "The purl parameter is required.",
type: "https://stellaops.org/problems/validation");
}
var symbolSet = await symbolProvider.GetByPackageAsync(tenant!, purl.Trim(), cancellationToken);
return Results.Ok(ToSymbolSetResponse(symbolSet));
}).WithName("GetAffectedSymbolsByPackage");
app.MapPost("/v1/signals/symbols/batch", async (
HttpContext context,
[FromBody] SignalsSymbolBatchRequest request,
[FromServices] IAffectedSymbolProvider symbolProvider,
CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
{
return tenantError;
}
var authorizationError = EnsureTenantAuthorized(context, tenant);
if (authorizationError is not null)
{
return authorizationError;
}
if (request.AdvisoryIds is not { Count: > 0 })
{
return Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "Advisory IDs required",
detail: "At least one advisoryId is required in the batch request.",
type: "https://stellaops.org/problems/validation");
}
if (request.AdvisoryIds.Count > 100)
{
return Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "Batch size exceeded",
detail: "Maximum batch size is 100 advisory IDs.",
type: "https://stellaops.org/problems/validation");
}
var results = await symbolProvider.GetByAdvisoriesBatchAsync(tenant!, request.AdvisoryIds, cancellationToken);
var response = new SignalsSymbolBatchResponse(
Results: results.ToDictionary(
kvp => kvp.Key,
kvp => ToSymbolSetResponse(kvp.Value)));
return Results.Ok(response);
}).WithName("GetAffectedSymbolsBatch");
app.MapGet("/v1/signals/symbols/exists/{advisoryId}", async (
HttpContext context,
string advisoryId,
[FromServices] IAffectedSymbolProvider symbolProvider,
CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
{
return tenantError;
}
var authorizationError = EnsureTenantAuthorized(context, tenant);
if (authorizationError is not null)
{
return authorizationError;
}
if (string.IsNullOrWhiteSpace(advisoryId))
{
return ConcelierProblemResultFactory.AdvisoryIdRequired(context);
}
var exists = await symbolProvider.HasSymbolsAsync(tenant!, advisoryId.Trim(), cancellationToken);
return Results.Ok(new SignalsSymbolExistsResponse(Exists: exists, AdvisoryId: advisoryId.Trim()));
}).WithName("CheckAffectedSymbolsExist");
await app.RunAsync();
}
@@ -3718,6 +3939,112 @@ private readonly record struct LinksetObservationSummary(
public static LinksetObservationSummary Empty { get; } = new(null, null, null, null);
}
// ==========================================
// Signals API Response Types (CONCELIER-SIG-26-001)
// ==========================================
record SignalsSymbolQueryResponse(
List<SignalsSymbolResponse> Symbols,
int TotalCount,
bool HasMore,
string ComputedAt);
record SignalsSymbolResponse(
string AdvisoryId,
string ObservationId,
string Symbol,
string SymbolType,
string? Purl,
string? Module,
string? ClassName,
string? FilePath,
int? LineNumber,
string? VersionRange,
string CanonicalId,
bool HasSourceLocation,
SignalsSymbolProvenanceResponse Provenance);
record SignalsSymbolProvenanceResponse(
string Source,
string Vendor,
string ObservationHash,
string FetchedAt,
string? IngestJobId,
string? UpstreamId,
string? UpstreamUrl);
record SignalsSymbolSetResponse(
string TenantId,
string AdvisoryId,
List<SignalsSymbolResponse> Symbols,
List<SignalsSymbolSourceSummaryResponse> SourceSummaries,
int UniqueSymbolCount,
bool HasSourceLocations,
string ComputedAt);
record SignalsSymbolSourceSummaryResponse(
string Source,
int SymbolCount,
int WithLocationCount,
Dictionary<string, int> CountByType,
string LatestFetchAt);
record SignalsSymbolBatchRequest(
List<string> AdvisoryIds);
record SignalsSymbolBatchResponse(
Dictionary<string, SignalsSymbolSetResponse> Results);
record SignalsSymbolExistsResponse(
bool Exists,
string AdvisoryId);
// ==========================================
// Signals API Helper Methods
// ==========================================
static SignalsSymbolResponse ToSymbolResponse(AffectedSymbol symbol)
{
return new SignalsSymbolResponse(
AdvisoryId: symbol.AdvisoryId,
ObservationId: symbol.ObservationId,
Symbol: symbol.Symbol,
SymbolType: symbol.SymbolType.ToString(),
Purl: symbol.Purl,
Module: symbol.Module,
ClassName: symbol.ClassName,
FilePath: symbol.FilePath,
LineNumber: symbol.LineNumber,
VersionRange: symbol.VersionRange,
CanonicalId: symbol.CanonicalId,
HasSourceLocation: symbol.HasSourceLocation,
Provenance: new SignalsSymbolProvenanceResponse(
Source: symbol.Provenance.Source,
Vendor: symbol.Provenance.Vendor,
ObservationHash: symbol.Provenance.ObservationHash,
FetchedAt: symbol.Provenance.FetchedAt.ToString("O", CultureInfo.InvariantCulture),
IngestJobId: symbol.Provenance.IngestJobId,
UpstreamId: symbol.Provenance.UpstreamId,
UpstreamUrl: symbol.Provenance.UpstreamUrl));
}
static SignalsSymbolSetResponse ToSymbolSetResponse(AffectedSymbolSet symbolSet)
{
return new SignalsSymbolSetResponse(
TenantId: symbolSet.TenantId,
AdvisoryId: symbolSet.AdvisoryId,
Symbols: symbolSet.Symbols.Select(ToSymbolResponse).ToList(),
SourceSummaries: symbolSet.SourceSummaries.Select(s => new SignalsSymbolSourceSummaryResponse(
Source: s.Source,
SymbolCount: s.SymbolCount,
WithLocationCount: s.WithLocationCount,
CountByType: s.CountByType.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value),
LatestFetchAt: s.LatestFetchAt.ToString("O", CultureInfo.InvariantCulture))).ToList(),
UniqueSymbolCount: symbolSet.UniqueSymbolCount,
HasSourceLocations: symbolSet.HasSourceLocations,
ComputedAt: symbolSet.ComputedAt.ToString("O", CultureInfo.InvariantCulture));
}
static PluginHostOptions BuildPluginOptions(ConcelierOptions options, string contentRoot)
{
var pluginOptions = new PluginHostOptions