up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
cryptopro-linux-csp / build-and-test (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-09 09:38:09 +02:00
parent bc0762e97d
commit 108d1c64b3
193 changed files with 7265 additions and 13029 deletions

View File

@@ -1,40 +1,23 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Storage;
using StellaOps.Excititor.WebService.Contracts;
using StellaOps.Excititor.WebService.Services;
namespace StellaOps.Excititor.WebService.Endpoints;
/// <summary>
/// Attestation API endpoints (WEB-OBS-54-001).
/// Exposes /attestations/vex/* endpoints returning DSSE verification state,
/// builder identity, and chain-of-custody links.
/// Attestation API endpoints (temporarily disabled while Mongo is removed and Postgres storage is adopted).
/// </summary>
public static class AttestationEndpoints
{
public static void MapAttestationEndpoints(this WebApplication app)
{
// GET /attestations/vex/list - List attestations
app.MapGet("/attestations/vex/list", async (
// GET /attestations/vex/list
app.MapGet("/attestations/vex/list", (
HttpContext context,
IOptions<VexStorageOptions> storageOptions,
[FromServices] IMongoDatabase database,
TimeProvider timeProvider,
[FromQuery] int? limit,
[FromQuery] string? cursor,
[FromQuery] string? vulnerabilityId,
[FromQuery] string? productKey,
CancellationToken cancellationToken) =>
IOptions<VexStorageOptions> storageOptions) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
if (scopeResult is not null)
@@ -42,70 +25,22 @@ public static class AttestationEndpoints
return scopeResult;
}
if (!TryResolveTenant(context, storageOptions.Value, out var tenant, out var tenantError))
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out _, out var tenantError))
{
return tenantError;
}
var take = Math.Clamp(limit.GetValueOrDefault(50), 1, 200);
var collection = database.GetCollection<BsonDocument>(VexMongoCollectionNames.Attestations);
var builder = Builders<BsonDocument>.Filter;
var filters = new List<FilterDefinition<BsonDocument>>();
if (!string.IsNullOrWhiteSpace(vulnerabilityId))
{
filters.Add(builder.Eq("VulnerabilityId", vulnerabilityId.Trim().ToUpperInvariant()));
}
if (!string.IsNullOrWhiteSpace(productKey))
{
filters.Add(builder.Eq("ProductKey", productKey.Trim().ToLowerInvariant()));
}
// Parse cursor if provided
if (!string.IsNullOrWhiteSpace(cursor) && TryDecodeCursor(cursor, out var cursorTime, out var cursorId))
{
var ltTime = builder.Lt("IssuedAt", cursorTime);
var eqTimeLtId = builder.And(
builder.Eq("IssuedAt", cursorTime),
builder.Lt("_id", cursorId));
filters.Add(builder.Or(ltTime, eqTimeLtId));
}
var filter = filters.Count == 0 ? builder.Empty : builder.And(filters);
var sort = Builders<BsonDocument>.Sort.Descending("IssuedAt").Descending("_id");
var documents = await collection
.Find(filter)
.Sort(sort)
.Limit(take)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var items = documents.Select(doc => ToListItem(doc, tenant, timeProvider)).ToList();
string? nextCursor = null;
var hasMore = documents.Count == take;
if (hasMore && documents.Count > 0)
{
var last = documents[^1];
var lastTime = last.GetValue("IssuedAt", BsonNull.Value).ToUniversalTime();
var lastId = last.GetValue("_id", BsonNull.Value).AsString;
nextCursor = EncodeCursor(lastTime, lastId);
}
var response = new VexAttestationListResponse(items, nextCursor, hasMore, items.Count);
return Results.Ok(response);
return Results.Problem(
detail: "Attestation listing is temporarily unavailable during Postgres migration (Mongo/BSON removed).",
statusCode: StatusCodes.Status503ServiceUnavailable,
title: "Service unavailable");
}).WithName("ListVexAttestations");
// GET /attestations/vex/{attestationId} - Get attestation details
app.MapGet("/attestations/vex/{attestationId}", async (
// GET /attestations/vex/{attestationId}
app.MapGet("/attestations/vex/{attestationId}", (
HttpContext context,
string attestationId,
IOptions<VexStorageOptions> storageOptions,
[FromServices] IVexAttestationLinkStore attestationStore,
TimeProvider timeProvider,
CancellationToken cancellationToken) =>
IOptions<VexStorageOptions> storageOptions) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
if (scopeResult is not null)
@@ -113,235 +48,23 @@ public static class AttestationEndpoints
return scopeResult;
}
if (!TryResolveTenant(context, storageOptions.Value, out var tenant, out var tenantError))
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out _, out var tenantError))
{
return tenantError;
}
if (string.IsNullOrWhiteSpace(attestationId))
{
return Results.BadRequest(new { error = new { code = "ERR_ATTESTATION_ID", message = "attestationId is required" } });
return Results.Problem(
detail: "attestationId is required.",
statusCode: StatusCodes.Status400BadRequest,
title: "Validation error");
}
var attestation = await attestationStore.FindAsync(attestationId.Trim(), cancellationToken).ConfigureAwait(false);
if (attestation is null)
{
return Results.NotFound(new { error = new { code = "ERR_NOT_FOUND", message = $"Attestation '{attestationId}' not found" } });
}
// Build subject from observation context
var subjectDigest = attestation.Metadata.TryGetValue("digest", out var dig) ? dig : attestation.ObservationId;
var subject = new VexAttestationSubject(
Digest: subjectDigest,
DigestAlgorithm: "sha256",
Name: $"{attestation.VulnerabilityId}/{attestation.ProductKey}",
Uri: null);
var builder = new VexAttestationBuilderIdentity(
Id: attestation.SupplierId,
Version: null,
BuilderId: attestation.SupplierId,
InvocationId: attestation.ObservationId);
// Get verification state from metadata
var isValid = attestation.Metadata.TryGetValue("verified", out var verified) && verified == "true";
DateTimeOffset? verifiedAt = null;
if (attestation.Metadata.TryGetValue("verifiedAt", out var verifiedAtStr) &&
DateTimeOffset.TryParse(verifiedAtStr, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsedVerifiedAt))
{
verifiedAt = parsedVerifiedAt;
}
var verification = new VexAttestationVerificationState(
Valid: isValid,
VerifiedAt: verifiedAt,
SignatureType: attestation.Metadata.GetValueOrDefault("signatureType", "dsse"),
KeyId: attestation.Metadata.GetValueOrDefault("keyId"),
Issuer: attestation.Metadata.GetValueOrDefault("issuer"),
EnvelopeDigest: attestation.Metadata.GetValueOrDefault("envelopeDigest"),
Diagnostics: attestation.Metadata);
var custodyLinks = new List<VexAttestationCustodyLink>
{
new(
Step: 1,
Actor: attestation.SupplierId,
Action: "created",
Timestamp: attestation.IssuedAt,
Reference: attestation.AttestationId)
};
// Add linkset link
custodyLinks.Add(new VexAttestationCustodyLink(
Step: 2,
Actor: "excititor",
Action: "linked_to_observation",
Timestamp: attestation.IssuedAt,
Reference: attestation.LinksetId));
var metadata = new Dictionary<string, string>(StringComparer.Ordinal)
{
["observationId"] = attestation.ObservationId,
["linksetId"] = attestation.LinksetId,
["vulnerabilityId"] = attestation.VulnerabilityId,
["productKey"] = attestation.ProductKey
};
if (!string.IsNullOrWhiteSpace(attestation.JustificationSummary))
{
metadata["justificationSummary"] = attestation.JustificationSummary;
}
var response = new VexAttestationDetailResponse(
AttestationId: attestation.AttestationId,
Tenant: tenant,
CreatedAt: attestation.IssuedAt,
PredicateType: attestation.Metadata.GetValueOrDefault("predicateType", "https://in-toto.io/attestation/v1"),
Subject: subject,
Builder: builder,
Verification: verification,
ChainOfCustody: custodyLinks,
Metadata: metadata);
return Results.Ok(response);
return Results.Problem(
detail: "Attestation retrieval is temporarily unavailable during Postgres migration (Mongo/BSON removed).",
statusCode: StatusCodes.Status503ServiceUnavailable,
title: "Service unavailable");
}).WithName("GetVexAttestation");
// GET /attestations/vex/lookup - Lookup attestations by linkset or observation
app.MapGet("/attestations/vex/lookup", async (
HttpContext context,
IOptions<VexStorageOptions> storageOptions,
[FromServices] IMongoDatabase database,
TimeProvider timeProvider,
[FromQuery] string? linksetId,
[FromQuery] string? observationId,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
if (scopeResult is not null)
{
return scopeResult;
}
if (!TryResolveTenant(context, storageOptions.Value, out var tenant, out var tenantError))
{
return tenantError;
}
if (string.IsNullOrWhiteSpace(linksetId) && string.IsNullOrWhiteSpace(observationId))
{
return Results.BadRequest(new { error = new { code = "ERR_PARAMS", message = "Either linksetId or observationId is required" } });
}
var take = Math.Clamp(limit.GetValueOrDefault(50), 1, 100);
var collection = database.GetCollection<BsonDocument>(VexMongoCollectionNames.Attestations);
var builder = Builders<BsonDocument>.Filter;
FilterDefinition<BsonDocument> filter;
if (!string.IsNullOrWhiteSpace(linksetId))
{
filter = builder.Eq("LinksetId", linksetId.Trim());
}
else
{
filter = builder.Eq("ObservationId", observationId!.Trim());
}
var sort = Builders<BsonDocument>.Sort.Descending("IssuedAt");
var documents = await collection
.Find(filter)
.Sort(sort)
.Limit(take)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var items = documents.Select(doc => ToListItem(doc, tenant, timeProvider)).ToList();
var response = new VexAttestationLookupResponse(
SubjectDigest: linksetId ?? observationId ?? string.Empty,
Attestations: items,
QueriedAt: timeProvider.GetUtcNow());
return Results.Ok(response);
}).WithName("LookupVexAttestations");
}
private static VexAttestationListItem ToListItem(BsonDocument doc, string tenant, TimeProvider timeProvider)
{
return new VexAttestationListItem(
AttestationId: doc.GetValue("_id", BsonNull.Value).AsString ?? string.Empty,
Tenant: tenant,
CreatedAt: doc.GetValue("IssuedAt", BsonNull.Value).IsBsonDateTime
? new DateTimeOffset(doc["IssuedAt"].ToUniversalTime(), TimeSpan.Zero)
: timeProvider.GetUtcNow(),
PredicateType: "https://in-toto.io/attestation/v1",
SubjectDigest: doc.GetValue("ObservationId", BsonNull.Value).AsString ?? string.Empty,
Valid: doc.Contains("Metadata") && !doc["Metadata"].IsBsonNull &&
doc["Metadata"].AsBsonDocument.Contains("verified") &&
doc["Metadata"]["verified"].AsString == "true",
BuilderId: doc.GetValue("SupplierId", BsonNull.Value).AsString);
}
private static bool TryResolveTenant(HttpContext context, VexStorageOptions options, out string tenant, out IResult? problem)
{
tenant = options.DefaultTenant;
problem = null;
if (context.Request.Headers.TryGetValue("X-Stella-Tenant", out var headerValues) && headerValues.Count > 0)
{
var requestedTenant = headerValues[0]?.Trim();
if (string.IsNullOrEmpty(requestedTenant))
{
problem = Results.BadRequest(new { error = new { code = "ERR_TENANT", message = "X-Stella-Tenant header must not be empty" } });
return false;
}
if (!string.Equals(requestedTenant, options.DefaultTenant, StringComparison.OrdinalIgnoreCase))
{
problem = Results.Json(
new { error = new { code = "ERR_TENANT_FORBIDDEN", message = $"Tenant '{requestedTenant}' is not allowed" } },
statusCode: StatusCodes.Status403Forbidden);
return false;
}
tenant = requestedTenant;
}
return true;
}
private static bool TryDecodeCursor(string cursor, out DateTime timestamp, out string id)
{
timestamp = default;
id = string.Empty;
try
{
var payload = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(cursor));
var parts = payload.Split('|');
if (parts.Length != 2)
{
return false;
}
if (!DateTimeOffset.TryParse(parts[0], CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed))
{
return false;
}
timestamp = parsed.UtcDateTime;
id = parts[1];
return true;
}
catch
{
return false;
}
}
private static string EncodeCursor(DateTime timestamp, string id)
{
var payload = FormattableString.Invariant($"{timestamp:O}|{id}");
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(payload));
}
}