Implement ledger metrics for observability and add tests for Ruby packages endpoints
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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:
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user