Implement ledger metrics for observability and add tests for Ruby packages endpoints
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added `LedgerMetrics` class to record write latency and total events for ledger operations.
- Created comprehensive tests for Ruby packages endpoints, covering scenarios for missing inventory, successful retrieval, and identifier handling.
- Introduced `TestSurfaceSecretsScope` for managing environment variables during tests.
- Developed `ProvenanceMongoExtensions` for attaching DSSE provenance and trust information to event documents.
- Implemented `EventProvenanceWriter` and `EventWriter` classes for managing event provenance in MongoDB.
- Established MongoDB indexes for efficient querying of events based on provenance and trust.
- Added models and JSON parsing logic for DSSE provenance and trust information.
This commit is contained in:
master
2025-11-13 09:29:09 +02:00
parent 151f6b35cc
commit 61f963fd52
101 changed files with 5881 additions and 1776 deletions

View File

@@ -168,12 +168,16 @@ internal static class ScanEndpoints
var snapshot = await coordinator.GetAsync(parsed, context.RequestAborted).ConfigureAwait(false);
if (snapshot is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
snapshot = await TryResolveSnapshotAsync(scanId, coordinator, cancellationToken).ConfigureAwait(false);
if (snapshot is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
}
}
SurfacePointersDto? surfacePointers = null;
@@ -282,10 +286,12 @@ internal static class ScanEndpoints
private static async Task<IResult> HandleEntryTraceAsync(
string scanId,
IScanCoordinator coordinator,
IEntryTraceResultStore resultStore,
HttpContext context,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(coordinator);
ArgumentNullException.ThrowIfNull(resultStore);
if (!ScanId.TryParse(scanId, out var parsed))
@@ -298,15 +304,25 @@ internal static class ScanEndpoints
detail: "Scan identifier is required.");
}
var result = await resultStore.GetAsync(parsed.Value, cancellationToken).ConfigureAwait(false);
var targetScanId = parsed.Value;
var result = await resultStore.GetAsync(targetScanId, cancellationToken).ConfigureAwait(false);
if (result is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"EntryTrace not found",
StatusCodes.Status404NotFound,
detail: "EntryTrace data is not available for the requested scan.");
var snapshot = await TryResolveSnapshotAsync(scanId, coordinator, cancellationToken).ConfigureAwait(false);
if (snapshot is not null && !string.Equals(snapshot.ScanId.Value, targetScanId, StringComparison.Ordinal))
{
result = await resultStore.GetAsync(snapshot.ScanId.Value, cancellationToken).ConfigureAwait(false);
}
if (result is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"EntryTrace not found",
StatusCodes.Status404NotFound,
detail: "EntryTrace data is not available for the requested scan.");
}
}
var response = new EntryTraceResponse(
@@ -321,10 +337,12 @@ internal static class ScanEndpoints
private static async Task<IResult> HandleRubyPackagesAsync(
string scanId,
IScanCoordinator coordinator,
IRubyPackageInventoryStore inventoryStore,
HttpContext context,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(coordinator);
ArgumentNullException.ThrowIfNull(inventoryStore);
if (!ScanId.TryParse(scanId, out var parsed))
@@ -340,12 +358,27 @@ internal static class ScanEndpoints
var inventory = await inventoryStore.GetAsync(parsed.Value, cancellationToken).ConfigureAwait(false);
if (inventory is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Ruby packages not found",
StatusCodes.Status404NotFound,
detail: "Ruby package inventory is not available for the requested scan.");
RubyPackageInventory? fallback = null;
if (!LooksLikeScanId(scanId))
{
var snapshot = await TryResolveSnapshotAsync(scanId, coordinator, cancellationToken).ConfigureAwait(false);
if (snapshot is not null)
{
fallback = await inventoryStore.GetAsync(snapshot.ScanId.Value, cancellationToken).ConfigureAwait(false);
}
}
if (fallback is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Ruby packages not found",
StatusCodes.Status404NotFound,
detail: "Ruby package inventory is not available for the requested scan.");
}
inventory = fallback;
}
var response = new RubyPackagesResponse
@@ -420,4 +453,130 @@ internal static class ScanEndpoints
var trimmed = segment.Trim('/');
return "/" + trimmed;
}
private static async ValueTask<ScanSnapshot?> TryResolveSnapshotAsync(
string identifier,
IScanCoordinator coordinator,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(coordinator);
if (string.IsNullOrWhiteSpace(identifier))
{
return null;
}
var trimmed = identifier.Trim();
var decoded = Uri.UnescapeDataString(trimmed);
if (LooksLikeScanId(decoded))
{
return null;
}
var (reference, digest) = ExtractTargetHints(decoded);
if (reference is null && digest is null)
{
return null;
}
return await coordinator.TryFindByTargetAsync(reference, digest, cancellationToken).ConfigureAwait(false);
}
private static (string? Reference, string? Digest) ExtractTargetHints(string identifier)
{
if (string.IsNullOrWhiteSpace(identifier))
{
return (null, null);
}
var trimmed = identifier.Trim();
if (TryExtractDigest(trimmed, out var digest, out var reference))
{
return (reference, digest);
}
return (trimmed, null);
}
private static bool TryExtractDigest(string candidate, out string? digest, out string? reference)
{
var atIndex = candidate.IndexOf('@');
if (atIndex >= 0 && atIndex < candidate.Length - 1)
{
var digestCandidate = candidate[(atIndex + 1)..];
if (IsDigestValue(digestCandidate))
{
digest = digestCandidate.ToLowerInvariant();
reference = candidate[..atIndex].Trim();
if (string.IsNullOrWhiteSpace(reference))
{
reference = null;
}
return true;
}
}
if (IsDigestValue(candidate))
{
digest = candidate.ToLowerInvariant();
reference = null;
return true;
}
digest = null;
reference = null;
return false;
}
private static bool IsDigestValue(string value)
{
var separatorIndex = value.IndexOf(':');
if (separatorIndex <= 0 || separatorIndex >= value.Length - 1)
{
return false;
}
var algorithm = value[..separatorIndex];
var digestPart = value[(separatorIndex + 1)..];
if (string.IsNullOrWhiteSpace(algorithm) || string.IsNullOrWhiteSpace(digestPart) || digestPart.Length < 32)
{
return false;
}
foreach (var c in digestPart)
{
if (!IsHexChar(c))
{
return false;
}
}
return true;
}
private static bool LooksLikeScanId(string value)
{
if (value.Length != 40)
{
return false;
}
foreach (var c in value)
{
if (!IsHexChar(c))
{
return false;
}
}
return true;
}
private static bool IsHexChar(char c)
=> (c >= '0' && c <= '9')
|| (c >= 'a' && c <= 'f')
|| (c >= 'A' && c <= 'F');
}