Restore live platform compatibility contracts
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user