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
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user