Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
namespace StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Evidence graph for a finding showing all contributing evidence.
|
||||
/// </summary>
|
||||
public sealed record EvidenceGraphResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Finding this graph is for.
|
||||
/// </summary>
|
||||
public required Guid FindingId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerability ID.
|
||||
/// </summary>
|
||||
public required string VulnerabilityId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// All evidence nodes.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<EvidenceNode> Nodes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Edges representing derivation relationships.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<EvidenceEdge> Edges { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Root node (verdict).
|
||||
/// </summary>
|
||||
public required string RootNodeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Graph generation timestamp.
|
||||
/// </summary>
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A node in the evidence graph.
|
||||
/// </summary>
|
||||
public sealed record EvidenceNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Node identifier (content-addressed).
|
||||
/// </summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Node type.
|
||||
/// </summary>
|
||||
public required EvidenceNodeType Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable label.
|
||||
/// </summary>
|
||||
public required string Label { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content digest (sha256:...).
|
||||
/// </summary>
|
||||
public required string Digest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Issuer of this evidence.
|
||||
/// </summary>
|
||||
public string? Issuer { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when created.
|
||||
/// </summary>
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signature status.
|
||||
/// </summary>
|
||||
public required SignatureStatus Signature { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> Metadata { get; init; }
|
||||
= new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// URL to fetch raw content.
|
||||
/// </summary>
|
||||
public string? ContentUrl { get; init; }
|
||||
}
|
||||
|
||||
public enum EvidenceNodeType
|
||||
{
|
||||
/// <summary>Final verdict.</summary>
|
||||
Verdict,
|
||||
|
||||
/// <summary>Policy evaluation trace.</summary>
|
||||
PolicyTrace,
|
||||
|
||||
/// <summary>VEX statement.</summary>
|
||||
VexStatement,
|
||||
|
||||
/// <summary>Reachability analysis.</summary>
|
||||
Reachability,
|
||||
|
||||
/// <summary>Runtime observation.</summary>
|
||||
RuntimeObservation,
|
||||
|
||||
/// <summary>SBOM component.</summary>
|
||||
SbomComponent,
|
||||
|
||||
/// <summary>Advisory source.</summary>
|
||||
Advisory,
|
||||
|
||||
/// <summary>Build provenance.</summary>
|
||||
Provenance,
|
||||
|
||||
/// <summary>Attestation envelope.</summary>
|
||||
Attestation
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signature verification status.
|
||||
/// </summary>
|
||||
public sealed record SignatureStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether signed.
|
||||
/// </summary>
|
||||
public required bool IsSigned { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether signature is valid.
|
||||
/// </summary>
|
||||
public bool? IsValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signer identity (if known).
|
||||
/// </summary>
|
||||
public string? SignerIdentity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signing timestamp.
|
||||
/// </summary>
|
||||
public DateTimeOffset? SignedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Key ID used for signing.
|
||||
/// </summary>
|
||||
public string? KeyId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor log index (if published).
|
||||
/// </summary>
|
||||
public long? RekorLogIndex { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Edge representing derivation relationship.
|
||||
/// </summary>
|
||||
public sealed record EvidenceEdge
|
||||
{
|
||||
/// <summary>
|
||||
/// Source node ID.
|
||||
/// </summary>
|
||||
public required string From { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Target node ID.
|
||||
/// </summary>
|
||||
public required string To { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Relationship type.
|
||||
/// </summary>
|
||||
public required EvidenceRelation Relation { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable label.
|
||||
/// </summary>
|
||||
public string? Label { get; init; }
|
||||
}
|
||||
|
||||
public enum EvidenceRelation
|
||||
{
|
||||
/// <summary>Derived from (input to output).</summary>
|
||||
DerivedFrom,
|
||||
|
||||
/// <summary>Verified by (attestation verifies content).</summary>
|
||||
VerifiedBy,
|
||||
|
||||
/// <summary>Supersedes (newer replaces older).</summary>
|
||||
Supersedes,
|
||||
|
||||
/// <summary>References (general reference).</summary>
|
||||
References,
|
||||
|
||||
/// <summary>Corroborates (supports claim).</summary>
|
||||
Corroborates
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
namespace StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Condensed finding summary for vulnerability-first UX.
|
||||
/// </summary>
|
||||
public sealed record FindingSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// Finding ID.
|
||||
/// </summary>
|
||||
public required Guid FindingId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerability ID (CVE, GHSA, etc.).
|
||||
/// </summary>
|
||||
public required string VulnerabilityId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerability title.
|
||||
/// </summary>
|
||||
public required string Title { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Affected component (PURL).
|
||||
/// </summary>
|
||||
public required string Component { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Verdict status.
|
||||
/// </summary>
|
||||
public required VerdictStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Verdict chip for quick visual reference.
|
||||
/// </summary>
|
||||
public required VerdictChip Chip { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Unified confidence score (0-1).
|
||||
/// </summary>
|
||||
public required decimal Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// One-liner summary explaining the verdict.
|
||||
/// </summary>
|
||||
public required string OneLiner { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Proof badges indicating evidence quality.
|
||||
/// </summary>
|
||||
public required ProofBadges Badges { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVSS score (if available).
|
||||
/// </summary>
|
||||
public decimal? CvssScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Severity level.
|
||||
/// </summary>
|
||||
public string? Severity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// First seen timestamp.
|
||||
/// </summary>
|
||||
public required DateTimeOffset FirstSeen { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Last updated timestamp.
|
||||
/// </summary>
|
||||
public required DateTimeOffset LastUpdated { get; init; }
|
||||
}
|
||||
|
||||
public enum VerdictStatus
|
||||
{
|
||||
Affected,
|
||||
NotAffected,
|
||||
UnderReview,
|
||||
Mitigated
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visual verdict chip with color coding.
|
||||
/// </summary>
|
||||
public sealed record VerdictChip
|
||||
{
|
||||
/// <summary>
|
||||
/// Display text.
|
||||
/// </summary>
|
||||
public required string Label { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Color scheme.
|
||||
/// </summary>
|
||||
public required ChipColor Color { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Icon name (optional).
|
||||
/// </summary>
|
||||
public string? Icon { get; init; }
|
||||
}
|
||||
|
||||
public enum ChipColor
|
||||
{
|
||||
Red, // Affected
|
||||
Green, // NotAffected
|
||||
Yellow, // UnderReview
|
||||
Blue // Mitigated
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Proof badges indicating evidence quality.
|
||||
/// </summary>
|
||||
public sealed record ProofBadges
|
||||
{
|
||||
/// <summary>
|
||||
/// Reachability analysis badge.
|
||||
/// </summary>
|
||||
public required BadgeStatus Reachability { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime corroboration badge.
|
||||
/// </summary>
|
||||
public required BadgeStatus Runtime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy evaluation badge.
|
||||
/// </summary>
|
||||
public required BadgeStatus Policy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Provenance/attestation badge.
|
||||
/// </summary>
|
||||
public required BadgeStatus Provenance { get; init; }
|
||||
}
|
||||
|
||||
public enum BadgeStatus
|
||||
{
|
||||
/// <summary>Evidence not available.</summary>
|
||||
None,
|
||||
|
||||
/// <summary>Evidence available but inconclusive.</summary>
|
||||
Partial,
|
||||
|
||||
/// <summary>Strong evidence available.</summary>
|
||||
Strong,
|
||||
|
||||
/// <summary>Cryptographically verified evidence.</summary>
|
||||
Verified
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Paginated response for finding summaries.
|
||||
/// </summary>
|
||||
public sealed record FindingSummaryPage
|
||||
{
|
||||
public required IReadOnlyList<FindingSummary> Items { get; init; }
|
||||
public required int TotalCount { get; init; }
|
||||
public required int Page { get; init; }
|
||||
public required int PageSize { get; init; }
|
||||
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filter for finding summaries query.
|
||||
/// </summary>
|
||||
public sealed record FindingSummaryFilter
|
||||
{
|
||||
public int Page { get; init; } = 1;
|
||||
public int PageSize { get; init; } = 50;
|
||||
public string? Status { get; init; }
|
||||
public string? Severity { get; init; }
|
||||
public decimal? MinConfidence { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
using StellaOps.Findings.Ledger.WebService.Services;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.WebService.Endpoints;
|
||||
|
||||
public static class EvidenceGraphEndpoints
|
||||
{
|
||||
public static void MapEvidenceGraphEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/v1/findings")
|
||||
.WithTags("Evidence Graph")
|
||||
.RequireAuthorization();
|
||||
|
||||
// GET /api/v1/findings/{findingId}/evidence-graph
|
||||
group.MapGet("/{findingId:guid}/evidence-graph", async Task<Results<Ok<EvidenceGraphResponse>, NotFound>> (
|
||||
Guid findingId,
|
||||
[FromQuery] bool includeContent = false,
|
||||
IEvidenceGraphBuilder builder,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var graph = await builder.BuildAsync(findingId, ct);
|
||||
return graph is not null
|
||||
? TypedResults.Ok(graph)
|
||||
: TypedResults.NotFound();
|
||||
})
|
||||
.WithName("GetEvidenceGraph")
|
||||
.WithDescription("Get evidence graph for finding visualization")
|
||||
.Produces<EvidenceGraphResponse>(200)
|
||||
.Produces(404);
|
||||
|
||||
// GET /api/v1/findings/{findingId}/evidence/{nodeId}
|
||||
group.MapGet("/{findingId:guid}/evidence/{nodeId}", async Task<Results<Ok<object>, NotFound>> (
|
||||
Guid findingId,
|
||||
string nodeId,
|
||||
IEvidenceContentService contentService,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var content = await contentService.GetContentAsync(findingId, nodeId, ct);
|
||||
return content is not null
|
||||
? TypedResults.Ok(content)
|
||||
: TypedResults.NotFound();
|
||||
})
|
||||
.WithName("GetEvidenceNodeContent")
|
||||
.WithDescription("Get raw content for an evidence node")
|
||||
.Produces<object>(200)
|
||||
.Produces(404);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for retrieving evidence node content.
|
||||
/// </summary>
|
||||
public interface IEvidenceContentService
|
||||
{
|
||||
Task<object?> GetContentAsync(Guid findingId, string nodeId, CancellationToken ct);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
using StellaOps.Findings.Ledger.WebService.Services;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.WebService.Endpoints;
|
||||
|
||||
public static class FindingSummaryEndpoints
|
||||
{
|
||||
public static void MapFindingSummaryEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/v1/findings")
|
||||
.WithTags("Findings")
|
||||
.RequireAuthorization();
|
||||
|
||||
// GET /api/v1/findings/{findingId}/summary
|
||||
group.MapGet("/{findingId:guid}/summary", async Task<Results<Ok<FindingSummary>, NotFound>> (
|
||||
Guid findingId,
|
||||
IFindingSummaryService service,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var summary = await service.GetSummaryAsync(findingId, ct);
|
||||
return summary is not null
|
||||
? TypedResults.Ok(summary)
|
||||
: TypedResults.NotFound();
|
||||
})
|
||||
.WithName("GetFindingSummary")
|
||||
.WithDescription("Get condensed finding summary for vulnerability-first UX")
|
||||
.Produces<FindingSummary>(200)
|
||||
.Produces(404);
|
||||
|
||||
// GET /api/v1/findings/summaries
|
||||
group.MapGet("/summaries", async Task<Ok<FindingSummaryPage>> (
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 50,
|
||||
[FromQuery] string? status = null,
|
||||
[FromQuery] string? severity = null,
|
||||
[FromQuery] decimal? minConfidence = null,
|
||||
IFindingSummaryService service,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var filter = new FindingSummaryFilter
|
||||
{
|
||||
Page = page,
|
||||
PageSize = Math.Clamp(pageSize, 1, 100),
|
||||
Status = status,
|
||||
Severity = severity,
|
||||
MinConfidence = minConfidence
|
||||
};
|
||||
|
||||
var result = await service.GetSummariesAsync(filter, ct);
|
||||
return TypedResults.Ok(result);
|
||||
})
|
||||
.WithName("GetFindingSummaries")
|
||||
.WithDescription("Get paginated list of finding summaries")
|
||||
.Produces<FindingSummaryPage>(200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Scanner.Reachability.MiniMap;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.WebService.Endpoints;
|
||||
|
||||
public static class ReachabilityMapEndpoints
|
||||
{
|
||||
public static void MapReachabilityMapEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/v1/findings")
|
||||
.WithTags("Reachability")
|
||||
.RequireAuthorization();
|
||||
|
||||
// GET /api/v1/findings/{findingId}/reachability-map
|
||||
group.MapGet("/{findingId:guid}/reachability-map", async Task<Results<Ok<ReachabilityMiniMap>, NotFound>> (
|
||||
Guid findingId,
|
||||
[FromQuery] int maxPaths = 10,
|
||||
IReachabilityMapService service,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var map = await service.GetMiniMapAsync(findingId, maxPaths, ct);
|
||||
return map is not null
|
||||
? TypedResults.Ok(map)
|
||||
: TypedResults.NotFound();
|
||||
})
|
||||
.WithName("GetReachabilityMiniMap")
|
||||
.WithDescription("Get condensed reachability visualization")
|
||||
.Produces<ReachabilityMiniMap>(200)
|
||||
.Produces(404);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for retrieving reachability mini-maps for findings.
|
||||
/// </summary>
|
||||
public interface IReachabilityMapService
|
||||
{
|
||||
Task<ReachabilityMiniMap?> GetMiniMapAsync(Guid findingId, int maxPaths, CancellationToken ct);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Scanner.Analyzers.Native.RuntimeCapture.Timeline;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.WebService.Endpoints;
|
||||
|
||||
public static class RuntimeTimelineEndpoints
|
||||
{
|
||||
public static void MapRuntimeTimelineEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/v1/findings")
|
||||
.WithTags("Runtime")
|
||||
.RequireAuthorization();
|
||||
|
||||
// GET /api/v1/findings/{findingId}/runtime-timeline
|
||||
group.MapGet("/{findingId:guid}/runtime-timeline", async Task<Results<Ok<RuntimeTimeline>, NotFound>> (
|
||||
Guid findingId,
|
||||
[FromQuery] DateTimeOffset? from,
|
||||
[FromQuery] DateTimeOffset? to,
|
||||
[FromQuery] int bucketHours = 1,
|
||||
IRuntimeTimelineService service,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var options = new TimelineOptions
|
||||
{
|
||||
WindowStart = from,
|
||||
WindowEnd = to,
|
||||
BucketSize = TimeSpan.FromHours(Math.Clamp(bucketHours, 1, 24))
|
||||
};
|
||||
|
||||
var timeline = await service.GetTimelineAsync(findingId, options, ct);
|
||||
return timeline is not null
|
||||
? TypedResults.Ok(timeline)
|
||||
: TypedResults.NotFound();
|
||||
})
|
||||
.WithName("GetRuntimeTimeline")
|
||||
.WithDescription("Get runtime corroboration timeline")
|
||||
.Produces<RuntimeTimeline>(200)
|
||||
.Produces(404);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for retrieving runtime timelines for findings.
|
||||
/// </summary>
|
||||
public interface IRuntimeTimelineService
|
||||
{
|
||||
Task<RuntimeTimeline?> GetTimelineAsync(Guid findingId, TimelineOptions options, CancellationToken ct);
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
using StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.WebService.Services;
|
||||
|
||||
public interface IEvidenceGraphBuilder
|
||||
{
|
||||
Task<EvidenceGraphResponse?> BuildAsync(Guid findingId, CancellationToken ct);
|
||||
}
|
||||
|
||||
public sealed class EvidenceGraphBuilder : IEvidenceGraphBuilder
|
||||
{
|
||||
private readonly IEvidenceRepository _evidenceRepo;
|
||||
private readonly IAttestationVerifier _attestationVerifier;
|
||||
|
||||
public EvidenceGraphBuilder(
|
||||
IEvidenceRepository evidenceRepo,
|
||||
IAttestationVerifier attestationVerifier)
|
||||
{
|
||||
_evidenceRepo = evidenceRepo;
|
||||
_attestationVerifier = attestationVerifier;
|
||||
}
|
||||
|
||||
public async Task<EvidenceGraphResponse?> BuildAsync(
|
||||
Guid findingId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var evidence = await _evidenceRepo.GetFullEvidenceAsync(findingId, ct);
|
||||
if (evidence is null)
|
||||
return null;
|
||||
|
||||
var nodes = new List<EvidenceNode>();
|
||||
var edges = new List<EvidenceEdge>();
|
||||
|
||||
// Build verdict node (root)
|
||||
var verdictNode = BuildVerdictNode(evidence.Verdict);
|
||||
nodes.Add(verdictNode);
|
||||
|
||||
// Build policy trace node
|
||||
if (evidence.PolicyTrace is not null)
|
||||
{
|
||||
var policyNode = await BuildPolicyNodeAsync(evidence.PolicyTrace, ct);
|
||||
nodes.Add(policyNode);
|
||||
edges.Add(new EvidenceEdge
|
||||
{
|
||||
From = policyNode.Id,
|
||||
To = verdictNode.Id,
|
||||
Relation = EvidenceRelation.DerivedFrom,
|
||||
Label = "policy evaluation"
|
||||
});
|
||||
}
|
||||
|
||||
// Build VEX nodes
|
||||
foreach (var vex in evidence.VexStatements)
|
||||
{
|
||||
var vexNode = await BuildVexNodeAsync(vex, ct);
|
||||
nodes.Add(vexNode);
|
||||
edges.Add(new EvidenceEdge
|
||||
{
|
||||
From = vexNode.Id,
|
||||
To = verdictNode.Id,
|
||||
Relation = EvidenceRelation.DerivedFrom,
|
||||
Label = vex.Status.ToLowerInvariant()
|
||||
});
|
||||
}
|
||||
|
||||
// Build reachability node
|
||||
if (evidence.Reachability is not null)
|
||||
{
|
||||
var reachNode = await BuildReachabilityNodeAsync(evidence.Reachability, ct);
|
||||
nodes.Add(reachNode);
|
||||
edges.Add(new EvidenceEdge
|
||||
{
|
||||
From = reachNode.Id,
|
||||
To = verdictNode.Id,
|
||||
Relation = EvidenceRelation.Corroborates,
|
||||
Label = "reachability analysis"
|
||||
});
|
||||
}
|
||||
|
||||
// Build runtime nodes
|
||||
foreach (var runtime in evidence.RuntimeObservations)
|
||||
{
|
||||
var runtimeNode = await BuildRuntimeNodeAsync(runtime, ct);
|
||||
nodes.Add(runtimeNode);
|
||||
edges.Add(new EvidenceEdge
|
||||
{
|
||||
From = runtimeNode.Id,
|
||||
To = verdictNode.Id,
|
||||
Relation = EvidenceRelation.Corroborates,
|
||||
Label = "runtime observation"
|
||||
});
|
||||
}
|
||||
|
||||
// Build SBOM node
|
||||
if (evidence.SbomComponent is not null)
|
||||
{
|
||||
var sbomNode = BuildSbomNode(evidence.SbomComponent);
|
||||
nodes.Add(sbomNode);
|
||||
edges.Add(new EvidenceEdge
|
||||
{
|
||||
From = sbomNode.Id,
|
||||
To = verdictNode.Id,
|
||||
Relation = EvidenceRelation.References,
|
||||
Label = "component"
|
||||
});
|
||||
}
|
||||
|
||||
// Build provenance node
|
||||
if (evidence.Provenance is not null)
|
||||
{
|
||||
var provNode = await BuildProvenanceNodeAsync(evidence.Provenance, ct);
|
||||
nodes.Add(provNode);
|
||||
edges.Add(new EvidenceEdge
|
||||
{
|
||||
From = provNode.Id,
|
||||
To = verdictNode.Id,
|
||||
Relation = EvidenceRelation.VerifiedBy,
|
||||
Label = "provenance"
|
||||
});
|
||||
}
|
||||
|
||||
return new EvidenceGraphResponse
|
||||
{
|
||||
FindingId = findingId,
|
||||
VulnerabilityId = evidence.VulnerabilityId,
|
||||
Nodes = nodes,
|
||||
Edges = edges,
|
||||
RootNodeId = verdictNode.Id,
|
||||
GeneratedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private EvidenceNode BuildVerdictNode(VerdictEvidence verdict)
|
||||
{
|
||||
return new EvidenceNode
|
||||
{
|
||||
Id = $"verdict:{verdict.Digest}",
|
||||
Type = EvidenceNodeType.Verdict,
|
||||
Label = $"Verdict: {verdict.Status}",
|
||||
Digest = verdict.Digest,
|
||||
Issuer = verdict.Issuer,
|
||||
Timestamp = verdict.Timestamp,
|
||||
Signature = new SignatureStatus { IsSigned = false },
|
||||
ContentUrl = $"/api/v1/evidence/{verdict.Digest}"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<EvidenceNode> BuildPolicyNodeAsync(
|
||||
PolicyTraceEvidence policy,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var signature = await VerifySignatureAsync(policy.AttestationDigest, ct);
|
||||
return new EvidenceNode
|
||||
{
|
||||
Id = $"policy:{policy.Digest}",
|
||||
Type = EvidenceNodeType.PolicyTrace,
|
||||
Label = $"Policy: {policy.PolicyName}",
|
||||
Digest = policy.Digest,
|
||||
Issuer = policy.Issuer,
|
||||
Timestamp = policy.Timestamp,
|
||||
Signature = signature,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["policyName"] = policy.PolicyName,
|
||||
["policyVersion"] = policy.PolicyVersion
|
||||
},
|
||||
ContentUrl = $"/api/v1/evidence/{policy.Digest}"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<EvidenceNode> BuildVexNodeAsync(
|
||||
VexEvidence vex,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var signature = await VerifySignatureAsync(vex.AttestationDigest, ct);
|
||||
return new EvidenceNode
|
||||
{
|
||||
Id = $"vex:{vex.Digest}",
|
||||
Type = EvidenceNodeType.VexStatement,
|
||||
Label = $"VEX: {vex.Status}",
|
||||
Digest = vex.Digest,
|
||||
Issuer = vex.Issuer,
|
||||
Timestamp = vex.Timestamp,
|
||||
Signature = signature,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["status"] = vex.Status,
|
||||
["justification"] = vex.Justification ?? string.Empty
|
||||
},
|
||||
ContentUrl = $"/api/v1/evidence/{vex.Digest}"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<EvidenceNode> BuildReachabilityNodeAsync(
|
||||
ReachabilityEvidence reach,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var signature = await VerifySignatureAsync(reach.AttestationDigest, ct);
|
||||
return new EvidenceNode
|
||||
{
|
||||
Id = $"reach:{reach.Digest}",
|
||||
Type = EvidenceNodeType.Reachability,
|
||||
Label = $"Reachability: {reach.State}",
|
||||
Digest = reach.Digest,
|
||||
Issuer = reach.Issuer,
|
||||
Timestamp = reach.Timestamp,
|
||||
Signature = signature,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["state"] = reach.State,
|
||||
["confidence"] = reach.Confidence.ToString("F2")
|
||||
},
|
||||
ContentUrl = $"/api/v1/evidence/{reach.Digest}"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<EvidenceNode> BuildRuntimeNodeAsync(
|
||||
RuntimeEvidence runtime,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var signature = await VerifySignatureAsync(runtime.AttestationDigest, ct);
|
||||
return new EvidenceNode
|
||||
{
|
||||
Id = $"runtime:{runtime.Digest}",
|
||||
Type = EvidenceNodeType.RuntimeObservation,
|
||||
Label = $"Runtime: {runtime.ObservationType}",
|
||||
Digest = runtime.Digest,
|
||||
Issuer = runtime.Issuer,
|
||||
Timestamp = runtime.Timestamp,
|
||||
Signature = signature,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["observationType"] = runtime.ObservationType,
|
||||
["durationMinutes"] = runtime.DurationMinutes.ToString()
|
||||
},
|
||||
ContentUrl = $"/api/v1/evidence/{runtime.Digest}"
|
||||
};
|
||||
}
|
||||
|
||||
private EvidenceNode BuildSbomNode(SbomComponentEvidence sbom)
|
||||
{
|
||||
return new EvidenceNode
|
||||
{
|
||||
Id = $"sbom:{sbom.Digest}",
|
||||
Type = EvidenceNodeType.SbomComponent,
|
||||
Label = $"Component: {sbom.ComponentName}",
|
||||
Digest = sbom.Digest,
|
||||
Issuer = sbom.Issuer,
|
||||
Timestamp = sbom.Timestamp,
|
||||
Signature = new SignatureStatus { IsSigned = false },
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["purl"] = sbom.Purl,
|
||||
["version"] = sbom.Version
|
||||
},
|
||||
ContentUrl = $"/api/v1/evidence/{sbom.Digest}"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<EvidenceNode> BuildProvenanceNodeAsync(
|
||||
ProvenanceEvidence prov,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var signature = await VerifySignatureAsync(prov.AttestationDigest, ct);
|
||||
return new EvidenceNode
|
||||
{
|
||||
Id = $"prov:{prov.Digest}",
|
||||
Type = EvidenceNodeType.Provenance,
|
||||
Label = $"Provenance: {prov.BuilderType}",
|
||||
Digest = prov.Digest,
|
||||
Issuer = prov.Issuer,
|
||||
Timestamp = prov.Timestamp,
|
||||
Signature = signature,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["builderType"] = prov.BuilderType,
|
||||
["repoUrl"] = prov.RepoUrl ?? string.Empty
|
||||
},
|
||||
ContentUrl = $"/api/v1/evidence/{prov.Digest}"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<SignatureStatus> VerifySignatureAsync(
|
||||
string? attestationDigest,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (attestationDigest is null)
|
||||
{
|
||||
return new SignatureStatus { IsSigned = false };
|
||||
}
|
||||
|
||||
var result = await _attestationVerifier.VerifyAsync(attestationDigest, ct);
|
||||
return new SignatureStatus
|
||||
{
|
||||
IsSigned = true,
|
||||
IsValid = result.IsValid,
|
||||
SignerIdentity = result.SignerIdentity,
|
||||
SignedAt = result.SignedAt,
|
||||
KeyId = result.KeyId,
|
||||
RekorLogIndex = result.RekorLogIndex
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Repository for evidence retrieval.
|
||||
/// </summary>
|
||||
public interface IEvidenceRepository
|
||||
{
|
||||
Task<FullEvidence?> GetFullEvidenceAsync(Guid findingId, CancellationToken ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for attestation verification.
|
||||
/// </summary>
|
||||
public interface IAttestationVerifier
|
||||
{
|
||||
Task<AttestationVerificationResult> VerifyAsync(string digest, CancellationToken ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full evidence bundle for a finding.
|
||||
/// </summary>
|
||||
public sealed record FullEvidence
|
||||
{
|
||||
public required string VulnerabilityId { get; init; }
|
||||
public required VerdictEvidence Verdict { get; init; }
|
||||
public PolicyTraceEvidence? PolicyTrace { get; init; }
|
||||
public IReadOnlyList<VexEvidence> VexStatements { get; init; } = Array.Empty<VexEvidence>();
|
||||
public ReachabilityEvidence? Reachability { get; init; }
|
||||
public IReadOnlyList<RuntimeEvidence> RuntimeObservations { get; init; } = Array.Empty<RuntimeEvidence>();
|
||||
public SbomComponentEvidence? SbomComponent { get; init; }
|
||||
public ProvenanceEvidence? Provenance { get; init; }
|
||||
}
|
||||
|
||||
public sealed record VerdictEvidence
|
||||
{
|
||||
public required string Status { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PolicyTraceEvidence
|
||||
{
|
||||
public required string PolicyName { get; init; }
|
||||
public required string PolicyVersion { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public string? AttestationDigest { get; init; }
|
||||
}
|
||||
|
||||
public sealed record VexEvidence
|
||||
{
|
||||
public required string Status { get; init; }
|
||||
public string? Justification { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public string? AttestationDigest { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ReachabilityEvidence
|
||||
{
|
||||
public required string State { get; init; }
|
||||
public required decimal Confidence { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public string? AttestationDigest { get; init; }
|
||||
}
|
||||
|
||||
public sealed record RuntimeEvidence
|
||||
{
|
||||
public required string ObservationType { get; init; }
|
||||
public required int DurationMinutes { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public string? AttestationDigest { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SbomComponentEvidence
|
||||
{
|
||||
public required string ComponentName { get; init; }
|
||||
public required string Purl { get; init; }
|
||||
public required string Version { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ProvenanceEvidence
|
||||
{
|
||||
public required string BuilderType { get; init; }
|
||||
public string? RepoUrl { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public string? AttestationDigest { get; init; }
|
||||
}
|
||||
|
||||
public sealed record AttestationVerificationResult
|
||||
{
|
||||
public required bool IsValid { get; init; }
|
||||
public string? SignerIdentity { get; init; }
|
||||
public DateTimeOffset? SignedAt { get; init; }
|
||||
public string? KeyId { get; init; }
|
||||
public long? RekorLogIndex { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.WebService.Services;
|
||||
|
||||
public interface IFindingSummaryBuilder
|
||||
{
|
||||
FindingSummary Build(FindingData finding);
|
||||
}
|
||||
|
||||
public sealed class FindingSummaryBuilder : IFindingSummaryBuilder
|
||||
{
|
||||
public FindingSummary Build(FindingData finding)
|
||||
{
|
||||
var status = DetermineStatus(finding);
|
||||
var chip = BuildChip(status, finding);
|
||||
var oneLiner = GenerateOneLiner(status, finding);
|
||||
var badges = ComputeBadges(finding);
|
||||
|
||||
return new FindingSummary
|
||||
{
|
||||
FindingId = finding.Id,
|
||||
VulnerabilityId = finding.VulnerabilityId,
|
||||
Title = finding.Title ?? finding.VulnerabilityId,
|
||||
Component = finding.ComponentPurl,
|
||||
Status = status,
|
||||
Chip = chip,
|
||||
Confidence = finding.Confidence,
|
||||
OneLiner = oneLiner,
|
||||
Badges = badges,
|
||||
CvssScore = finding.CvssScore,
|
||||
Severity = finding.Severity,
|
||||
FirstSeen = finding.FirstSeen,
|
||||
LastUpdated = finding.LastUpdated
|
||||
};
|
||||
}
|
||||
|
||||
private static VerdictStatus DetermineStatus(FindingData finding)
|
||||
{
|
||||
if (finding.IsMitigated)
|
||||
return VerdictStatus.Mitigated;
|
||||
|
||||
if (finding.IsAffected.HasValue)
|
||||
return finding.IsAffected.Value ? VerdictStatus.Affected : VerdictStatus.NotAffected;
|
||||
|
||||
return VerdictStatus.UnderReview;
|
||||
}
|
||||
|
||||
private static VerdictChip BuildChip(VerdictStatus status, FindingData finding)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
VerdictStatus.Affected => new VerdictChip
|
||||
{
|
||||
Label = "AFFECTED",
|
||||
Color = ChipColor.Red,
|
||||
Icon = "alert-circle"
|
||||
},
|
||||
VerdictStatus.NotAffected => new VerdictChip
|
||||
{
|
||||
Label = "NOT AFFECTED",
|
||||
Color = ChipColor.Green,
|
||||
Icon = "check-circle"
|
||||
},
|
||||
VerdictStatus.UnderReview => new VerdictChip
|
||||
{
|
||||
Label = "REVIEW NEEDED",
|
||||
Color = ChipColor.Yellow,
|
||||
Icon = "help-circle"
|
||||
},
|
||||
VerdictStatus.Mitigated => new VerdictChip
|
||||
{
|
||||
Label = "MITIGATED",
|
||||
Color = ChipColor.Blue,
|
||||
Icon = "shield-check"
|
||||
},
|
||||
_ => new VerdictChip
|
||||
{
|
||||
Label = "UNKNOWN",
|
||||
Color = ChipColor.Yellow,
|
||||
Icon = "question"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string GenerateOneLiner(VerdictStatus status, FindingData finding)
|
||||
{
|
||||
var componentName = ExtractComponentName(finding.ComponentPurl);
|
||||
|
||||
return status switch
|
||||
{
|
||||
VerdictStatus.Affected when finding.IsReachable == true =>
|
||||
$"Vulnerable code in {componentName} is reachable and actively used",
|
||||
|
||||
VerdictStatus.Affected when finding.IsReachable == false =>
|
||||
$"Vulnerability present in {componentName} but code is unreachable",
|
||||
|
||||
VerdictStatus.Affected =>
|
||||
$"Vulnerability affects {componentName} (reachability unknown)",
|
||||
|
||||
VerdictStatus.NotAffected when finding.HasRuntimeEvidence =>
|
||||
$"Component {componentName} not loaded during runtime observation",
|
||||
|
||||
VerdictStatus.NotAffected =>
|
||||
$"Vulnerability does not affect this version of {componentName}",
|
||||
|
||||
VerdictStatus.Mitigated when !string.IsNullOrEmpty(finding.MitigationReason) =>
|
||||
$"Mitigated: {finding.MitigationReason}",
|
||||
|
||||
VerdictStatus.Mitigated =>
|
||||
$"Vulnerability in {componentName} has been mitigated",
|
||||
|
||||
VerdictStatus.UnderReview when finding.Confidence < 0.5m =>
|
||||
$"Low confidence verdict for {componentName} requires manual review",
|
||||
|
||||
VerdictStatus.UnderReview =>
|
||||
$"Analysis incomplete for {componentName}",
|
||||
|
||||
_ => $"Status unknown for {componentName}"
|
||||
};
|
||||
}
|
||||
|
||||
private static ProofBadges ComputeBadges(FindingData finding)
|
||||
{
|
||||
var reachability = finding.IsReachable switch
|
||||
{
|
||||
true when finding.HasCallGraph => BadgeStatus.Strong,
|
||||
true => BadgeStatus.Partial,
|
||||
false when finding.HasCallGraph => BadgeStatus.Strong,
|
||||
_ => BadgeStatus.None
|
||||
};
|
||||
|
||||
var runtime = finding.HasRuntimeEvidence switch
|
||||
{
|
||||
true when finding.RuntimeConfirmed => BadgeStatus.Verified,
|
||||
true => BadgeStatus.Strong,
|
||||
_ => BadgeStatus.None
|
||||
};
|
||||
|
||||
var policy = finding.HasPolicyEvaluation switch
|
||||
{
|
||||
true when finding.PolicyPassed => BadgeStatus.Strong,
|
||||
true => BadgeStatus.Partial,
|
||||
_ => BadgeStatus.None
|
||||
};
|
||||
|
||||
var provenance = finding.HasAttestation switch
|
||||
{
|
||||
true when finding.AttestationVerified => BadgeStatus.Verified,
|
||||
true => BadgeStatus.Strong,
|
||||
_ => BadgeStatus.None
|
||||
};
|
||||
|
||||
return new ProofBadges
|
||||
{
|
||||
Reachability = reachability,
|
||||
Runtime = runtime,
|
||||
Policy = policy,
|
||||
Provenance = provenance
|
||||
};
|
||||
}
|
||||
|
||||
private static string ExtractComponentName(string purl)
|
||||
{
|
||||
var parts = purl.Split('/');
|
||||
var namePart = parts.LastOrDefault() ?? purl;
|
||||
return namePart.Split('@').FirstOrDefault() ?? namePart;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal finding data structure.
|
||||
/// </summary>
|
||||
public sealed record FindingData
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required string VulnerabilityId { get; init; }
|
||||
public string? Title { get; init; }
|
||||
public required string ComponentPurl { get; init; }
|
||||
public bool? IsAffected { get; init; }
|
||||
public bool IsMitigated { get; init; }
|
||||
public string? MitigationReason { get; init; }
|
||||
public required decimal Confidence { get; init; }
|
||||
public bool? IsReachable { get; init; }
|
||||
public bool HasCallGraph { get; init; }
|
||||
public bool HasRuntimeEvidence { get; init; }
|
||||
public bool RuntimeConfirmed { get; init; }
|
||||
public bool HasPolicyEvaluation { get; init; }
|
||||
public bool PolicyPassed { get; init; }
|
||||
public bool HasAttestation { get; init; }
|
||||
public bool AttestationVerified { get; init; }
|
||||
public decimal? CvssScore { get; init; }
|
||||
public string? Severity { get; init; }
|
||||
public required DateTimeOffset FirstSeen { get; init; }
|
||||
public required DateTimeOffset LastUpdated { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.WebService.Services;
|
||||
|
||||
public interface IFindingSummaryService
|
||||
{
|
||||
Task<FindingSummary?> GetSummaryAsync(Guid findingId, CancellationToken ct);
|
||||
Task<FindingSummaryPage> GetSummariesAsync(FindingSummaryFilter filter, CancellationToken ct);
|
||||
}
|
||||
|
||||
public sealed class FindingSummaryService : IFindingSummaryService
|
||||
{
|
||||
private readonly IFindingSummaryBuilder _builder;
|
||||
private readonly IFindingRepository _repository;
|
||||
|
||||
public FindingSummaryService(
|
||||
IFindingSummaryBuilder builder,
|
||||
IFindingRepository repository)
|
||||
{
|
||||
_builder = builder;
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<FindingSummary?> GetSummaryAsync(Guid findingId, CancellationToken ct)
|
||||
{
|
||||
var finding = await _repository.GetByIdAsync(findingId, ct);
|
||||
if (finding is null)
|
||||
return null;
|
||||
|
||||
return _builder.Build(finding);
|
||||
}
|
||||
|
||||
public async Task<FindingSummaryPage> GetSummariesAsync(FindingSummaryFilter filter, CancellationToken ct)
|
||||
{
|
||||
var (findings, totalCount) = await _repository.GetPagedAsync(
|
||||
page: filter.Page,
|
||||
pageSize: filter.PageSize,
|
||||
status: filter.Status,
|
||||
severity: filter.Severity,
|
||||
minConfidence: filter.MinConfidence,
|
||||
ct);
|
||||
|
||||
var summaries = findings.Select(f => _builder.Build(f)).ToList();
|
||||
|
||||
return new FindingSummaryPage
|
||||
{
|
||||
Items = summaries,
|
||||
TotalCount = totalCount,
|
||||
Page = filter.Page,
|
||||
PageSize = filter.PageSize
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Repository for finding data access.
|
||||
/// </summary>
|
||||
public interface IFindingRepository
|
||||
{
|
||||
Task<FindingData?> GetByIdAsync(Guid id, CancellationToken ct);
|
||||
Task<(IReadOnlyList<FindingData> findings, int totalCount)> GetPagedAsync(
|
||||
int page,
|
||||
int pageSize,
|
||||
string? status,
|
||||
string? severity,
|
||||
decimal? minConfidence,
|
||||
CancellationToken ct);
|
||||
}
|
||||
@@ -19,6 +19,8 @@
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Configuration\StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="..\..\Telemetry\StellaOps.Telemetry.Core\StellaOps.Telemetry.Core\StellaOps.Telemetry.Core.csproj" />
|
||||
<ProjectReference Include="..\..\Scanner\__Libraries\StellaOps.Scanner.Reachability\StellaOps.Scanner.Reachability.csproj" />
|
||||
<ProjectReference Include="..\..\Scanner\__Libraries\StellaOps.Scanner.Analyzers.Native\StellaOps.Scanner.Analyzers.Native.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user