work
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
using StellaOps.VulnExplorer.Api.Models;
|
||||
|
||||
namespace StellaOps.VulnExplorer.Api.Data;
|
||||
|
||||
internal static class SampleData
|
||||
{
|
||||
private static readonly VulnSummary[] summaries =
|
||||
{
|
||||
new(
|
||||
Id: "vuln-0001",
|
||||
Severity: "HIGH",
|
||||
Score: 8.2,
|
||||
Kev: true,
|
||||
Exploitability: "known",
|
||||
FixAvailable: true,
|
||||
CveIds: new[] { "CVE-2025-0001" },
|
||||
Purls: new[] { "pkg:maven/org.example/app@1.2.3" },
|
||||
PolicyVersion: "policy-main",
|
||||
RationaleId: "rat-0001"),
|
||||
new(
|
||||
Id: "vuln-0002",
|
||||
Severity: "MEDIUM",
|
||||
Score: 5.4,
|
||||
Kev: false,
|
||||
Exploitability: "unknown",
|
||||
FixAvailable: false,
|
||||
CveIds: new[] { "CVE-2024-2222" },
|
||||
Purls: new[] { "pkg:npm/foo@4.5.6" },
|
||||
PolicyVersion: "policy-main",
|
||||
RationaleId: "rat-0002")
|
||||
};
|
||||
|
||||
private static readonly VulnDetail[] details =
|
||||
{
|
||||
new(
|
||||
Id: "vuln-0001",
|
||||
Severity: "HIGH",
|
||||
Score: 8.2,
|
||||
Kev: true,
|
||||
Exploitability: "known",
|
||||
FixAvailable: true,
|
||||
CveIds: summaries[0].CveIds,
|
||||
Purls: summaries[0].Purls,
|
||||
Summary: "Example vulnerable library with RCE.",
|
||||
AffectedPackages: new[]
|
||||
{
|
||||
new PackageAffect("pkg:maven/org.example/app", new[] { "1.2.3" })
|
||||
},
|
||||
AdvisoryRefs: new[]
|
||||
{
|
||||
new AdvisoryRef("https://example.com/advisory/0001", "Upstream advisory")
|
||||
},
|
||||
FirstSeen: DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
|
||||
LastSeen: DateTimeOffset.Parse("2025-11-01T00:00:00Z"),
|
||||
PolicyVersion: summaries[0].PolicyVersion,
|
||||
RationaleId: summaries[0].RationaleId,
|
||||
Provenance: new EvidenceProvenance("ledger-1", "evidence-1")),
|
||||
new(
|
||||
Id: "vuln-0002",
|
||||
Severity: "MEDIUM",
|
||||
Score: 5.4,
|
||||
Kev: false,
|
||||
Exploitability: "unknown",
|
||||
FixAvailable: false,
|
||||
CveIds: summaries[1].CveIds,
|
||||
Purls: summaries[1].Purls,
|
||||
Summary: "Prototype pollution risk.",
|
||||
AffectedPackages: new[]
|
||||
{
|
||||
new PackageAffect("pkg:npm/foo", new[] { "4.5.6" })
|
||||
},
|
||||
AdvisoryRefs: Array.Empty<AdvisoryRef>(),
|
||||
FirstSeen: DateTimeOffset.Parse("2024-06-10T00:00:00Z"),
|
||||
LastSeen: DateTimeOffset.Parse("2025-08-15T00:00:00Z"),
|
||||
PolicyVersion: summaries[1].PolicyVersion,
|
||||
RationaleId: summaries[1].RationaleId,
|
||||
Provenance: new EvidenceProvenance("ledger-2", "evidence-2"))
|
||||
};
|
||||
|
||||
public static IReadOnlyList<VulnSummary> Summaries => summaries;
|
||||
|
||||
public static bool TryGetDetail(string id, out VulnDetail? detail)
|
||||
{
|
||||
detail = details.FirstOrDefault(d => string.Equals(d.Id, id, StringComparison.Ordinal));
|
||||
return detail is not null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace StellaOps.VulnExplorer.Api.Models;
|
||||
|
||||
public sealed record VulnSummary(
|
||||
string Id,
|
||||
string Severity,
|
||||
double Score,
|
||||
bool Kev,
|
||||
string Exploitability,
|
||||
bool FixAvailable,
|
||||
IReadOnlyList<string> CveIds,
|
||||
IReadOnlyList<string> Purls,
|
||||
string PolicyVersion,
|
||||
string RationaleId);
|
||||
|
||||
public sealed record VulnDetail(
|
||||
string Id,
|
||||
string Severity,
|
||||
double Score,
|
||||
bool Kev,
|
||||
string Exploitability,
|
||||
bool FixAvailable,
|
||||
IReadOnlyList<string> CveIds,
|
||||
IReadOnlyList<string> Purls,
|
||||
string Summary,
|
||||
IReadOnlyList<PackageAffect> AffectedPackages,
|
||||
IReadOnlyList<AdvisoryRef> AdvisoryRefs,
|
||||
DateTimeOffset FirstSeen,
|
||||
DateTimeOffset LastSeen,
|
||||
string PolicyVersion,
|
||||
string RationaleId,
|
||||
EvidenceProvenance Provenance);
|
||||
|
||||
public sealed record PackageAffect(string Purl, IReadOnlyList<string> Versions);
|
||||
|
||||
public sealed record AdvisoryRef(string Url, string Title);
|
||||
|
||||
public sealed record EvidenceProvenance(string LedgerEntryId, string EvidenceBundleId);
|
||||
|
||||
public sealed record VulnListResponse(IReadOnlyList<VulnSummary> Items, string? NextPageToken);
|
||||
115
src/VulnExplorer/StellaOps.VulnExplorer.Api/Program.cs
Normal file
115
src/VulnExplorer/StellaOps.VulnExplorer.Api/Program.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using StellaOps.VulnExplorer.Api.Data;
|
||||
using StellaOps.VulnExplorer.Api.Models;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.MapGet("/v1/vulns", (
|
||||
HttpRequest request,
|
||||
[AsParameters] VulnFilter filter) =>
|
||||
{
|
||||
var tenant = request.Headers["x-stella-tenant"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
return Results.BadRequest(new { error = "x-stella-tenant required" });
|
||||
}
|
||||
|
||||
var data = ApplyFilter(SampleData.Summaries, filter);
|
||||
var pageSize = Math.Clamp(filter.PageSize ?? 50, 1, 200);
|
||||
var offset = ParsePageToken(filter.PageToken);
|
||||
|
||||
var page = data.Skip(offset).Take(pageSize).ToArray();
|
||||
var nextOffset = offset + page.Length;
|
||||
var next = nextOffset < data.Count ? nextOffset.ToString(CultureInfo.InvariantCulture) : null;
|
||||
|
||||
var response = new VulnListResponse(page, next);
|
||||
return Results.Ok(response);
|
||||
})
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapGet("/v1/vulns/{id}", (HttpRequest request, string id) =>
|
||||
{
|
||||
var tenant = request.Headers["x-stella-tenant"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
return Results.BadRequest(new { error = "x-stella-tenant required" });
|
||||
}
|
||||
|
||||
return SampleData.TryGetDetail(id, out var detail) && detail is not null
|
||||
? Results.Ok(detail)
|
||||
: Results.NotFound();
|
||||
})
|
||||
.WithOpenApi();
|
||||
|
||||
app.Run();
|
||||
|
||||
static int ParsePageToken(string? token) =>
|
||||
int.TryParse(token, out var offset) && offset >= 0 ? offset : 0;
|
||||
|
||||
static IReadOnlyList<VulnSummary> ApplyFilter(IReadOnlyList<VulnSummary> source, VulnFilter filter)
|
||||
{
|
||||
IEnumerable<VulnSummary> query = source;
|
||||
|
||||
if (filter.Cve?.Count > 0)
|
||||
{
|
||||
var set = filter.Cve.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
query = query.Where(v => v.CveIds.Any(set.Contains));
|
||||
}
|
||||
|
||||
if (filter.Purl?.Count > 0)
|
||||
{
|
||||
var set = filter.Purl.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
query = query.Where(v => v.Purls.Any(set.Contains));
|
||||
}
|
||||
|
||||
if (filter.Severity?.Count > 0)
|
||||
{
|
||||
var set = filter.Severity.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
query = query.Where(v => set.Contains(v.Severity));
|
||||
}
|
||||
|
||||
if (filter.Exploitability is not null)
|
||||
{
|
||||
query = query.Where(v => string.Equals(v.Exploitability, filter.Exploitability, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (filter.FixAvailable is not null)
|
||||
{
|
||||
query = query.Where(v => v.FixAvailable == filter.FixAvailable);
|
||||
}
|
||||
|
||||
// deterministic ordering: score desc, id asc
|
||||
query = query
|
||||
.OrderByDescending(v => v.Score)
|
||||
.ThenBy(v => v.Id, StringComparer.Ordinal);
|
||||
|
||||
return query.ToArray();
|
||||
}
|
||||
|
||||
public record VulnFilter(
|
||||
[FromHeader(Name = "x-stella-tenant")] string Tenant,
|
||||
[FromQuery(Name = "policyVersion")] string? PolicyVersion,
|
||||
[FromQuery(Name = "pageSize")] int? PageSize,
|
||||
[FromQuery(Name = "pageToken")] string? PageToken,
|
||||
[FromQuery(Name = "cve")] IReadOnlyList<string>? Cve,
|
||||
[FromQuery(Name = "purl")] IReadOnlyList<string>? Purl,
|
||||
[FromQuery(Name = "severity")] IReadOnlyList<string>? Severity,
|
||||
[FromQuery(Name = "exploitability")] string? Exploitability,
|
||||
[FromQuery(Name = "fixAvailable")] bool? FixAvailable);
|
||||
|
||||
public partial class Program { }
|
||||
|
||||
public partial class Program { }
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RootNamespace>StellaOps.VulnExplorer.Api</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user