feat: Add VEX Status Chip component and integration tests for reachability drift detection
- Introduced `VexStatusChipComponent` to display VEX status with color coding and tooltips. - Implemented integration tests for reachability drift detection, covering various scenarios including drift detection, determinism, and error handling. - Enhanced `ScannerToSignalsReachabilityTests` with a null implementation of `ICallGraphSyncService` for better test isolation. - Updated project references to include the new Reachability Drift library.
This commit is contained in:
@@ -0,0 +1,672 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// AttestationChainVerifier.cs
|
||||
// Sprint: SPRINT_3801_0001_0003_chain_verification (CHAIN-003)
|
||||
// Description: Verifies attestation chain integrity.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies attestation chain integrity.
|
||||
/// </summary>
|
||||
public sealed class AttestationChainVerifier : IAttestationChainVerifier
|
||||
{
|
||||
private readonly ILogger<AttestationChainVerifier> _logger;
|
||||
private readonly AttestationChainVerifierOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IPolicyDecisionAttestationService _policyAttestationService;
|
||||
private readonly IRichGraphAttestationService _richGraphAttestationService;
|
||||
private readonly IHumanApprovalAttestationService _humanApprovalAttestationService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="AttestationChainVerifier"/>.
|
||||
/// </summary>
|
||||
public AttestationChainVerifier(
|
||||
ILogger<AttestationChainVerifier> logger,
|
||||
IOptions<AttestationChainVerifierOptions> options,
|
||||
TimeProvider timeProvider,
|
||||
IPolicyDecisionAttestationService policyAttestationService,
|
||||
IRichGraphAttestationService richGraphAttestationService,
|
||||
IHumanApprovalAttestationService humanApprovalAttestationService)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_policyAttestationService = policyAttestationService ?? throw new ArgumentNullException(nameof(policyAttestationService));
|
||||
_richGraphAttestationService = richGraphAttestationService ?? throw new ArgumentNullException(nameof(richGraphAttestationService));
|
||||
_humanApprovalAttestationService = humanApprovalAttestationService ?? throw new ArgumentNullException(nameof(humanApprovalAttestationService));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ChainVerificationResult> VerifyChainAsync(
|
||||
ChainVerificationInput input,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.FindingId))
|
||||
{
|
||||
throw new ArgumentException("FindingId is required", nameof(input));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.RootDigest))
|
||||
{
|
||||
throw new ArgumentException("RootDigest is required", nameof(input));
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Verifying attestation chain for scan {ScanId}, finding {FindingId}",
|
||||
input.ScanId,
|
||||
input.FindingId);
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var details = new List<AttestationVerificationDetail>();
|
||||
var attestations = new List<ChainAttestation>();
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var hasFailures = false;
|
||||
var hasExpired = false;
|
||||
|
||||
// Collect attestations in chain order
|
||||
// 1. RichGraph attestation (reachability analysis)
|
||||
var richGraphResult = await VerifyRichGraphAttestationAsync(
|
||||
input.ScanId,
|
||||
input.FindingId,
|
||||
now,
|
||||
input.ExpirationGracePeriod,
|
||||
cancellationToken);
|
||||
|
||||
if (richGraphResult.Detail != null)
|
||||
{
|
||||
details.Add(richGraphResult.Detail);
|
||||
}
|
||||
|
||||
if (richGraphResult.Attestation != null)
|
||||
{
|
||||
attestations.Add(richGraphResult.Attestation);
|
||||
}
|
||||
|
||||
hasFailures |= richGraphResult.IsFailed;
|
||||
hasExpired |= richGraphResult.IsExpired;
|
||||
|
||||
// 2. Policy decision attestation
|
||||
var policyResult = await VerifyPolicyAttestationAsync(
|
||||
input.ScanId,
|
||||
input.FindingId,
|
||||
now,
|
||||
input.ExpirationGracePeriod,
|
||||
cancellationToken);
|
||||
|
||||
if (policyResult.Detail != null)
|
||||
{
|
||||
details.Add(policyResult.Detail);
|
||||
}
|
||||
|
||||
if (policyResult.Attestation != null)
|
||||
{
|
||||
attestations.Add(policyResult.Attestation);
|
||||
}
|
||||
|
||||
hasFailures |= policyResult.IsFailed;
|
||||
hasExpired |= policyResult.IsExpired;
|
||||
|
||||
// 3. Human approval attestation
|
||||
var humanApprovalResult = await VerifyHumanApprovalAttestationAsync(
|
||||
input.ScanId,
|
||||
input.FindingId,
|
||||
now,
|
||||
input.ExpirationGracePeriod,
|
||||
cancellationToken);
|
||||
|
||||
if (humanApprovalResult.Detail != null)
|
||||
{
|
||||
details.Add(humanApprovalResult.Detail);
|
||||
}
|
||||
|
||||
if (humanApprovalResult.Attestation != null)
|
||||
{
|
||||
attestations.Add(humanApprovalResult.Attestation);
|
||||
}
|
||||
|
||||
hasFailures |= humanApprovalResult.IsFailed;
|
||||
hasExpired |= humanApprovalResult.IsExpired;
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
// Determine chain status
|
||||
var chainStatus = DetermineChainStatus(
|
||||
attestations,
|
||||
hasFailures,
|
||||
hasExpired,
|
||||
input.RequiredTypes,
|
||||
input.RequireHumanApproval);
|
||||
|
||||
// Build the chain
|
||||
var chain = new AttestationChain
|
||||
{
|
||||
ChainId = ComputeChainId(input.ScanId, input.FindingId, input.RootDigest),
|
||||
ScanId = input.ScanId.ToString(),
|
||||
FindingId = input.FindingId,
|
||||
RootDigest = input.RootDigest,
|
||||
Attestations = attestations.ToImmutableList(),
|
||||
Verified = chainStatus == ChainStatus.Complete,
|
||||
VerifiedAt = now,
|
||||
Status = chainStatus,
|
||||
ExpiresAt = GetEarliestExpiration(attestations)
|
||||
};
|
||||
|
||||
_logger.LogInformation(
|
||||
"Chain verification completed in {ElapsedMs}ms: {Status} with {Count} attestations",
|
||||
stopwatch.ElapsedMilliseconds,
|
||||
chainStatus,
|
||||
attestations.Count);
|
||||
|
||||
if (chainStatus == ChainStatus.Complete)
|
||||
{
|
||||
return ChainVerificationResult.Succeeded(chain, details);
|
||||
}
|
||||
|
||||
var errorMessage = chainStatus switch
|
||||
{
|
||||
ChainStatus.Expired => "One or more attestations have expired",
|
||||
ChainStatus.Invalid => "Signature verification failed or attestation revoked",
|
||||
ChainStatus.Broken => "Chain link broken or digest mismatch",
|
||||
ChainStatus.Partial => "Required attestations are missing",
|
||||
ChainStatus.Empty => "No attestations found in chain",
|
||||
_ => "Chain verification failed"
|
||||
};
|
||||
|
||||
// Include details in failure result so callers can inspect why it failed
|
||||
return new ChainVerificationResult
|
||||
{
|
||||
Success = false,
|
||||
Chain = chain,
|
||||
Error = errorMessage,
|
||||
Details = details
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<AttestationChain?> GetChainAsync(
|
||||
ScanId scanId,
|
||||
string findingId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scanId);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(findingId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var attestations = new List<ChainAttestation>();
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
// Collect attestations (without full verification)
|
||||
// Note: This is a simplified implementation; in production we'd have a more
|
||||
// efficient way to query attestations by finding ID
|
||||
|
||||
// For now, we return null since we don't have a lookup by finding ID
|
||||
// The full implementation would query attestation stores
|
||||
_logger.LogDebug(
|
||||
"GetChainAsync called for scan {ScanId}, finding {FindingId}",
|
||||
scanId,
|
||||
findingId);
|
||||
|
||||
// Placeholder: return null until we have proper attestation indexing
|
||||
await Task.CompletedTask;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsChainComplete(AttestationChain chain, params AttestationType[] requiredTypes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(chain);
|
||||
|
||||
if (requiredTypes.Length == 0)
|
||||
{
|
||||
return chain.Attestations.Count > 0;
|
||||
}
|
||||
|
||||
var presentTypes = chain.Attestations
|
||||
.Where(a => a.Verified)
|
||||
.Select(a => a.Type)
|
||||
.ToHashSet();
|
||||
|
||||
return requiredTypes.All(t => presentTypes.Contains(t));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTimeOffset? GetEarliestExpiration(AttestationChain chain)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(chain);
|
||||
return GetEarliestExpiration(chain.Attestations);
|
||||
}
|
||||
|
||||
private static DateTimeOffset? GetEarliestExpiration(IEnumerable<ChainAttestation> attestations)
|
||||
{
|
||||
var expirations = attestations
|
||||
.Where(a => a.Verified)
|
||||
.Select(a => a.ExpiresAt)
|
||||
.ToList();
|
||||
|
||||
return expirations.Count > 0 ? expirations.Min() : null;
|
||||
}
|
||||
|
||||
private async Task<AttestationVerificationResult> VerifyRichGraphAttestationAsync(
|
||||
ScanId scanId,
|
||||
string findingId,
|
||||
DateTimeOffset now,
|
||||
TimeSpan gracePeriod,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// Try to get the RichGraph attestation
|
||||
// Note: We use the finding ID as the graph ID for lookup
|
||||
// In practice, we'd have a mapping from finding to graph
|
||||
var attestation = await _richGraphAttestationService.GetAttestationAsync(
|
||||
scanId,
|
||||
findingId,
|
||||
cancellationToken);
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
if (attestation == null)
|
||||
{
|
||||
return new AttestationVerificationResult
|
||||
{
|
||||
Detail = new AttestationVerificationDetail
|
||||
{
|
||||
Type = AttestationType.RichGraph,
|
||||
AttestationId = "not-found",
|
||||
Status = AttestationVerificationStatus.NotFound,
|
||||
Verified = false,
|
||||
VerificationTime = stopwatch.Elapsed,
|
||||
Error = "RichGraph attestation not found"
|
||||
},
|
||||
IsFailed = false, // Not found is partial, not failed
|
||||
IsExpired = false
|
||||
};
|
||||
}
|
||||
|
||||
var statement = attestation.Statement!;
|
||||
var expiresAt = statement.Predicate.ExpiresAt ?? now.AddDays(7);
|
||||
var isExpired = now > expiresAt.Add(gracePeriod);
|
||||
|
||||
var chainAttestation = new ChainAttestation
|
||||
{
|
||||
Type = AttestationType.RichGraph,
|
||||
AttestationId = attestation.AttestationId!,
|
||||
CreatedAt = statement.Predicate.ComputedAt,
|
||||
ExpiresAt = expiresAt,
|
||||
Verified = !isExpired,
|
||||
VerificationStatus = isExpired
|
||||
? AttestationVerificationStatus.Expired
|
||||
: AttestationVerificationStatus.Valid,
|
||||
SubjectDigest = statement.Predicate.GraphDigest,
|
||||
PredicateType = statement.PredicateType
|
||||
};
|
||||
|
||||
return new AttestationVerificationResult
|
||||
{
|
||||
Attestation = chainAttestation,
|
||||
Detail = new AttestationVerificationDetail
|
||||
{
|
||||
Type = AttestationType.RichGraph,
|
||||
AttestationId = attestation.AttestationId!,
|
||||
Status = chainAttestation.VerificationStatus,
|
||||
Verified = chainAttestation.Verified,
|
||||
VerificationTime = stopwatch.Elapsed
|
||||
},
|
||||
IsFailed = false,
|
||||
IsExpired = isExpired
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
_logger.LogWarning(ex, "Failed to verify RichGraph attestation for scan {ScanId}", scanId);
|
||||
|
||||
return new AttestationVerificationResult
|
||||
{
|
||||
Detail = new AttestationVerificationDetail
|
||||
{
|
||||
Type = AttestationType.RichGraph,
|
||||
AttestationId = "error",
|
||||
Status = AttestationVerificationStatus.ChainBroken,
|
||||
Verified = false,
|
||||
VerificationTime = stopwatch.Elapsed,
|
||||
Error = ex.Message
|
||||
},
|
||||
IsFailed = true,
|
||||
IsExpired = false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AttestationVerificationResult> VerifyPolicyAttestationAsync(
|
||||
ScanId scanId,
|
||||
string findingId,
|
||||
DateTimeOffset now,
|
||||
TimeSpan gracePeriod,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// Try to get the policy attestation
|
||||
var attestation = await _policyAttestationService.GetAttestationAsync(
|
||||
scanId,
|
||||
findingId,
|
||||
cancellationToken);
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
if (attestation == null)
|
||||
{
|
||||
return new AttestationVerificationResult
|
||||
{
|
||||
Detail = new AttestationVerificationDetail
|
||||
{
|
||||
Type = AttestationType.PolicyDecision,
|
||||
AttestationId = "not-found",
|
||||
Status = AttestationVerificationStatus.NotFound,
|
||||
Verified = false,
|
||||
VerificationTime = stopwatch.Elapsed,
|
||||
Error = "Policy decision attestation not found"
|
||||
},
|
||||
IsFailed = false, // Not found is partial, not failed
|
||||
IsExpired = false
|
||||
};
|
||||
}
|
||||
|
||||
var statement = attestation.Statement!;
|
||||
var expiresAt = statement.Predicate.ExpiresAt ?? now.AddDays(7);
|
||||
var isExpired = now > expiresAt.Add(gracePeriod);
|
||||
|
||||
var chainAttestation = new ChainAttestation
|
||||
{
|
||||
Type = AttestationType.PolicyDecision,
|
||||
AttestationId = attestation.AttestationId!,
|
||||
CreatedAt = statement.Predicate.EvaluatedAt,
|
||||
ExpiresAt = expiresAt,
|
||||
Verified = !isExpired,
|
||||
VerificationStatus = isExpired
|
||||
? AttestationVerificationStatus.Expired
|
||||
: AttestationVerificationStatus.Valid,
|
||||
SubjectDigest = statement.Subject[0].Digest["sha256"],
|
||||
PredicateType = statement.PredicateType
|
||||
};
|
||||
|
||||
return new AttestationVerificationResult
|
||||
{
|
||||
Attestation = chainAttestation,
|
||||
Detail = new AttestationVerificationDetail
|
||||
{
|
||||
Type = AttestationType.PolicyDecision,
|
||||
AttestationId = attestation.AttestationId!,
|
||||
Status = chainAttestation.VerificationStatus,
|
||||
Verified = chainAttestation.Verified,
|
||||
VerificationTime = stopwatch.Elapsed
|
||||
},
|
||||
IsFailed = false,
|
||||
IsExpired = isExpired
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
_logger.LogWarning(ex, "Failed to verify policy attestation for scan {ScanId}", scanId);
|
||||
|
||||
return new AttestationVerificationResult
|
||||
{
|
||||
Detail = new AttestationVerificationDetail
|
||||
{
|
||||
Type = AttestationType.PolicyDecision,
|
||||
AttestationId = "error",
|
||||
Status = AttestationVerificationStatus.ChainBroken,
|
||||
Verified = false,
|
||||
VerificationTime = stopwatch.Elapsed,
|
||||
Error = ex.Message
|
||||
},
|
||||
IsFailed = true,
|
||||
IsExpired = false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AttestationVerificationResult> VerifyHumanApprovalAttestationAsync(
|
||||
ScanId scanId,
|
||||
string findingId,
|
||||
DateTimeOffset now,
|
||||
TimeSpan gracePeriod,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// Try to get the human approval attestation
|
||||
var attestation = await _humanApprovalAttestationService.GetAttestationAsync(
|
||||
scanId,
|
||||
findingId,
|
||||
cancellationToken);
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
if (attestation == null)
|
||||
{
|
||||
return new AttestationVerificationResult
|
||||
{
|
||||
Detail = new AttestationVerificationDetail
|
||||
{
|
||||
Type = AttestationType.HumanApproval,
|
||||
AttestationId = "not-found",
|
||||
Status = AttestationVerificationStatus.NotFound,
|
||||
Verified = false,
|
||||
VerificationTime = stopwatch.Elapsed,
|
||||
Error = "Human approval attestation not found"
|
||||
},
|
||||
IsFailed = false, // Not found is partial, not failed
|
||||
IsExpired = false
|
||||
};
|
||||
}
|
||||
|
||||
// Check if attestation was revoked
|
||||
if (attestation.IsRevoked)
|
||||
{
|
||||
return new AttestationVerificationResult
|
||||
{
|
||||
Detail = new AttestationVerificationDetail
|
||||
{
|
||||
Type = AttestationType.HumanApproval,
|
||||
AttestationId = attestation.AttestationId!,
|
||||
Status = AttestationVerificationStatus.Revoked,
|
||||
Verified = false,
|
||||
VerificationTime = stopwatch.Elapsed,
|
||||
Error = "Human approval attestation has been revoked"
|
||||
},
|
||||
IsFailed = true,
|
||||
IsExpired = false
|
||||
};
|
||||
}
|
||||
|
||||
var statement = attestation.Statement!;
|
||||
|
||||
// Default to 30 days (human approval default TTL) if not specified
|
||||
var expiresAt = statement.Predicate.ExpiresAt ?? now.AddDays(30);
|
||||
var isExpired = now > expiresAt.Add(gracePeriod);
|
||||
|
||||
// Get subject digest if available
|
||||
var subjectDigest = statement.Subject.Count > 0
|
||||
&& statement.Subject[0].Digest.TryGetValue("sha256", out var digest)
|
||||
? digest
|
||||
: string.Empty;
|
||||
|
||||
var chainAttestation = new ChainAttestation
|
||||
{
|
||||
Type = AttestationType.HumanApproval,
|
||||
AttestationId = attestation.AttestationId!,
|
||||
CreatedAt = statement.Predicate.ApprovedAt,
|
||||
ExpiresAt = expiresAt,
|
||||
Verified = !isExpired,
|
||||
VerificationStatus = isExpired
|
||||
? AttestationVerificationStatus.Expired
|
||||
: AttestationVerificationStatus.Valid,
|
||||
SubjectDigest = subjectDigest,
|
||||
PredicateType = statement.PredicateType
|
||||
};
|
||||
|
||||
return new AttestationVerificationResult
|
||||
{
|
||||
Attestation = chainAttestation,
|
||||
Detail = new AttestationVerificationDetail
|
||||
{
|
||||
Type = AttestationType.HumanApproval,
|
||||
AttestationId = attestation.AttestationId!,
|
||||
Status = chainAttestation.VerificationStatus,
|
||||
Verified = chainAttestation.Verified,
|
||||
VerificationTime = stopwatch.Elapsed
|
||||
},
|
||||
IsFailed = false,
|
||||
IsExpired = isExpired
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
_logger.LogWarning(ex, "Failed to verify human approval attestation for scan {ScanId}", scanId);
|
||||
|
||||
return new AttestationVerificationResult
|
||||
{
|
||||
Detail = new AttestationVerificationDetail
|
||||
{
|
||||
Type = AttestationType.HumanApproval,
|
||||
AttestationId = "error",
|
||||
Status = AttestationVerificationStatus.ChainBroken,
|
||||
Verified = false,
|
||||
VerificationTime = stopwatch.Elapsed,
|
||||
Error = ex.Message
|
||||
},
|
||||
IsFailed = true,
|
||||
IsExpired = false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static ChainStatus DetermineChainStatus(
|
||||
List<ChainAttestation> attestations,
|
||||
bool hasFailures,
|
||||
bool hasExpired,
|
||||
IReadOnlyList<AttestationType>? requiredTypes,
|
||||
bool requireHumanApproval)
|
||||
{
|
||||
if (hasFailures)
|
||||
{
|
||||
return ChainStatus.Invalid;
|
||||
}
|
||||
|
||||
if (attestations.Count == 0)
|
||||
{
|
||||
return ChainStatus.Empty;
|
||||
}
|
||||
|
||||
if (hasExpired)
|
||||
{
|
||||
return ChainStatus.Expired;
|
||||
}
|
||||
|
||||
// Check for broken chain (digest mismatches would be detected during verification)
|
||||
if (attestations.Any(a => a.VerificationStatus == AttestationVerificationStatus.ChainBroken))
|
||||
{
|
||||
return ChainStatus.Broken;
|
||||
}
|
||||
|
||||
// Check for required types
|
||||
var presentTypes = attestations
|
||||
.Where(a => a.Verified)
|
||||
.Select(a => a.Type)
|
||||
.ToHashSet();
|
||||
|
||||
if (requiredTypes != null && requiredTypes.Count > 0)
|
||||
{
|
||||
if (!requiredTypes.All(t => presentTypes.Contains(t)))
|
||||
{
|
||||
return ChainStatus.Partial;
|
||||
}
|
||||
}
|
||||
|
||||
if (requireHumanApproval && !presentTypes.Contains(AttestationType.HumanApproval))
|
||||
{
|
||||
return ChainStatus.Partial;
|
||||
}
|
||||
|
||||
// All verified attestations present
|
||||
return ChainStatus.Complete;
|
||||
}
|
||||
|
||||
private static string ComputeChainId(ScanId scanId, string findingId, string rootDigest)
|
||||
{
|
||||
var input = $"{scanId}|{findingId}|{rootDigest}";
|
||||
return ComputeSha256(input);
|
||||
}
|
||||
|
||||
private static string ComputeSha256(string input)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return $"sha256:{Convert.ToHexStringLower(hash)}";
|
||||
}
|
||||
|
||||
private sealed record AttestationVerificationResult
|
||||
{
|
||||
public ChainAttestation? Attestation { get; init; }
|
||||
public AttestationVerificationDetail? Detail { get; init; }
|
||||
public bool IsFailed { get; init; }
|
||||
public bool IsExpired { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for attestation chain verification.
|
||||
/// </summary>
|
||||
public sealed class AttestationChainVerifierOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Default grace period for expired attestations in minutes.
|
||||
/// </summary>
|
||||
public int DefaultGracePeriodMinutes { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to require human approval for high-severity findings.
|
||||
/// </summary>
|
||||
public bool RequireHumanApprovalForHighSeverity { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum chain depth to verify.
|
||||
/// </summary>
|
||||
public int MaxChainDepth { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to fail on missing attestations vs. reporting partial status.
|
||||
/// </summary>
|
||||
public bool FailOnMissingAttestations { get; set; } = false;
|
||||
}
|
||||
Reference in New Issue
Block a user