Implement VEX document verification system with issuer management and signature verification
- Added IIssuerDirectory interface for managing VEX document issuers, including methods for registration, revocation, and trust validation. - Created InMemoryIssuerDirectory class as an in-memory implementation of IIssuerDirectory for testing and single-instance deployments. - Introduced ISignatureVerifier interface for verifying signatures on VEX documents, with support for multiple signature formats. - Developed SignatureVerifier class as the default implementation of ISignatureVerifier, allowing extensibility for different signature formats. - Implemented handlers for DSSE and JWS signature formats, including methods for verification and signature extraction. - Defined various records and enums for issuer and signature metadata, enhancing the structure and clarity of the verification process.
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Policy.Engine.Attestation;
|
||||
|
||||
namespace StellaOps.Policy.Engine.ConsoleSurface;
|
||||
|
||||
/// <summary>
|
||||
/// Console request for attestation report query per CONTRACT-VERIFICATION-POLICY-006.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleAttestationReportRequest(
|
||||
[property: JsonPropertyName("artifact_digests")] IReadOnlyList<string>? ArtifactDigests,
|
||||
[property: JsonPropertyName("artifact_uri_pattern")] string? ArtifactUriPattern,
|
||||
[property: JsonPropertyName("policy_ids")] IReadOnlyList<string>? PolicyIds,
|
||||
[property: JsonPropertyName("predicate_types")] IReadOnlyList<string>? PredicateTypes,
|
||||
[property: JsonPropertyName("status_filter")] IReadOnlyList<string>? StatusFilter,
|
||||
[property: JsonPropertyName("from_time")] DateTimeOffset? FromTime,
|
||||
[property: JsonPropertyName("to_time")] DateTimeOffset? ToTime,
|
||||
[property: JsonPropertyName("group_by")] ConsoleReportGroupBy? GroupBy,
|
||||
[property: JsonPropertyName("sort_by")] ConsoleReportSortBy? SortBy,
|
||||
[property: JsonPropertyName("page")] int Page = 1,
|
||||
[property: JsonPropertyName("page_size")] int PageSize = 25);
|
||||
|
||||
/// <summary>
|
||||
/// Grouping options for Console attestation reports.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<ConsoleReportGroupBy>))]
|
||||
internal enum ConsoleReportGroupBy
|
||||
{
|
||||
None,
|
||||
Policy,
|
||||
PredicateType,
|
||||
Status,
|
||||
ArtifactUri
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorting options for Console attestation reports.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<ConsoleReportSortBy>))]
|
||||
internal enum ConsoleReportSortBy
|
||||
{
|
||||
EvaluatedAtDesc,
|
||||
EvaluatedAtAsc,
|
||||
StatusAsc,
|
||||
StatusDesc,
|
||||
CoverageDesc,
|
||||
CoverageAsc
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Console response for attestation reports.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleAttestationReportResponse(
|
||||
[property: JsonPropertyName("schema_version")] string SchemaVersion,
|
||||
[property: JsonPropertyName("summary")] ConsoleReportSummary Summary,
|
||||
[property: JsonPropertyName("reports")] IReadOnlyList<ConsoleArtifactReport> Reports,
|
||||
[property: JsonPropertyName("groups")] IReadOnlyList<ConsoleReportGroup>? Groups,
|
||||
[property: JsonPropertyName("pagination")] ConsolePagination Pagination,
|
||||
[property: JsonPropertyName("filters_applied")] ConsoleFiltersApplied FiltersApplied);
|
||||
|
||||
/// <summary>
|
||||
/// Summary of attestation reports for Console.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleReportSummary(
|
||||
[property: JsonPropertyName("total_artifacts")] int TotalArtifacts,
|
||||
[property: JsonPropertyName("total_attestations")] int TotalAttestations,
|
||||
[property: JsonPropertyName("status_breakdown")] ImmutableDictionary<string, int> StatusBreakdown,
|
||||
[property: JsonPropertyName("coverage_rate")] double CoverageRate,
|
||||
[property: JsonPropertyName("compliance_rate")] double ComplianceRate,
|
||||
[property: JsonPropertyName("average_age_hours")] double AverageAgeHours);
|
||||
|
||||
/// <summary>
|
||||
/// Console-friendly artifact attestation report.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleArtifactReport(
|
||||
[property: JsonPropertyName("artifact_digest")] string ArtifactDigest,
|
||||
[property: JsonPropertyName("artifact_uri")] string? ArtifactUri,
|
||||
[property: JsonPropertyName("artifact_short_digest")] string ArtifactShortDigest,
|
||||
[property: JsonPropertyName("status")] string Status,
|
||||
[property: JsonPropertyName("status_label")] string StatusLabel,
|
||||
[property: JsonPropertyName("status_icon")] string StatusIcon,
|
||||
[property: JsonPropertyName("attestation_count")] int AttestationCount,
|
||||
[property: JsonPropertyName("coverage_percentage")] double CoveragePercentage,
|
||||
[property: JsonPropertyName("policies_passed")] int PoliciesPassed,
|
||||
[property: JsonPropertyName("policies_failed")] int PoliciesFailed,
|
||||
[property: JsonPropertyName("evaluated_at")] DateTimeOffset EvaluatedAt,
|
||||
[property: JsonPropertyName("evaluated_at_relative")] string EvaluatedAtRelative,
|
||||
[property: JsonPropertyName("details")] ConsoleReportDetails? Details);
|
||||
|
||||
/// <summary>
|
||||
/// Detailed report information for Console.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleReportDetails(
|
||||
[property: JsonPropertyName("predicate_types")] IReadOnlyList<ConsolePredicateTypeStatus> PredicateTypes,
|
||||
[property: JsonPropertyName("policies")] IReadOnlyList<ConsolePolicyStatus> Policies,
|
||||
[property: JsonPropertyName("signers")] IReadOnlyList<ConsoleSignerInfo> Signers,
|
||||
[property: JsonPropertyName("issues")] IReadOnlyList<ConsoleIssue> Issues);
|
||||
|
||||
/// <summary>
|
||||
/// Predicate type status for Console.
|
||||
/// </summary>
|
||||
internal sealed record ConsolePredicateTypeStatus(
|
||||
[property: JsonPropertyName("type")] string Type,
|
||||
[property: JsonPropertyName("type_label")] string TypeLabel,
|
||||
[property: JsonPropertyName("status")] string Status,
|
||||
[property: JsonPropertyName("status_label")] string StatusLabel,
|
||||
[property: JsonPropertyName("freshness")] string Freshness);
|
||||
|
||||
/// <summary>
|
||||
/// Policy status for Console.
|
||||
/// </summary>
|
||||
internal sealed record ConsolePolicyStatus(
|
||||
[property: JsonPropertyName("policy_id")] string PolicyId,
|
||||
[property: JsonPropertyName("policy_version")] string PolicyVersion,
|
||||
[property: JsonPropertyName("status")] string Status,
|
||||
[property: JsonPropertyName("status_label")] string StatusLabel,
|
||||
[property: JsonPropertyName("verdict")] string Verdict);
|
||||
|
||||
/// <summary>
|
||||
/// Signer information for Console.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleSignerInfo(
|
||||
[property: JsonPropertyName("key_fingerprint_short")] string KeyFingerprintShort,
|
||||
[property: JsonPropertyName("issuer")] string? Issuer,
|
||||
[property: JsonPropertyName("subject")] string? Subject,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm,
|
||||
[property: JsonPropertyName("verified")] bool Verified,
|
||||
[property: JsonPropertyName("trusted")] bool Trusted);
|
||||
|
||||
/// <summary>
|
||||
/// Issue for Console display.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleIssue(
|
||||
[property: JsonPropertyName("severity")] string Severity,
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("field")] string? Field);
|
||||
|
||||
/// <summary>
|
||||
/// Report group for Console.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleReportGroup(
|
||||
[property: JsonPropertyName("key")] string Key,
|
||||
[property: JsonPropertyName("label")] string Label,
|
||||
[property: JsonPropertyName("count")] int Count,
|
||||
[property: JsonPropertyName("status_breakdown")] ImmutableDictionary<string, int> StatusBreakdown);
|
||||
|
||||
/// <summary>
|
||||
/// Pagination information for Console.
|
||||
/// </summary>
|
||||
internal sealed record ConsolePagination(
|
||||
[property: JsonPropertyName("page")] int Page,
|
||||
[property: JsonPropertyName("page_size")] int PageSize,
|
||||
[property: JsonPropertyName("total_pages")] int TotalPages,
|
||||
[property: JsonPropertyName("total_items")] int TotalItems,
|
||||
[property: JsonPropertyName("has_next")] bool HasNext,
|
||||
[property: JsonPropertyName("has_previous")] bool HasPrevious);
|
||||
|
||||
/// <summary>
|
||||
/// Applied filters information for Console.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleFiltersApplied(
|
||||
[property: JsonPropertyName("artifact_count")] int ArtifactCount,
|
||||
[property: JsonPropertyName("policy_ids")] IReadOnlyList<string>? PolicyIds,
|
||||
[property: JsonPropertyName("predicate_types")] IReadOnlyList<string>? PredicateTypes,
|
||||
[property: JsonPropertyName("status_filter")] IReadOnlyList<string>? StatusFilter,
|
||||
[property: JsonPropertyName("time_range")] ConsoleTimeRange? TimeRange);
|
||||
|
||||
/// <summary>
|
||||
/// Time range for Console filters.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleTimeRange(
|
||||
[property: JsonPropertyName("from")] DateTimeOffset? From,
|
||||
[property: JsonPropertyName("to")] DateTimeOffset? To);
|
||||
|
||||
/// <summary>
|
||||
/// Console request for attestation statistics dashboard.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleAttestationDashboardRequest(
|
||||
[property: JsonPropertyName("time_range")] string? TimeRange,
|
||||
[property: JsonPropertyName("policy_ids")] IReadOnlyList<string>? PolicyIds,
|
||||
[property: JsonPropertyName("artifact_uri_pattern")] string? ArtifactUriPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Console response for attestation statistics dashboard.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleAttestationDashboardResponse(
|
||||
[property: JsonPropertyName("schema_version")] string SchemaVersion,
|
||||
[property: JsonPropertyName("overview")] ConsoleDashboardOverview Overview,
|
||||
[property: JsonPropertyName("trends")] ConsoleDashboardTrends Trends,
|
||||
[property: JsonPropertyName("top_issues")] IReadOnlyList<ConsoleDashboardIssue> TopIssues,
|
||||
[property: JsonPropertyName("policy_compliance")] IReadOnlyList<ConsoleDashboardPolicyCompliance> PolicyCompliance,
|
||||
[property: JsonPropertyName("evaluated_at")] DateTimeOffset EvaluatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Dashboard overview for Console.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleDashboardOverview(
|
||||
[property: JsonPropertyName("total_artifacts")] int TotalArtifacts,
|
||||
[property: JsonPropertyName("total_attestations")] int TotalAttestations,
|
||||
[property: JsonPropertyName("pass_rate")] double PassRate,
|
||||
[property: JsonPropertyName("coverage_rate")] double CoverageRate,
|
||||
[property: JsonPropertyName("average_freshness_hours")] double AverageFreshnessHours);
|
||||
|
||||
/// <summary>
|
||||
/// Dashboard trends for Console.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleDashboardTrends(
|
||||
[property: JsonPropertyName("pass_rate_change")] double PassRateChange,
|
||||
[property: JsonPropertyName("coverage_rate_change")] double CoverageRateChange,
|
||||
[property: JsonPropertyName("attestation_count_change")] int AttestationCountChange,
|
||||
[property: JsonPropertyName("trend_direction")] string TrendDirection);
|
||||
|
||||
/// <summary>
|
||||
/// Dashboard issue for Console.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleDashboardIssue(
|
||||
[property: JsonPropertyName("issue")] string Issue,
|
||||
[property: JsonPropertyName("count")] int Count,
|
||||
[property: JsonPropertyName("severity")] string Severity);
|
||||
|
||||
/// <summary>
|
||||
/// Dashboard policy compliance for Console.
|
||||
/// </summary>
|
||||
internal sealed record ConsoleDashboardPolicyCompliance(
|
||||
[property: JsonPropertyName("policy_id")] string PolicyId,
|
||||
[property: JsonPropertyName("policy_version")] string PolicyVersion,
|
||||
[property: JsonPropertyName("compliance_rate")] double ComplianceRate,
|
||||
[property: JsonPropertyName("artifacts_evaluated")] int ArtifactsEvaluated);
|
||||
@@ -0,0 +1,470 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Policy.Engine.Attestation;
|
||||
|
||||
namespace StellaOps.Policy.Engine.ConsoleSurface;
|
||||
|
||||
/// <summary>
|
||||
/// Service for Console attestation report integration per CONTRACT-VERIFICATION-POLICY-006.
|
||||
/// </summary>
|
||||
internal sealed class ConsoleAttestationReportService
|
||||
{
|
||||
private const string SchemaVersion = "1.0.0";
|
||||
|
||||
private readonly IAttestationReportService _reportService;
|
||||
private readonly IVerificationPolicyStore _policyStore;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public ConsoleAttestationReportService(
|
||||
IAttestationReportService reportService,
|
||||
IVerificationPolicyStore policyStore,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_reportService = reportService ?? throw new ArgumentNullException(nameof(reportService));
|
||||
_policyStore = policyStore ?? throw new ArgumentNullException(nameof(policyStore));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public async Task<ConsoleAttestationReportResponse> QueryReportsAsync(
|
||||
ConsoleAttestationReportRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
// Convert Console request to internal query
|
||||
var query = new AttestationReportQuery(
|
||||
ArtifactDigests: request.ArtifactDigests,
|
||||
ArtifactUriPattern: request.ArtifactUriPattern,
|
||||
PolicyIds: request.PolicyIds,
|
||||
PredicateTypes: request.PredicateTypes,
|
||||
StatusFilter: ParseStatusFilter(request.StatusFilter),
|
||||
FromTime: request.FromTime,
|
||||
ToTime: request.ToTime,
|
||||
IncludeDetails: true,
|
||||
Limit: request.PageSize,
|
||||
Offset: (request.Page - 1) * request.PageSize);
|
||||
|
||||
// Get reports
|
||||
var response = await _reportService.ListReportsAsync(query, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Get statistics for summary
|
||||
var statistics = await _reportService.GetStatisticsAsync(query, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Convert to Console format
|
||||
var consoleReports = response.Reports.Select(r => ToConsoleReport(r, now)).ToList();
|
||||
|
||||
// Calculate groups if requested
|
||||
IReadOnlyList<ConsoleReportGroup>? groups = null;
|
||||
if (request.GroupBy.HasValue && request.GroupBy.Value != ConsoleReportGroupBy.None)
|
||||
{
|
||||
groups = CalculateGroups(response.Reports, request.GroupBy.Value);
|
||||
}
|
||||
|
||||
// Calculate pagination
|
||||
var totalPages = (int)Math.Ceiling((double)response.Total / request.PageSize);
|
||||
var pagination = new ConsolePagination(
|
||||
Page: request.Page,
|
||||
PageSize: request.PageSize,
|
||||
TotalPages: totalPages,
|
||||
TotalItems: response.Total,
|
||||
HasNext: request.Page < totalPages,
|
||||
HasPrevious: request.Page > 1);
|
||||
|
||||
// Create summary
|
||||
var summary = new ConsoleReportSummary(
|
||||
TotalArtifacts: statistics.TotalArtifacts,
|
||||
TotalAttestations: statistics.TotalAttestations,
|
||||
StatusBreakdown: statistics.StatusDistribution
|
||||
.ToImmutableDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value),
|
||||
CoverageRate: Math.Round(statistics.CoverageRate, 2),
|
||||
ComplianceRate: CalculateComplianceRate(response.Reports),
|
||||
AverageAgeHours: Math.Round(statistics.AverageAgeSeconds / 3600, 2));
|
||||
|
||||
return new ConsoleAttestationReportResponse(
|
||||
SchemaVersion: SchemaVersion,
|
||||
Summary: summary,
|
||||
Reports: consoleReports,
|
||||
Groups: groups,
|
||||
Pagination: pagination,
|
||||
FiltersApplied: new ConsoleFiltersApplied(
|
||||
ArtifactCount: request.ArtifactDigests?.Count ?? 0,
|
||||
PolicyIds: request.PolicyIds,
|
||||
PredicateTypes: request.PredicateTypes,
|
||||
StatusFilter: request.StatusFilter,
|
||||
TimeRange: request.FromTime.HasValue || request.ToTime.HasValue
|
||||
? new ConsoleTimeRange(request.FromTime, request.ToTime)
|
||||
: null));
|
||||
}
|
||||
|
||||
public async Task<ConsoleAttestationDashboardResponse> GetDashboardAsync(
|
||||
ConsoleAttestationDashboardRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var (fromTime, toTime) = ParseTimeRange(request.TimeRange, now);
|
||||
|
||||
var query = new AttestationReportQuery(
|
||||
ArtifactDigests: null,
|
||||
ArtifactUriPattern: request.ArtifactUriPattern,
|
||||
PolicyIds: request.PolicyIds,
|
||||
PredicateTypes: null,
|
||||
StatusFilter: null,
|
||||
FromTime: fromTime,
|
||||
ToTime: toTime,
|
||||
IncludeDetails: false,
|
||||
Limit: int.MaxValue,
|
||||
Offset: 0);
|
||||
|
||||
var statistics = await _reportService.GetStatisticsAsync(query, cancellationToken).ConfigureAwait(false);
|
||||
var reports = await _reportService.ListReportsAsync(query, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Calculate pass rate
|
||||
var passCount = statistics.StatusDistribution.GetValueOrDefault(AttestationReportStatus.Pass, 0);
|
||||
var failCount = statistics.StatusDistribution.GetValueOrDefault(AttestationReportStatus.Fail, 0);
|
||||
var warnCount = statistics.StatusDistribution.GetValueOrDefault(AttestationReportStatus.Warn, 0);
|
||||
var total = passCount + failCount + warnCount;
|
||||
var passRate = total > 0 ? (double)passCount / total * 100 : 0;
|
||||
|
||||
// Calculate overview
|
||||
var overview = new ConsoleDashboardOverview(
|
||||
TotalArtifacts: statistics.TotalArtifacts,
|
||||
TotalAttestations: statistics.TotalAttestations,
|
||||
PassRate: Math.Round(passRate, 2),
|
||||
CoverageRate: Math.Round(statistics.CoverageRate, 2),
|
||||
AverageFreshnessHours: Math.Round(statistics.AverageAgeSeconds / 3600, 2));
|
||||
|
||||
// Calculate trends (simplified - would normally compare to previous period)
|
||||
var trends = new ConsoleDashboardTrends(
|
||||
PassRateChange: 0,
|
||||
CoverageRateChange: 0,
|
||||
AttestationCountChange: 0,
|
||||
TrendDirection: "stable");
|
||||
|
||||
// Get top issues
|
||||
var topIssues = reports.Reports
|
||||
.SelectMany(r => r.VerificationResults)
|
||||
.SelectMany(v => v.Issues)
|
||||
.GroupBy(i => i)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.Take(5)
|
||||
.Select(g => new ConsoleDashboardIssue(
|
||||
Issue: g.Key,
|
||||
Count: g.Count(),
|
||||
Severity: "error"))
|
||||
.ToList();
|
||||
|
||||
// Get policy compliance
|
||||
var policyCompliance = await CalculatePolicyComplianceAsync(reports.Reports, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new ConsoleAttestationDashboardResponse(
|
||||
SchemaVersion: SchemaVersion,
|
||||
Overview: overview,
|
||||
Trends: trends,
|
||||
TopIssues: topIssues,
|
||||
PolicyCompliance: policyCompliance,
|
||||
EvaluatedAt: now);
|
||||
}
|
||||
|
||||
private ConsoleArtifactReport ToConsoleReport(ArtifactAttestationReport report, DateTimeOffset now)
|
||||
{
|
||||
var age = now - report.EvaluatedAt;
|
||||
var ageRelative = FormatRelativeTime(age);
|
||||
|
||||
return new ConsoleArtifactReport(
|
||||
ArtifactDigest: report.ArtifactDigest,
|
||||
ArtifactUri: report.ArtifactUri,
|
||||
ArtifactShortDigest: report.ArtifactDigest.Length > 12
|
||||
? report.ArtifactDigest[..12]
|
||||
: report.ArtifactDigest,
|
||||
Status: report.OverallStatus.ToString().ToLowerInvariant(),
|
||||
StatusLabel: GetStatusLabel(report.OverallStatus),
|
||||
StatusIcon: GetStatusIcon(report.OverallStatus),
|
||||
AttestationCount: report.AttestationCount,
|
||||
CoveragePercentage: report.Coverage.CoveragePercentage,
|
||||
PoliciesPassed: report.PolicyCompliance.PoliciesPassed,
|
||||
PoliciesFailed: report.PolicyCompliance.PoliciesFailed,
|
||||
EvaluatedAt: report.EvaluatedAt,
|
||||
EvaluatedAtRelative: ageRelative,
|
||||
Details: ToConsoleDetails(report));
|
||||
}
|
||||
|
||||
private static ConsoleReportDetails ToConsoleDetails(ArtifactAttestationReport report)
|
||||
{
|
||||
var predicateTypes = report.VerificationResults
|
||||
.GroupBy(v => v.PredicateType)
|
||||
.Select(g => new ConsolePredicateTypeStatus(
|
||||
Type: g.Key,
|
||||
TypeLabel: GetPredicateTypeLabel(g.Key),
|
||||
Status: g.First().Status.ToString().ToLowerInvariant(),
|
||||
StatusLabel: GetStatusLabel(g.First().Status),
|
||||
Freshness: FormatFreshness(g.First().FreshnessStatus)))
|
||||
.ToList();
|
||||
|
||||
var policies = report.PolicyCompliance.PolicyResults
|
||||
.Select(p => new ConsolePolicyStatus(
|
||||
PolicyId: p.PolicyId,
|
||||
PolicyVersion: p.PolicyVersion,
|
||||
Status: p.Status.ToString().ToLowerInvariant(),
|
||||
StatusLabel: GetStatusLabel(p.Status),
|
||||
Verdict: p.Verdict))
|
||||
.ToList();
|
||||
|
||||
var signers = report.VerificationResults
|
||||
.SelectMany(v => v.SignatureStatus.Signers)
|
||||
.DistinctBy(s => s.KeyFingerprint)
|
||||
.Select(s => new ConsoleSignerInfo(
|
||||
KeyFingerprintShort: s.KeyFingerprint.Length > 8
|
||||
? s.KeyFingerprint[..8]
|
||||
: s.KeyFingerprint,
|
||||
Issuer: s.Issuer,
|
||||
Subject: s.Subject,
|
||||
Algorithm: s.Algorithm,
|
||||
Verified: s.Verified,
|
||||
Trusted: s.Trusted))
|
||||
.ToList();
|
||||
|
||||
var issues = report.VerificationResults
|
||||
.SelectMany(v => v.Issues)
|
||||
.Distinct()
|
||||
.Select(i => new ConsoleIssue(
|
||||
Severity: "error",
|
||||
Message: i,
|
||||
Field: null))
|
||||
.ToList();
|
||||
|
||||
return new ConsoleReportDetails(
|
||||
PredicateTypes: predicateTypes,
|
||||
Policies: policies,
|
||||
Signers: signers,
|
||||
Issues: issues);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ConsoleReportGroup> CalculateGroups(
|
||||
IReadOnlyList<ArtifactAttestationReport> reports,
|
||||
ConsoleReportGroupBy groupBy)
|
||||
{
|
||||
return groupBy switch
|
||||
{
|
||||
ConsoleReportGroupBy.Policy => GroupByPolicy(reports),
|
||||
ConsoleReportGroupBy.PredicateType => GroupByPredicateType(reports),
|
||||
ConsoleReportGroupBy.Status => GroupByStatus(reports),
|
||||
ConsoleReportGroupBy.ArtifactUri => GroupByArtifactUri(reports),
|
||||
_ => []
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ConsoleReportGroup> GroupByPolicy(IReadOnlyList<ArtifactAttestationReport> reports)
|
||||
{
|
||||
return reports
|
||||
.SelectMany(r => r.PolicyCompliance.PolicyResults)
|
||||
.GroupBy(p => p.PolicyId)
|
||||
.Select(g => new ConsoleReportGroup(
|
||||
Key: g.Key,
|
||||
Label: g.Key,
|
||||
Count: g.Count(),
|
||||
StatusBreakdown: g.GroupBy(p => p.Status.ToString())
|
||||
.ToImmutableDictionary(s => s.Key, s => s.Count())))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ConsoleReportGroup> GroupByPredicateType(IReadOnlyList<ArtifactAttestationReport> reports)
|
||||
{
|
||||
return reports
|
||||
.SelectMany(r => r.VerificationResults)
|
||||
.GroupBy(v => v.PredicateType)
|
||||
.Select(g => new ConsoleReportGroup(
|
||||
Key: g.Key,
|
||||
Label: GetPredicateTypeLabel(g.Key),
|
||||
Count: g.Count(),
|
||||
StatusBreakdown: g.GroupBy(v => v.Status.ToString())
|
||||
.ToImmutableDictionary(s => s.Key, s => s.Count())))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ConsoleReportGroup> GroupByStatus(IReadOnlyList<ArtifactAttestationReport> reports)
|
||||
{
|
||||
return reports
|
||||
.GroupBy(r => r.OverallStatus)
|
||||
.Select(g => new ConsoleReportGroup(
|
||||
Key: g.Key.ToString(),
|
||||
Label: GetStatusLabel(g.Key),
|
||||
Count: g.Count(),
|
||||
StatusBreakdown: ImmutableDictionary<string, int>.Empty.Add(g.Key.ToString(), g.Count())))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ConsoleReportGroup> GroupByArtifactUri(IReadOnlyList<ArtifactAttestationReport> reports)
|
||||
{
|
||||
return reports
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r.ArtifactUri))
|
||||
.GroupBy(r => ExtractRepository(r.ArtifactUri!))
|
||||
.Select(g => new ConsoleReportGroup(
|
||||
Key: g.Key,
|
||||
Label: g.Key,
|
||||
Count: g.Count(),
|
||||
StatusBreakdown: g.GroupBy(r => r.OverallStatus.ToString())
|
||||
.ToImmutableDictionary(s => s.Key, s => s.Count())))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<ConsoleDashboardPolicyCompliance>> CalculatePolicyComplianceAsync(
|
||||
IReadOnlyList<ArtifactAttestationReport> reports,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var policyResults = reports
|
||||
.SelectMany(r => r.PolicyCompliance.PolicyResults)
|
||||
.GroupBy(p => p.PolicyId)
|
||||
.Select(g =>
|
||||
{
|
||||
var total = g.Count();
|
||||
var passed = g.Count(p => p.Status == AttestationReportStatus.Pass);
|
||||
var complianceRate = total > 0 ? (double)passed / total * 100 : 0;
|
||||
|
||||
return new ConsoleDashboardPolicyCompliance(
|
||||
PolicyId: g.Key,
|
||||
PolicyVersion: g.First().PolicyVersion,
|
||||
ComplianceRate: Math.Round(complianceRate, 2),
|
||||
ArtifactsEvaluated: total);
|
||||
})
|
||||
.OrderByDescending(p => p.ArtifactsEvaluated)
|
||||
.Take(10)
|
||||
.ToList();
|
||||
|
||||
return policyResults;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AttestationReportStatus>? ParseStatusFilter(IReadOnlyList<string>? statusFilter)
|
||||
{
|
||||
if (statusFilter == null || statusFilter.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return statusFilter
|
||||
.Select(s => Enum.TryParse<AttestationReportStatus>(s, true, out var status) ? status : (AttestationReportStatus?)null)
|
||||
.Where(s => s.HasValue)
|
||||
.Select(s => s!.Value)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static (DateTimeOffset? from, DateTimeOffset? to) ParseTimeRange(string? timeRange, DateTimeOffset now)
|
||||
{
|
||||
return timeRange?.ToLowerInvariant() switch
|
||||
{
|
||||
"1h" => (now.AddHours(-1), now),
|
||||
"24h" => (now.AddDays(-1), now),
|
||||
"7d" => (now.AddDays(-7), now),
|
||||
"30d" => (now.AddDays(-30), now),
|
||||
"90d" => (now.AddDays(-90), now),
|
||||
_ => (null, null)
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalculateComplianceRate(IReadOnlyList<ArtifactAttestationReport> reports)
|
||||
{
|
||||
if (reports.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var compliant = reports.Count(r =>
|
||||
r.OverallStatus == AttestationReportStatus.Pass ||
|
||||
r.OverallStatus == AttestationReportStatus.Warn);
|
||||
|
||||
return Math.Round((double)compliant / reports.Count * 100, 2);
|
||||
}
|
||||
|
||||
private static string GetStatusLabel(AttestationReportStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
AttestationReportStatus.Pass => "Passed",
|
||||
AttestationReportStatus.Fail => "Failed",
|
||||
AttestationReportStatus.Warn => "Warning",
|
||||
AttestationReportStatus.Skipped => "Skipped",
|
||||
AttestationReportStatus.Pending => "Pending",
|
||||
_ => "Unknown"
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetStatusIcon(AttestationReportStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
AttestationReportStatus.Pass => "check-circle",
|
||||
AttestationReportStatus.Fail => "x-circle",
|
||||
AttestationReportStatus.Warn => "alert-triangle",
|
||||
AttestationReportStatus.Skipped => "minus-circle",
|
||||
AttestationReportStatus.Pending => "clock",
|
||||
_ => "help-circle"
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetPredicateTypeLabel(string predicateType)
|
||||
{
|
||||
return predicateType switch
|
||||
{
|
||||
PredicateTypes.SbomV1 => "SBOM",
|
||||
PredicateTypes.VexV1 => "VEX",
|
||||
PredicateTypes.VexDecisionV1 => "VEX Decision",
|
||||
PredicateTypes.PolicyV1 => "Policy",
|
||||
PredicateTypes.PromotionV1 => "Promotion",
|
||||
PredicateTypes.EvidenceV1 => "Evidence",
|
||||
PredicateTypes.GraphV1 => "Graph",
|
||||
PredicateTypes.ReplayV1 => "Replay",
|
||||
PredicateTypes.SlsaProvenanceV1 => "SLSA v1",
|
||||
PredicateTypes.SlsaProvenanceV02 => "SLSA v0.2",
|
||||
PredicateTypes.CycloneDxBom => "CycloneDX",
|
||||
PredicateTypes.SpdxDocument => "SPDX",
|
||||
PredicateTypes.OpenVex => "OpenVEX",
|
||||
_ => predicateType
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatFreshness(FreshnessVerificationStatus freshness)
|
||||
{
|
||||
return freshness.IsFresh ? "Fresh" : $"{freshness.AgeSeconds / 3600}h old";
|
||||
}
|
||||
|
||||
private static string FormatRelativeTime(TimeSpan age)
|
||||
{
|
||||
if (age.TotalMinutes < 1)
|
||||
{
|
||||
return "just now";
|
||||
}
|
||||
|
||||
if (age.TotalHours < 1)
|
||||
{
|
||||
return $"{(int)age.TotalMinutes}m ago";
|
||||
}
|
||||
|
||||
if (age.TotalDays < 1)
|
||||
{
|
||||
return $"{(int)age.TotalHours}h ago";
|
||||
}
|
||||
|
||||
if (age.TotalDays < 7)
|
||||
{
|
||||
return $"{(int)age.TotalDays}d ago";
|
||||
}
|
||||
|
||||
return $"{(int)(age.TotalDays / 7)}w ago";
|
||||
}
|
||||
|
||||
private static string ExtractRepository(string artifactUri)
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = new Uri(artifactUri);
|
||||
var path = uri.AbsolutePath.Split('/');
|
||||
return path.Length >= 2 ? path[1] : uri.Host;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return artifactUri;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user