save development progress
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SbomEndpointExtensions.cs
|
||||
// Sprint: SPRINT_8200_0013_0003_SCAN_sbom_intersection_scoring
|
||||
// Tasks: SBOM-8200-022 through SBOM-8200-024
|
||||
// Description: API endpoints for SBOM registration and learning
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Concelier.SbomIntegration;
|
||||
using StellaOps.Concelier.SbomIntegration.Models;
|
||||
using HttpResults = Microsoft.AspNetCore.Http.Results;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint extensions for SBOM operations.
|
||||
/// </summary>
|
||||
internal static class SbomEndpointExtensions
|
||||
{
|
||||
public static void MapSbomEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/v1")
|
||||
.WithTags("SBOM Learning");
|
||||
|
||||
// POST /api/v1/learn/sbom - Register and learn from an SBOM
|
||||
group.MapPost("/learn/sbom", async (
|
||||
[FromBody] LearnSbomRequest request,
|
||||
ISbomRegistryService registryService,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var input = new SbomRegistrationInput
|
||||
{
|
||||
Digest = request.SbomDigest,
|
||||
Format = ParseSbomFormat(request.Format),
|
||||
SpecVersion = request.SpecVersion ?? "1.6",
|
||||
PrimaryName = request.PrimaryName,
|
||||
PrimaryVersion = request.PrimaryVersion,
|
||||
Purls = request.Purls,
|
||||
Source = request.Source ?? "api",
|
||||
TenantId = request.TenantId,
|
||||
ReachabilityMap = request.ReachabilityMap,
|
||||
DeploymentMap = request.DeploymentMap
|
||||
};
|
||||
|
||||
var result = await registryService.LearnSbomAsync(input, ct).ConfigureAwait(false);
|
||||
|
||||
return HttpResults.Ok(new SbomLearnResponse
|
||||
{
|
||||
SbomDigest = result.Registration.Digest,
|
||||
SbomId = result.Registration.Id,
|
||||
ComponentsProcessed = result.Registration.ComponentCount,
|
||||
AdvisoriesMatched = result.Matches.Count,
|
||||
ScoresUpdated = result.ScoresUpdated,
|
||||
ProcessingTimeMs = result.ProcessingTimeMs
|
||||
});
|
||||
})
|
||||
.WithName("LearnSbom")
|
||||
.WithSummary("Register SBOM and update interest scores for affected advisories")
|
||||
.Produces<SbomLearnResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status400BadRequest);
|
||||
|
||||
// GET /api/v1/sboms/{digest}/affected - Get advisories affecting an SBOM
|
||||
group.MapGet("/sboms/{digest}/affected", async (
|
||||
string digest,
|
||||
ISbomRegistryService registryService,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var registration = await registryService.GetByDigestAsync(digest, ct).ConfigureAwait(false);
|
||||
if (registration is null)
|
||||
{
|
||||
return HttpResults.NotFound(new { error = "SBOM not found", digest });
|
||||
}
|
||||
|
||||
var matches = await registryService.GetMatchesAsync(digest, ct).ConfigureAwait(false);
|
||||
|
||||
return HttpResults.Ok(new SbomAffectedResponse
|
||||
{
|
||||
SbomDigest = digest,
|
||||
SbomId = registration.Id,
|
||||
PrimaryName = registration.PrimaryName,
|
||||
PrimaryVersion = registration.PrimaryVersion,
|
||||
ComponentCount = registration.ComponentCount,
|
||||
AffectedCount = matches.Count,
|
||||
Matches = matches.Select(m => new SbomMatchInfo
|
||||
{
|
||||
CanonicalId = m.CanonicalId,
|
||||
Purl = m.Purl,
|
||||
IsReachable = m.IsReachable,
|
||||
IsDeployed = m.IsDeployed,
|
||||
Confidence = m.Confidence,
|
||||
Method = m.Method.ToString(),
|
||||
MatchedAt = m.MatchedAt
|
||||
}).ToList()
|
||||
});
|
||||
})
|
||||
.WithName("GetSbomAffected")
|
||||
.WithSummary("Get advisories affecting an SBOM")
|
||||
.Produces<SbomAffectedResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
// GET /api/v1/sboms - List registered SBOMs
|
||||
group.MapGet("/sboms", async (
|
||||
[FromQuery] int? offset,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? tenantId,
|
||||
ISbomRegistryService registryService,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var registrations = await registryService.ListAsync(
|
||||
offset ?? 0,
|
||||
limit ?? 50,
|
||||
tenantId,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
var count = await registryService.CountAsync(tenantId, ct).ConfigureAwait(false);
|
||||
|
||||
return HttpResults.Ok(new SbomListResponse
|
||||
{
|
||||
Items = registrations.Select(r => new SbomSummary
|
||||
{
|
||||
Id = r.Id,
|
||||
Digest = r.Digest,
|
||||
Format = r.Format.ToString(),
|
||||
PrimaryName = r.PrimaryName,
|
||||
PrimaryVersion = r.PrimaryVersion,
|
||||
ComponentCount = r.ComponentCount,
|
||||
AffectedCount = r.AffectedCount,
|
||||
RegisteredAt = r.RegisteredAt,
|
||||
LastMatchedAt = r.LastMatchedAt
|
||||
}).ToList(),
|
||||
TotalCount = count,
|
||||
Offset = offset ?? 0,
|
||||
Limit = limit ?? 50
|
||||
});
|
||||
})
|
||||
.WithName("ListSboms")
|
||||
.WithSummary("List registered SBOMs with pagination")
|
||||
.Produces<SbomListResponse>(StatusCodes.Status200OK);
|
||||
|
||||
// GET /api/v1/sboms/{digest} - Get SBOM registration details
|
||||
group.MapGet("/sboms/{digest}", async (
|
||||
string digest,
|
||||
ISbomRegistryService registryService,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var registration = await registryService.GetByDigestAsync(digest, ct).ConfigureAwait(false);
|
||||
|
||||
if (registration is null)
|
||||
{
|
||||
return HttpResults.NotFound(new { error = "SBOM not found", digest });
|
||||
}
|
||||
|
||||
return HttpResults.Ok(new SbomDetailResponse
|
||||
{
|
||||
Id = registration.Id,
|
||||
Digest = registration.Digest,
|
||||
Format = registration.Format.ToString(),
|
||||
SpecVersion = registration.SpecVersion,
|
||||
PrimaryName = registration.PrimaryName,
|
||||
PrimaryVersion = registration.PrimaryVersion,
|
||||
ComponentCount = registration.ComponentCount,
|
||||
AffectedCount = registration.AffectedCount,
|
||||
Source = registration.Source,
|
||||
TenantId = registration.TenantId,
|
||||
RegisteredAt = registration.RegisteredAt,
|
||||
LastMatchedAt = registration.LastMatchedAt
|
||||
});
|
||||
})
|
||||
.WithName("GetSbom")
|
||||
.WithSummary("Get SBOM registration details")
|
||||
.Produces<SbomDetailResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
// DELETE /api/v1/sboms/{digest} - Unregister an SBOM
|
||||
group.MapDelete("/sboms/{digest}", async (
|
||||
string digest,
|
||||
ISbomRegistryService registryService,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
await registryService.UnregisterAsync(digest, ct).ConfigureAwait(false);
|
||||
return HttpResults.NoContent();
|
||||
})
|
||||
.WithName("UnregisterSbom")
|
||||
.WithSummary("Unregister an SBOM")
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
|
||||
// POST /api/v1/sboms/{digest}/rematch - Rematch SBOM against current advisories
|
||||
group.MapPost("/sboms/{digest}/rematch", async (
|
||||
string digest,
|
||||
ISbomRegistryService registryService,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await registryService.RematchSbomAsync(digest, ct).ConfigureAwait(false);
|
||||
|
||||
return HttpResults.Ok(new SbomRematchResponse
|
||||
{
|
||||
SbomDigest = digest,
|
||||
PreviousAffectedCount = result.Registration.AffectedCount,
|
||||
NewAffectedCount = result.Matches.Count,
|
||||
ProcessingTimeMs = result.ProcessingTimeMs
|
||||
});
|
||||
}
|
||||
catch (InvalidOperationException ex) when (ex.Message.Contains("not found"))
|
||||
{
|
||||
return HttpResults.NotFound(new { error = ex.Message });
|
||||
}
|
||||
})
|
||||
.WithName("RematchSbom")
|
||||
.WithSummary("Re-match SBOM against current advisory database")
|
||||
.Produces<SbomRematchResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
// GET /api/v1/sboms/stats - Get SBOM registry statistics
|
||||
group.MapGet("/sboms/stats", async (
|
||||
[FromQuery] string? tenantId,
|
||||
ISbomRegistryService registryService,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var stats = await registryService.GetStatsAsync(tenantId, ct).ConfigureAwait(false);
|
||||
|
||||
return HttpResults.Ok(new SbomStatsResponse
|
||||
{
|
||||
TotalSboms = stats.TotalSboms,
|
||||
TotalPurls = stats.TotalPurls,
|
||||
TotalMatches = stats.TotalMatches,
|
||||
AffectedSboms = stats.AffectedSboms,
|
||||
AverageMatchesPerSbom = stats.AverageMatchesPerSbom
|
||||
});
|
||||
})
|
||||
.WithName("GetSbomStats")
|
||||
.WithSummary("Get SBOM registry statistics")
|
||||
.Produces<SbomStatsResponse>(StatusCodes.Status200OK);
|
||||
}
|
||||
|
||||
private static SbomFormat ParseSbomFormat(string? format)
|
||||
{
|
||||
return format?.ToLowerInvariant() switch
|
||||
{
|
||||
"cyclonedx" => SbomFormat.CycloneDX,
|
||||
"spdx" => SbomFormat.SPDX,
|
||||
_ => SbomFormat.CycloneDX
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#region Request/Response DTOs
|
||||
|
||||
public sealed record LearnSbomRequest
|
||||
{
|
||||
public required string SbomDigest { get; init; }
|
||||
public string? Format { get; init; }
|
||||
public string? SpecVersion { get; init; }
|
||||
public string? PrimaryName { get; init; }
|
||||
public string? PrimaryVersion { get; init; }
|
||||
public required IReadOnlyList<string> Purls { get; init; }
|
||||
public string? Source { get; init; }
|
||||
public string? TenantId { get; init; }
|
||||
public IReadOnlyDictionary<string, bool>? ReachabilityMap { get; init; }
|
||||
public IReadOnlyDictionary<string, bool>? DeploymentMap { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SbomLearnResponse
|
||||
{
|
||||
public required string SbomDigest { get; init; }
|
||||
public Guid SbomId { get; init; }
|
||||
public int ComponentsProcessed { get; init; }
|
||||
public int AdvisoriesMatched { get; init; }
|
||||
public int ScoresUpdated { get; init; }
|
||||
public double ProcessingTimeMs { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SbomAffectedResponse
|
||||
{
|
||||
public required string SbomDigest { get; init; }
|
||||
public Guid SbomId { get; init; }
|
||||
public string? PrimaryName { get; init; }
|
||||
public string? PrimaryVersion { get; init; }
|
||||
public int ComponentCount { get; init; }
|
||||
public int AffectedCount { get; init; }
|
||||
public required IReadOnlyList<SbomMatchInfo> Matches { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SbomMatchInfo
|
||||
{
|
||||
public Guid CanonicalId { get; init; }
|
||||
public required string Purl { get; init; }
|
||||
public bool IsReachable { get; init; }
|
||||
public bool IsDeployed { get; init; }
|
||||
public double Confidence { get; init; }
|
||||
public required string Method { get; init; }
|
||||
public DateTimeOffset MatchedAt { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SbomListResponse
|
||||
{
|
||||
public required IReadOnlyList<SbomSummary> Items { get; init; }
|
||||
public long TotalCount { get; init; }
|
||||
public int Offset { get; init; }
|
||||
public int Limit { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SbomSummary
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Format { get; init; }
|
||||
public string? PrimaryName { get; init; }
|
||||
public string? PrimaryVersion { get; init; }
|
||||
public int ComponentCount { get; init; }
|
||||
public int AffectedCount { get; init; }
|
||||
public DateTimeOffset RegisteredAt { get; init; }
|
||||
public DateTimeOffset? LastMatchedAt { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SbomDetailResponse
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Format { get; init; }
|
||||
public required string SpecVersion { get; init; }
|
||||
public string? PrimaryName { get; init; }
|
||||
public string? PrimaryVersion { get; init; }
|
||||
public int ComponentCount { get; init; }
|
||||
public int AffectedCount { get; init; }
|
||||
public required string Source { get; init; }
|
||||
public string? TenantId { get; init; }
|
||||
public DateTimeOffset RegisteredAt { get; init; }
|
||||
public DateTimeOffset? LastMatchedAt { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SbomRematchResponse
|
||||
{
|
||||
public required string SbomDigest { get; init; }
|
||||
public int PreviousAffectedCount { get; init; }
|
||||
public int NewAffectedCount { get; init; }
|
||||
public double ProcessingTimeMs { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SbomStatsResponse
|
||||
{
|
||||
public long TotalSboms { get; init; }
|
||||
public long TotalPurls { get; init; }
|
||||
public long TotalMatches { get; init; }
|
||||
public long AffectedSboms { get; init; }
|
||||
public double AverageMatchesPerSbom { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user