Files
git.stella-ops.org/src/Platform/StellaOps.Platform.WebService/Endpoints/AocCompatibilityEndpoints.cs
2026-03-10 01:37:24 +02:00

477 lines
19 KiB
C#

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Auth.ServerIntegration.Tenancy;
using System.Globalization;
namespace StellaOps.Platform.WebService.Endpoints;
public static class AocCompatibilityEndpoints
{
public static IEndpointRouteBuilder MapAocCompatibilityEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/aoc")
.WithTags("AOC Compatibility")
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.AocVerify))
.RequireTenant();
group.MapGet("/metrics", (
HttpContext httpContext,
[FromQuery] string? tenantId,
[FromQuery] int? windowMinutes,
TimeProvider timeProvider) =>
{
var tenant = ResolveTenant(httpContext, tenantId);
if (string.IsNullOrWhiteSpace(tenant))
{
return Results.BadRequest(new { error = "tenant_required" });
}
var effectiveWindowMinutes = windowMinutes is > 0 ? windowMinutes.Value : 1440;
var now = timeProvider.GetUtcNow();
return Results.Ok(new
{
passCount = 12847,
failCount = 23,
totalCount = 12870,
passRate = 0.9982,
recentViolations = BuildViolationSummaries(now),
ingestThroughput = new
{
docsPerMinute = 8.9,
avgLatencyMs = 145,
p95LatencyMs = 312,
queueDepth = 3,
errorRate = 0.18
},
timeWindow = new
{
start = now.AddMinutes(-effectiveWindowMinutes).ToString("O", CultureInfo.InvariantCulture),
end = now.ToString("O", CultureInfo.InvariantCulture),
durationMinutes = effectiveWindowMinutes
}
});
})
.WithName("AocCompatibility.GetMetrics");
group.MapPost("/verify", (
HttpContext httpContext,
AocVerifyRequest request,
TimeProvider timeProvider) =>
{
var tenant = ResolveTenant(httpContext, request.TenantId);
if (string.IsNullOrWhiteSpace(tenant))
{
return Results.BadRequest(new { error = "tenant_required" });
}
var now = timeProvider.GetUtcNow();
return Results.Ok(new
{
verificationId = $"verify-{tenant}-{now:yyyyMMddHHmmss}",
status = "partial",
checkedCount = Math.Clamp(request.Limit ?? 250, 1, 1000),
passedCount = 247,
failedCount = 3,
violations = new[]
{
new
{
documentId = "sbom-nginx-prod",
violationCode = "AOC-PROV-001",
field = "provenance.digest",
expected = "signed",
actual = "missing",
provenance = new
{
sourceId = "docker-hub",
ingestedAt = now.AddMinutes(-20).ToString("O", CultureInfo.InvariantCulture),
digest = "sha256:4fdb5e6a31a80f0d",
sourceType = "registry",
sourceUrl = "docker.io/library/nginx:1.27.4",
submitter = "scanner-agent-01"
}
}
},
completedAt = now.ToString("O", CultureInfo.InvariantCulture)
});
})
.WithName("AocCompatibility.Verify");
group.MapGet("/compliance/dashboard", (
HttpContext httpContext,
[FromQuery] string? tenantId,
TimeProvider timeProvider) =>
{
var tenant = ResolveTenant(httpContext, tenantId);
if (string.IsNullOrWhiteSpace(tenant))
{
return Results.BadRequest(new { error = "tenant_required" });
}
var now = timeProvider.GetUtcNow();
return Results.Ok(new
{
metrics = new
{
guardViolations = new
{
count = 23,
percentage = 0.18,
byReason = new Dictionary<string, int>(StringComparer.Ordinal)
{
["schema_invalid"] = 9,
["missing_required_fields"] = 8,
["hash_mismatch"] = 6
},
trend = "stable"
},
provenanceCompleteness = new
{
percentage = 99.1,
recordsWithValidHash = 12755,
totalRecords = 12870,
trend = "up"
},
deduplicationRate = new
{
percentage = 12.4,
duplicatesDetected = 1595,
totalIngested = 12870,
trend = "stable"
},
ingestionLatency = new
{
p50Ms = 122,
p95Ms = 301,
p99Ms = 410,
meetsSla = true,
slaTargetP95Ms = 500
},
supersedesDepth = new
{
maxDepth = 4,
avgDepth = 1.6,
distribution = new[]
{
new { depth = 1, count = 1180 },
new { depth = 2, count = 242 },
new { depth = 3, count = 44 },
new { depth = 4, count = 6 }
}
},
periodStart = now.AddDays(-7).ToString("O", CultureInfo.InvariantCulture),
periodEnd = now.ToString("O", CultureInfo.InvariantCulture)
},
recentViolations = BuildGuardViolations(now, page: 1, pageSize: 5).Items,
ingestionFlow = BuildIngestionFlow(now)
});
})
.WithName("AocCompatibility.GetComplianceDashboard");
group.MapGet("/compliance/violations", (
[FromQuery] int? page,
[FromQuery] int? pageSize,
TimeProvider timeProvider) =>
{
var effectivePage = page is > 0 ? page.Value : 1;
var effectivePageSize = pageSize is > 0 ? pageSize.Value : 20;
var response = BuildGuardViolations(timeProvider.GetUtcNow(), effectivePage, effectivePageSize);
return Results.Ok(response);
})
.WithName("AocCompatibility.GetViolations");
group.MapPost("/compliance/violations/{violationId}/retry", (string violationId) =>
Results.Ok(new
{
success = true,
message = $"Retry scheduled for {violationId}."
}))
.WithName("AocCompatibility.RetryViolation");
group.MapGet("/ingestion/flow", ([FromServices] TimeProvider timeProvider) =>
Results.Ok(BuildIngestionFlow(timeProvider.GetUtcNow())))
.WithName("AocCompatibility.GetIngestionFlow");
group.MapPost("/provenance/validate", (
AocProvenanceValidateRequest request,
TimeProvider timeProvider) =>
{
var now = timeProvider.GetUtcNow();
var inputType = string.IsNullOrWhiteSpace(request.InputType) ? "finding_id" : request.InputType!;
var inputValue = string.IsNullOrWhiteSpace(request.InputValue) ? "finding-001" : request.InputValue!;
return Results.Ok(new
{
inputType,
inputValue,
steps = new[]
{
new AocProvenanceStep(
"source",
"Registry intake",
now.AddMinutes(-40).ToString("O", CultureInfo.InvariantCulture),
"sha256:1f7d98a2bf54c390",
null,
"valid",
new Dictionary<string, object?>(StringComparer.Ordinal)
{
["source"] = "docker-hub",
["artifact"] = "nginx:1.27.4"
}),
new AocProvenanceStep(
"normalized",
"Concelier normalization",
now.AddMinutes(-33).ToString("O", CultureInfo.InvariantCulture),
"sha256:4fdb5e6a31a80f0d",
"sha256:1f7d98a2bf54c390",
"valid",
new Dictionary<string, object?>(StringComparer.Ordinal)
{
["pipeline"] = "concelier",
["tenant"] = "demo-prod"
}),
new AocProvenanceStep(
"finding",
"Finding materialized",
now.AddMinutes(-22).ToString("O", CultureInfo.InvariantCulture),
"sha256:0a52b69d9f9c72c0",
"sha256:4fdb5e6a31a80f0d",
"valid",
new Dictionary<string, object?>(StringComparer.Ordinal)
{
["findingId"] = inputValue,
["decision"] = "warn"
})
},
isComplete = true,
validationErrors = Array.Empty<string>(),
validatedAt = now.ToString("O", CultureInfo.InvariantCulture)
});
})
.WithName("AocCompatibility.ValidateProvenance");
group.MapPost("/compliance/reports", (
AocComplianceReportRequest request,
TimeProvider timeProvider) =>
{
var now = timeProvider.GetUtcNow();
return Results.Ok(new
{
reportId = $"aoc-report-{now:yyyyMMddHHmmss}",
generatedAt = now.ToString("O", CultureInfo.InvariantCulture),
period = new
{
start = request.StartDate ?? now.AddDays(-7).ToString("O", CultureInfo.InvariantCulture),
end = request.EndDate ?? now.ToString("O", CultureInfo.InvariantCulture)
},
guardViolationSummary = new
{
total = 23,
bySource = new Dictionary<string, int>(StringComparer.Ordinal)
{
["docker-hub"] = 12,
["github-packages"] = 11
},
byReason = new Dictionary<string, int>(StringComparer.Ordinal)
{
["schema_invalid"] = 9,
["missing_required_fields"] = 8,
["hash_mismatch"] = 6
}
},
provenanceCompliance = new
{
percentage = 99.1,
bySource = new Dictionary<string, double>(StringComparer.Ordinal)
{
["docker-hub"] = 99.5,
["github-packages"] = 98.7
}
},
deduplicationMetrics = new
{
rate = 12.4,
bySource = new Dictionary<string, double>(StringComparer.Ordinal)
{
["docker-hub"] = 10.8,
["github-packages"] = 14.1
}
},
latencyMetrics = new
{
p50Ms = 122,
p95Ms = 301,
p99Ms = 410,
bySource = new Dictionary<string, object>(StringComparer.Ordinal)
{
["docker-hub"] = new { p50 = 118, p95 = 292, p99 = 401 },
["github-packages"] = new { p50 = 126, p95 = 312, p99 = 420 }
}
}
});
})
.WithName("AocCompatibility.GenerateComplianceReport");
return app;
}
private static object[] BuildViolationSummaries(DateTimeOffset now) =>
[
new
{
code = "AOC-PROV-001",
description = "Missing provenance attestation",
count = 12,
severity = "high",
lastSeen = now.AddMinutes(-15).ToString("O", CultureInfo.InvariantCulture)
},
new
{
code = "AOC-DIGEST-002",
description = "Digest mismatch in manifest",
count = 7,
severity = "critical",
lastSeen = now.AddMinutes(-42).ToString("O", CultureInfo.InvariantCulture)
},
new
{
code = "AOC-SCHEMA-003",
description = "Schema validation failed",
count = 4,
severity = "medium",
lastSeen = now.AddHours(-2).ToString("O", CultureInfo.InvariantCulture)
}
];
private static object BuildIngestionFlow(DateTimeOffset now) => new
{
sources = new[]
{
new
{
sourceId = "docker-hub",
sourceName = "Docker Hub",
module = "concelier",
throughputPerMinute = 5.2,
latencyP50Ms = 112,
latencyP95Ms = 284,
latencyP99Ms = 365,
errorRate = 0.12,
backlogDepth = 2,
lastIngestionAt = now.AddMinutes(-3).ToString("O", CultureInfo.InvariantCulture),
status = "healthy"
},
new
{
sourceId = "github-packages",
sourceName = "GitHub Packages",
module = "excititor",
throughputPerMinute = 3.7,
latencyP50Ms = 133,
latencyP95Ms = 318,
latencyP99Ms = 411,
errorRate = 0.24,
backlogDepth = 1,
lastIngestionAt = now.AddMinutes(-6).ToString("O", CultureInfo.InvariantCulture),
status = "degraded"
}
},
totalThroughput = 8.9,
avgLatencyP95Ms = 301,
overallErrorRate = 0.18,
lastUpdatedAt = now.ToString("O", CultureInfo.InvariantCulture)
};
private static AocGuardViolationResponse BuildGuardViolations(DateTimeOffset now, int page, int pageSize)
{
var all = new[]
{
new AocGuardViolation(
"viol-001",
now.AddMinutes(-16).ToString("O", CultureInfo.InvariantCulture),
"docker-hub",
"missing_required_fields",
"Provenance digest missing from normalized advisory.",
"{\"digest\":null}",
"concelier",
true),
new AocGuardViolation(
"viol-002",
now.AddMinutes(-44).ToString("O", CultureInfo.InvariantCulture),
"github-packages",
"hash_mismatch",
"Manifest digest did not match DSSE payload.",
"{\"expected\":\"sha256:a\",\"actual\":\"sha256:b\"}",
"excititor",
true),
new AocGuardViolation(
"viol-003",
now.AddHours(-2).ToString("O", CultureInfo.InvariantCulture),
"docker-hub",
"schema_invalid",
"Document failed schema validation for SPDX 2.3.",
"{\"schema\":\"spdx-2.3\"}",
"concelier",
false)
};
var effectivePage = page > 0 ? page : 1;
var effectivePageSize = pageSize > 0 ? pageSize : 20;
var skip = (effectivePage - 1) * effectivePageSize;
var items = all.Skip(skip).Take(effectivePageSize).ToArray();
return new AocGuardViolationResponse(
items,
all.Length,
effectivePage,
effectivePageSize,
skip + items.Length < all.Length);
}
private static string? ResolveTenant(HttpContext httpContext, string? tenantId)
=> tenantId?.Trim()
?? httpContext.Request.Headers["X-StellaOps-Tenant"].FirstOrDefault()
?? httpContext.Request.Headers["X-Tenant-Id"].FirstOrDefault()
?? httpContext.User.Claims.FirstOrDefault(static claim =>
claim.Type is "stellaops:tenant" or "tenant_id")?.Value;
private sealed record AocVerifyRequest(string? TenantId, string? Since, int? Limit);
private sealed record AocProvenanceValidateRequest(string? InputType, string? InputValue);
private sealed record AocComplianceReportRequest(
string? StartDate,
string? EndDate,
IReadOnlyList<string>? Sources,
string? Format,
bool IncludeViolationDetails);
private sealed record AocGuardViolation(
string Id,
string Timestamp,
string Source,
string Reason,
string Message,
string PayloadSample,
string Module,
bool CanRetry);
private sealed record AocGuardViolationResponse(
IReadOnlyList<AocGuardViolation> Items,
int TotalCount,
int Page,
int PageSize,
bool HasMore);
private sealed record AocProvenanceStep(
string StepType,
string Label,
string Timestamp,
string Hash,
string? LinkedFromHash,
string Status,
IReadOnlyDictionary<string, object?> Details);
}