up
This commit is contained in:
@@ -16,7 +16,7 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Configuration
|
||||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||||
.AddEnvironmentVariables("SBOM_");
|
||||
|
||||
|
||||
builder.Services.AddOptions();
|
||||
builder.Services.AddLogging();
|
||||
|
||||
@@ -152,6 +152,21 @@ static string? FindFixture(IHostEnvironment env, string fileName)
|
||||
return null;
|
||||
}
|
||||
|
||||
static int NormalizeLimit(int? requested, int defaultValue, int ceiling)
|
||||
{
|
||||
if (!requested.HasValue)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (requested.Value <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.Min(requested.Value, ceiling);
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
@@ -225,27 +240,27 @@ app.MapPost("/entrypoints", async Task<IResult> (
|
||||
var items = await repo.ListAsync(tenantId, cancellationToken);
|
||||
return Results.Ok(new EntrypointListResponse(tenantId, items));
|
||||
});
|
||||
|
||||
|
||||
app.MapGet("/console/sboms", async Task<IResult> (
|
||||
[FromServices] ISbomQueryService service,
|
||||
[FromQuery] string? artifact,
|
||||
[FromQuery] string? license,
|
||||
[FromQuery] string? scope,
|
||||
[FromQuery(Name = "assetTag")] string? assetTag,
|
||||
[FromQuery] string? cursor,
|
||||
[FromQuery] int? limit,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
|
||||
{
|
||||
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
|
||||
}
|
||||
|
||||
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
return Results.BadRequest(new { error = "cursor must be an integer offset" });
|
||||
}
|
||||
|
||||
[FromQuery] string? scope,
|
||||
[FromQuery(Name = "assetTag")] string? assetTag,
|
||||
[FromQuery] string? cursor,
|
||||
[FromQuery] int? limit,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
|
||||
{
|
||||
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
|
||||
}
|
||||
|
||||
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
return Results.BadRequest(new { error = "cursor must be an integer offset" });
|
||||
}
|
||||
|
||||
var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture);
|
||||
var pageSize = limit ?? 50;
|
||||
|
||||
@@ -257,43 +272,43 @@ app.MapGet("/console/sboms", async Task<IResult> (
|
||||
cancellationToken);
|
||||
|
||||
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
|
||||
SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new TagList
|
||||
{
|
||||
{ "scope", scope ?? string.Empty },
|
||||
{ "env", string.Empty }
|
||||
});
|
||||
SbomMetrics.PathsQueryTotal.Add(1, new TagList
|
||||
{
|
||||
{ "cache_hit", result.CacheHit },
|
||||
{ "scope", scope ?? string.Empty }
|
||||
});
|
||||
|
||||
return Results.Ok(result.Result);
|
||||
});
|
||||
|
||||
SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("scope", scope ?? string.Empty),
|
||||
new KeyValuePair<string, object?>("env", string.Empty)
|
||||
});
|
||||
SbomMetrics.PathsQueryTotal.Add(1, new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("cache_hit", result.CacheHit),
|
||||
new KeyValuePair<string, object?>("scope", scope ?? string.Empty)
|
||||
});
|
||||
|
||||
return Results.Ok(result.Result);
|
||||
});
|
||||
|
||||
app.MapGet("/components/lookup", async Task<IResult> (
|
||||
[FromServices] ISbomQueryService service,
|
||||
[FromQuery] string? purl,
|
||||
[FromQuery] string? artifact,
|
||||
[FromQuery] string? cursor,
|
||||
[FromQuery] int? limit,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(purl))
|
||||
{
|
||||
return Results.BadRequest(new { error = "purl is required" });
|
||||
}
|
||||
|
||||
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
|
||||
{
|
||||
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
|
||||
}
|
||||
|
||||
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
return Results.BadRequest(new { error = "cursor must be an integer offset" });
|
||||
}
|
||||
|
||||
[FromQuery] string? cursor,
|
||||
[FromQuery] int? limit,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(purl))
|
||||
{
|
||||
return Results.BadRequest(new { error = "purl is required" });
|
||||
}
|
||||
|
||||
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
|
||||
{
|
||||
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
|
||||
}
|
||||
|
||||
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
return Results.BadRequest(new { error = "cursor must be an integer offset" });
|
||||
}
|
||||
|
||||
var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture);
|
||||
var pageSize = limit ?? 50;
|
||||
|
||||
@@ -304,22 +319,84 @@ app.MapGet("/components/lookup", async Task<IResult> (
|
||||
var result = await service.GetComponentLookupAsync(
|
||||
new ComponentLookupQuery(purl.Trim(), artifact?.Trim(), pageSize, offset),
|
||||
cancellationToken);
|
||||
|
||||
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
|
||||
SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new TagList
|
||||
{
|
||||
{ "scope", string.Empty },
|
||||
{ "env", string.Empty }
|
||||
});
|
||||
SbomMetrics.PathsQueryTotal.Add(1, new TagList
|
||||
{
|
||||
{ "cache_hit", result.CacheHit },
|
||||
{ "scope", string.Empty }
|
||||
});
|
||||
|
||||
return Results.Ok(result.Result);
|
||||
});
|
||||
|
||||
|
||||
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
|
||||
SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("scope", string.Empty),
|
||||
new KeyValuePair<string, object?>("env", string.Empty)
|
||||
});
|
||||
SbomMetrics.PathsQueryTotal.Add(1, new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("cache_hit", result.CacheHit),
|
||||
new KeyValuePair<string, object?>("scope", string.Empty)
|
||||
});
|
||||
|
||||
return Results.Ok(result.Result);
|
||||
});
|
||||
|
||||
app.MapGet("/sbom/context", async Task<IResult> (
|
||||
[FromServices] ISbomQueryService service,
|
||||
[FromServices] IClock clock,
|
||||
[FromQuery(Name = "artifactId")] string? artifactId,
|
||||
[FromQuery] string? purl,
|
||||
[FromQuery] int? maxTimelineEntries,
|
||||
[FromQuery] int? maxDependencyPaths,
|
||||
[FromQuery] bool? includeEnvironmentFlags,
|
||||
[FromQuery] bool? includeBlastRadius,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(artifactId))
|
||||
{
|
||||
return Results.BadRequest(new { error = "artifactId is required" });
|
||||
}
|
||||
|
||||
var normalizedArtifact = artifactId.Trim();
|
||||
var normalizedPurl = string.IsNullOrWhiteSpace(purl) ? null : purl.Trim();
|
||||
|
||||
var timelineLimit = NormalizeLimit(maxTimelineEntries, 50, 500);
|
||||
var dependencyLimit = NormalizeLimit(maxDependencyPaths, 25, 200);
|
||||
var includeEnvFlags = includeEnvironmentFlags ?? true;
|
||||
var includeBlast = includeBlastRadius ?? true;
|
||||
|
||||
IReadOnlyList<SbomVersion> versions = Array.Empty<SbomVersion>();
|
||||
if (timelineLimit > 0)
|
||||
{
|
||||
var timeline = await service.GetTimelineAsync(
|
||||
new SbomTimelineQuery(normalizedArtifact, timelineLimit, 0),
|
||||
cancellationToken);
|
||||
versions = timeline.Result.Versions;
|
||||
}
|
||||
|
||||
IReadOnlyList<SbomPath> dependencyPaths = Array.Empty<SbomPath>();
|
||||
if (dependencyLimit > 0 && !string.IsNullOrWhiteSpace(normalizedPurl))
|
||||
{
|
||||
var artifactFilter = normalizedArtifact.Contains('@', StringComparison.Ordinal)
|
||||
? normalizedArtifact
|
||||
: null;
|
||||
var pathResult = await service.GetPathsAsync(
|
||||
new SbomPathQuery(normalizedPurl!, artifactFilter, Scope: null, Environment: null, Limit: dependencyLimit, Offset: 0),
|
||||
cancellationToken);
|
||||
dependencyPaths = pathResult.Result.Paths;
|
||||
}
|
||||
|
||||
if (versions.Count == 0 && dependencyPaths.Count == 0)
|
||||
{
|
||||
return Results.NotFound(new { error = "No SBOM context available for specified artifact/purl." });
|
||||
}
|
||||
|
||||
var response = SbomContextAssembler.Build(
|
||||
normalizedArtifact,
|
||||
normalizedPurl,
|
||||
clock.UtcNow,
|
||||
versions,
|
||||
dependencyPaths,
|
||||
includeEnvFlags,
|
||||
includeBlast);
|
||||
|
||||
return Results.Ok(response);
|
||||
});
|
||||
|
||||
app.MapGet("/sbom/paths", async Task<IResult> (
|
||||
[FromServices] IServiceProvider services,
|
||||
[FromQuery] string? purl,
|
||||
@@ -327,22 +404,22 @@ app.MapGet("/sbom/paths", async Task<IResult> (
|
||||
[FromQuery] string? scope,
|
||||
[FromQuery(Name = "env")] string? environment,
|
||||
[FromQuery] string? cursor,
|
||||
[FromQuery] int? limit,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(purl))
|
||||
{
|
||||
return Results.BadRequest(new { error = "purl is required" });
|
||||
}
|
||||
|
||||
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
|
||||
{
|
||||
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
|
||||
}
|
||||
|
||||
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
return Results.BadRequest(new { error = "cursor must be an integer offset" });
|
||||
[FromQuery] int? limit,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(purl))
|
||||
{
|
||||
return Results.BadRequest(new { error = "purl is required" });
|
||||
}
|
||||
|
||||
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
|
||||
{
|
||||
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
|
||||
}
|
||||
|
||||
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
return Results.BadRequest(new { error = "cursor must be an integer offset" });
|
||||
}
|
||||
|
||||
var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture);
|
||||
@@ -353,22 +430,22 @@ app.MapGet("/sbom/paths", async Task<IResult> (
|
||||
var result = await service.GetPathsAsync(
|
||||
new SbomPathQuery(purl.Trim(), artifact?.Trim(), scope?.Trim(), environment?.Trim(), pageSize, offset),
|
||||
cancellationToken);
|
||||
|
||||
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
|
||||
SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new TagList
|
||||
{
|
||||
{ "scope", scope ?? string.Empty },
|
||||
{ "env", environment ?? string.Empty }
|
||||
});
|
||||
SbomMetrics.PathsQueryTotal.Add(1, new TagList
|
||||
{
|
||||
{ "cache_hit", result.CacheHit },
|
||||
{ "scope", scope ?? string.Empty }
|
||||
});
|
||||
|
||||
return Results.Ok(result.Result);
|
||||
});
|
||||
|
||||
|
||||
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
|
||||
SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("scope", scope ?? string.Empty),
|
||||
new KeyValuePair<string, object?>("env", environment ?? string.Empty)
|
||||
});
|
||||
SbomMetrics.PathsQueryTotal.Add(1, new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("cache_hit", result.CacheHit),
|
||||
new KeyValuePair<string, object?>("scope", scope ?? string.Empty)
|
||||
});
|
||||
|
||||
return Results.Ok(result.Result);
|
||||
});
|
||||
|
||||
app.MapGet("/sbom/versions", async Task<IResult> (
|
||||
[FromServices] ISbomQueryService service,
|
||||
[FromQuery] string? artifact,
|
||||
@@ -376,33 +453,40 @@ app.MapGet("/sbom/versions", async Task<IResult> (
|
||||
[FromQuery] int? limit,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(artifact))
|
||||
{
|
||||
return Results.BadRequest(new { error = "artifact is required" });
|
||||
}
|
||||
|
||||
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
|
||||
{
|
||||
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
|
||||
}
|
||||
|
||||
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
return Results.BadRequest(new { error = "cursor must be an integer offset" });
|
||||
}
|
||||
|
||||
var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture);
|
||||
var pageSize = limit ?? 50;
|
||||
|
||||
var start = Stopwatch.GetTimestamp();
|
||||
var result = await service.GetTimelineAsync(
|
||||
new SbomTimelineQuery(artifact.Trim(), pageSize, offset),
|
||||
cancellationToken);
|
||||
|
||||
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
|
||||
SbomMetrics.TimelineLatencySeconds.Record(elapsedSeconds, new TagList { { "artifact", artifact } });
|
||||
SbomMetrics.TimelineQueryTotal.Add(1, new TagList { { "artifact", artifact }, { "cache_hit", result.CacheHit } });
|
||||
|
||||
if (string.IsNullOrWhiteSpace(artifact))
|
||||
{
|
||||
return Results.BadRequest(new { error = "artifact is required" });
|
||||
}
|
||||
|
||||
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
|
||||
{
|
||||
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
|
||||
}
|
||||
|
||||
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
return Results.BadRequest(new { error = "cursor must be an integer offset" });
|
||||
}
|
||||
|
||||
var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture);
|
||||
var pageSize = limit ?? 50;
|
||||
|
||||
var start = Stopwatch.GetTimestamp();
|
||||
var result = await service.GetTimelineAsync(
|
||||
new SbomTimelineQuery(artifact.Trim(), pageSize, offset),
|
||||
cancellationToken);
|
||||
|
||||
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
|
||||
SbomMetrics.TimelineLatencySeconds.Record(elapsedSeconds, new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("artifact", artifact)
|
||||
});
|
||||
SbomMetrics.TimelineQueryTotal.Add(1, new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("artifact", artifact),
|
||||
new KeyValuePair<string, object?>("cache_hit", result.CacheHit)
|
||||
});
|
||||
|
||||
return Results.Ok(result.Result);
|
||||
});
|
||||
|
||||
@@ -445,10 +529,19 @@ app.MapGet("/sboms/{snapshotId}/projection", async Task<IResult> (
|
||||
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
var sizeBytes = System.Text.Encoding.UTF8.GetByteCount(json);
|
||||
SbomMetrics.ProjectionSizeBytes.Record(sizeBytes, new TagList { { "tenant", projection.TenantId } });
|
||||
SbomMetrics.ProjectionSizeBytes.Record(sizeBytes, new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", projection.TenantId)
|
||||
});
|
||||
SbomMetrics.ProjectionLatencySeconds.Record(Stopwatch.GetElapsedTime(start).TotalSeconds,
|
||||
new TagList { { "tenant", projection.TenantId } });
|
||||
SbomMetrics.ProjectionQueryTotal.Add(1, new TagList { { "tenant", projection.TenantId } });
|
||||
new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", projection.TenantId)
|
||||
});
|
||||
SbomMetrics.ProjectionQueryTotal.Add(1, new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", projection.TenantId)
|
||||
});
|
||||
|
||||
app.Logger.LogInformation("projection_returned tenant={Tenant} snapshot={Snapshot} size={SizeBytes}", projection.TenantId, projection.SnapshotId, sizeBytes);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user