using StellaOps.Findings.Ledger.WebService.Contracts; namespace StellaOps.Findings.Ledger.WebService.Services; public interface IFindingSummaryService { Task GetSummaryAsync(Guid findingId, CancellationToken ct); Task 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 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 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(); var sorted = ApplySort(summaries, filter.SortBy, filter.SortDirection); return new FindingSummaryPage { Items = sorted, TotalCount = totalCount, Page = filter.Page, PageSize = filter.PageSize }; } private static IReadOnlyList ApplySort( List summaries, string? sortBy, string sortDirection) { if (string.IsNullOrEmpty(sortBy)) return summaries; var descending = string.Equals(sortDirection, "desc", StringComparison.OrdinalIgnoreCase); IEnumerable ordered = sortBy.ToLowerInvariant() switch { "cvss" => descending ? summaries.OrderByDescending(s => s.CvssScore ?? 0m) : summaries.OrderBy(s => s.CvssScore ?? 0m), "severity" => descending ? summaries.OrderByDescending(s => s.Severity) : summaries.OrderBy(s => s.Severity), "status" => descending ? summaries.OrderByDescending(s => s.Status) : summaries.OrderBy(s => s.Status), "component" => descending ? summaries.OrderByDescending(s => s.Component) : summaries.OrderBy(s => s.Component), "firstseen" => descending ? summaries.OrderByDescending(s => s.FirstSeen) : summaries.OrderBy(s => s.FirstSeen), "lastupdated" => descending ? summaries.OrderByDescending(s => s.LastUpdated) : summaries.OrderBy(s => s.LastUpdated), _ => summaries }; return ordered.ToList(); } } /// /// Repository for finding data access. /// public interface IFindingRepository { Task GetByIdAsync(Guid id, CancellationToken ct); Task<(IReadOnlyList findings, int totalCount)> GetPagedAsync( int page, int pageSize, string? status, string? severity, decimal? minConfidence, CancellationToken ct); }