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,366 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// AttestationChain.cs
|
||||
// Sprint: SPRINT_3801_0001_0003_chain_verification (CHAIN-002)
|
||||
// Description: Models for attestation chain verification.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a chain of attestations for a finding.
|
||||
/// </summary>
|
||||
public sealed record AttestationChain
|
||||
{
|
||||
/// <summary>
|
||||
/// Content-addressed chain identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("chain_id")]
|
||||
public required string ChainId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The scan ID this chain belongs to.
|
||||
/// </summary>
|
||||
[JsonPropertyName("scan_id")]
|
||||
public required string ScanId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The finding ID (e.g., CVE identifier) this chain is for.
|
||||
/// </summary>
|
||||
[JsonPropertyName("finding_id")]
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The root digest (typically the scan/image digest).
|
||||
/// </summary>
|
||||
[JsonPropertyName("root_digest")]
|
||||
public required string RootDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The attestations in this chain, ordered from root to leaf.
|
||||
/// </summary>
|
||||
[JsonPropertyName("attestations")]
|
||||
public required ImmutableList<ChainAttestation> Attestations { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the entire chain is verified.
|
||||
/// </summary>
|
||||
[JsonPropertyName("verified")]
|
||||
public required bool Verified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the chain was verified.
|
||||
/// </summary>
|
||||
[JsonPropertyName("verified_at")]
|
||||
public required DateTimeOffset VerifiedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The chain status.
|
||||
/// </summary>
|
||||
[JsonPropertyName("chain_status")]
|
||||
public required ChainStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the earliest attestation in the chain expires.
|
||||
/// </summary>
|
||||
[JsonPropertyName("expires_at")]
|
||||
public DateTimeOffset? ExpiresAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single attestation in the chain.
|
||||
/// </summary>
|
||||
public sealed record ChainAttestation
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of attestation (e.g., "richgraph", "policy_decision", "human_approval").
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public required AttestationType Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The attestation ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("attestation_id")]
|
||||
public required string AttestationId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the attestation was created.
|
||||
/// </summary>
|
||||
[JsonPropertyName("created_at")]
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the attestation expires.
|
||||
/// </summary>
|
||||
[JsonPropertyName("expires_at")]
|
||||
public required DateTimeOffset ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the attestation signature verified.
|
||||
/// </summary>
|
||||
[JsonPropertyName("verified")]
|
||||
public required bool Verified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The verification status of this attestation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("verification_status")]
|
||||
public required AttestationVerificationStatus VerificationStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The subject digest this attestation covers.
|
||||
/// </summary>
|
||||
[JsonPropertyName("subject_digest")]
|
||||
public required string SubjectDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The predicate type URI.
|
||||
/// </summary>
|
||||
[JsonPropertyName("predicate_type")]
|
||||
public required string PredicateType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional error message if verification failed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("error")]
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type of attestation.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum AttestationType
|
||||
{
|
||||
/// <summary>
|
||||
/// RichGraph computation attestation.
|
||||
/// </summary>
|
||||
RichGraph,
|
||||
|
||||
/// <summary>
|
||||
/// Policy decision attestation.
|
||||
/// </summary>
|
||||
PolicyDecision,
|
||||
|
||||
/// <summary>
|
||||
/// Human approval attestation.
|
||||
/// </summary>
|
||||
HumanApproval,
|
||||
|
||||
/// <summary>
|
||||
/// SBOM generation attestation.
|
||||
/// </summary>
|
||||
Sbom,
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerability scan attestation.
|
||||
/// </summary>
|
||||
VulnerabilityScan
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The verification status of an attestation.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum AttestationVerificationStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Verification succeeded.
|
||||
/// </summary>
|
||||
Valid,
|
||||
|
||||
/// <summary>
|
||||
/// Attestation has expired.
|
||||
/// </summary>
|
||||
Expired,
|
||||
|
||||
/// <summary>
|
||||
/// Signature verification failed.
|
||||
/// </summary>
|
||||
InvalidSignature,
|
||||
|
||||
/// <summary>
|
||||
/// Attestation not found.
|
||||
/// </summary>
|
||||
NotFound,
|
||||
|
||||
/// <summary>
|
||||
/// Chain link broken (digest mismatch).
|
||||
/// </summary>
|
||||
ChainBroken,
|
||||
|
||||
/// <summary>
|
||||
/// Attestation has been revoked.
|
||||
/// </summary>
|
||||
Revoked,
|
||||
|
||||
/// <summary>
|
||||
/// Verification pending.
|
||||
/// </summary>
|
||||
Pending
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The overall status of the attestation chain.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum ChainStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// All attestations present and valid.
|
||||
/// </summary>
|
||||
Complete,
|
||||
|
||||
/// <summary>
|
||||
/// Some attestations missing but core valid.
|
||||
/// </summary>
|
||||
Partial,
|
||||
|
||||
/// <summary>
|
||||
/// One or more attestations past TTL.
|
||||
/// </summary>
|
||||
Expired,
|
||||
|
||||
/// <summary>
|
||||
/// Signature verification failed.
|
||||
/// </summary>
|
||||
Invalid,
|
||||
|
||||
/// <summary>
|
||||
/// Chain link missing or digest mismatch.
|
||||
/// </summary>
|
||||
Broken,
|
||||
|
||||
/// <summary>
|
||||
/// Chain is empty (no attestations).
|
||||
/// </summary>
|
||||
Empty
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input for chain verification.
|
||||
/// </summary>
|
||||
public sealed record ChainVerificationInput
|
||||
{
|
||||
/// <summary>
|
||||
/// The scan ID to verify chain for.
|
||||
/// </summary>
|
||||
public required Domain.ScanId ScanId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The finding ID to verify chain for.
|
||||
/// </summary>
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The expected root digest.
|
||||
/// </summary>
|
||||
public required string RootDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional: specific attestation types to verify.
|
||||
/// If null, verifies all available attestations.
|
||||
/// </summary>
|
||||
public IReadOnlyList<AttestationType>? RequiredTypes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to require human approval in the chain.
|
||||
/// </summary>
|
||||
public bool RequireHumanApproval { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Grace period for expired attestations (default: 0).
|
||||
/// </summary>
|
||||
public TimeSpan ExpirationGracePeriod { get; init; } = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of chain verification.
|
||||
/// </summary>
|
||||
public sealed record ChainVerificationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether verification succeeded.
|
||||
/// </summary>
|
||||
public required bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The verified chain.
|
||||
/// </summary>
|
||||
public AttestationChain? Chain { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if verification failed.
|
||||
/// </summary>
|
||||
public string? Error { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detailed verification results per attestation.
|
||||
/// </summary>
|
||||
public IReadOnlyList<AttestationVerificationDetail>? Details { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful result.
|
||||
/// </summary>
|
||||
public static ChainVerificationResult Succeeded(
|
||||
AttestationChain chain,
|
||||
IReadOnlyList<AttestationVerificationDetail>? details = null)
|
||||
=> new()
|
||||
{
|
||||
Success = true,
|
||||
Chain = chain,
|
||||
Details = details
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed result.
|
||||
/// </summary>
|
||||
public static ChainVerificationResult Failed(string error, AttestationChain? chain = null)
|
||||
=> new()
|
||||
{
|
||||
Success = false,
|
||||
Chain = chain,
|
||||
Error = error
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detailed verification result for a single attestation.
|
||||
/// </summary>
|
||||
public sealed record AttestationVerificationDetail
|
||||
{
|
||||
/// <summary>
|
||||
/// The attestation type.
|
||||
/// </summary>
|
||||
public required AttestationType Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The attestation ID.
|
||||
/// </summary>
|
||||
public required string AttestationId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The verification status.
|
||||
/// </summary>
|
||||
public required AttestationVerificationStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the attestation was verified successfully.
|
||||
/// </summary>
|
||||
public required bool Verified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Time taken for verification.
|
||||
/// </summary>
|
||||
public TimeSpan? VerificationTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if verification failed.
|
||||
/// </summary>
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
@@ -77,6 +77,12 @@ public sealed record FindingEvidenceResponse
|
||||
[JsonPropertyName("expires_at")]
|
||||
public DateTimeOffset? ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the evidence is stale (expired or near-expiry).
|
||||
/// </summary>
|
||||
[JsonPropertyName("is_stale")]
|
||||
public bool IsStale { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// References to DSSE/in-toto attestations backing this evidence.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// HumanApprovalStatement.cs
|
||||
// Sprint: SPRINT_3801_0001_0004_human_approval_attestation (APPROVE-002)
|
||||
// Description: In-toto statement format for human approval attestations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// In-toto statement for human approval attestations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Human approval attestations record decisions made by authorized personnel
|
||||
/// to accept, defer, reject, suppress, or escalate security findings.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Default TTL is 30 days to force periodic re-review of risk acceptances.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed record HumanApprovalStatement
|
||||
{
|
||||
/// <summary>
|
||||
/// The in-toto statement type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("_type")]
|
||||
public string Type => "https://in-toto.io/Statement/v1";
|
||||
|
||||
/// <summary>
|
||||
/// The predicate type URI.
|
||||
/// </summary>
|
||||
[JsonPropertyName("predicateType")]
|
||||
public string PredicateType => "stella.ops/human-approval@v1";
|
||||
|
||||
/// <summary>
|
||||
/// The subjects this attestation covers.
|
||||
/// </summary>
|
||||
[JsonPropertyName("subject")]
|
||||
public required IList<HumanApprovalSubject> Subject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The human approval predicate.
|
||||
/// </summary>
|
||||
[JsonPropertyName("predicate")]
|
||||
public required HumanApprovalPredicate Predicate { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subject reference for human approval attestation.
|
||||
/// </summary>
|
||||
public sealed record HumanApprovalSubject
|
||||
{
|
||||
/// <summary>
|
||||
/// The subject name (e.g., "scan:12345" or "finding:CVE-2024-12345").
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The subject digest(s).
|
||||
/// </summary>
|
||||
[JsonPropertyName("digest")]
|
||||
public required IDictionary<string, string> Digest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The human approval predicate data.
|
||||
/// </summary>
|
||||
public sealed record HumanApprovalPredicate
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema version identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("schema")]
|
||||
public string Schema => "human-approval-v1";
|
||||
|
||||
/// <summary>
|
||||
/// Unique approval identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("approval_id")]
|
||||
public required string ApprovalId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The finding ID (e.g., CVE identifier).
|
||||
/// </summary>
|
||||
[JsonPropertyName("finding_id")]
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The approval decision.
|
||||
/// </summary>
|
||||
[JsonPropertyName("decision")]
|
||||
public required ApprovalDecision Decision { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Information about the approver.
|
||||
/// </summary>
|
||||
[JsonPropertyName("approver")]
|
||||
public required ApproverInfo Approver { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Justification for the decision.
|
||||
/// </summary>
|
||||
[JsonPropertyName("justification")]
|
||||
public required string Justification { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the approval was made.
|
||||
/// </summary>
|
||||
[JsonPropertyName("approved_at")]
|
||||
public required DateTimeOffset ApprovedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the approval expires.
|
||||
/// </summary>
|
||||
[JsonPropertyName("expires_at")]
|
||||
public DateTimeOffset? ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the policy decision this approval is for.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policy_decision_ref")]
|
||||
public string? PolicyDecisionRef { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional restrictions on the approval scope.
|
||||
/// </summary>
|
||||
[JsonPropertyName("restrictions")]
|
||||
public ApprovalRestrictions? Restrictions { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional prior approval being superseded.
|
||||
/// </summary>
|
||||
[JsonPropertyName("supersedes")]
|
||||
public string? Supersedes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional metadata.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public IDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about the person who made the approval.
|
||||
/// </summary>
|
||||
public sealed record ApproverInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The approver's user identifier (e.g., email).
|
||||
/// </summary>
|
||||
[JsonPropertyName("user_id")]
|
||||
public required string UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The approver's display name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("display_name")]
|
||||
public string? DisplayName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The approver's role in the organization.
|
||||
/// </summary>
|
||||
[JsonPropertyName("role")]
|
||||
public string? Role { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional delegation chain (if approving on behalf of someone else).
|
||||
/// </summary>
|
||||
[JsonPropertyName("delegated_from")]
|
||||
public string? DelegatedFrom { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restrictions on the approval scope.
|
||||
/// </summary>
|
||||
public sealed record ApprovalRestrictions
|
||||
{
|
||||
/// <summary>
|
||||
/// Environments where the approval applies (e.g., "production", "staging").
|
||||
/// </summary>
|
||||
[JsonPropertyName("environments")]
|
||||
public IList<string>? Environments { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of affected instances.
|
||||
/// </summary>
|
||||
[JsonPropertyName("max_instances")]
|
||||
public int? MaxInstances { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Namespaces where the approval applies.
|
||||
/// </summary>
|
||||
[JsonPropertyName("namespaces")]
|
||||
public IList<string>? Namespaces { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Specific images/artifacts the approval applies to.
|
||||
/// </summary>
|
||||
[JsonPropertyName("artifacts")]
|
||||
public IList<string>? Artifacts { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Custom conditions that must be met.
|
||||
/// </summary>
|
||||
[JsonPropertyName("conditions")]
|
||||
public IDictionary<string, string>? Conditions { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The approval decision type.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum ApprovalDecision
|
||||
{
|
||||
/// <summary>
|
||||
/// Risk accepted with justification.
|
||||
/// </summary>
|
||||
AcceptRisk,
|
||||
|
||||
/// <summary>
|
||||
/// Decision deferred, requires re-review.
|
||||
/// </summary>
|
||||
Defer,
|
||||
|
||||
/// <summary>
|
||||
/// Finding must be remediated.
|
||||
/// </summary>
|
||||
Reject,
|
||||
|
||||
/// <summary>
|
||||
/// Finding suppressed (false positive).
|
||||
/// </summary>
|
||||
Suppress,
|
||||
|
||||
/// <summary>
|
||||
/// Escalated to higher authority.
|
||||
/// </summary>
|
||||
Escalate
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PolicyDecisionStatement.cs
|
||||
// Sprint: SPRINT_3801_0001_0001_policy_decision_attestation
|
||||
// Description: In-toto statement for policy decision attestations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// In-toto statement for policy evaluation decisions.
|
||||
/// Predicate type: stella.ops/policy-decision@v1
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This statement attests that a policy decision was made for a finding
|
||||
/// based on the evidence available at evaluation time.
|
||||
/// </remarks>
|
||||
public sealed record PolicyDecisionStatement
|
||||
{
|
||||
/// <summary>
|
||||
/// The statement type, always "https://in-toto.io/Statement/v1".
|
||||
/// </summary>
|
||||
[JsonPropertyName("_type")]
|
||||
public string Type => "https://in-toto.io/Statement/v1";
|
||||
|
||||
/// <summary>
|
||||
/// The subjects this statement is about (scan + finding artifacts).
|
||||
/// </summary>
|
||||
[JsonPropertyName("subject")]
|
||||
public required IReadOnlyList<PolicyDecisionSubject> Subject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The predicate type URI.
|
||||
/// </summary>
|
||||
[JsonPropertyName("predicateType")]
|
||||
public string PredicateType => "stella.ops/policy-decision@v1";
|
||||
|
||||
/// <summary>
|
||||
/// The policy decision predicate payload.
|
||||
/// </summary>
|
||||
[JsonPropertyName("predicate")]
|
||||
public required PolicyDecisionPredicate Predicate { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subject in a policy decision statement.
|
||||
/// </summary>
|
||||
public sealed record PolicyDecisionSubject
|
||||
{
|
||||
/// <summary>
|
||||
/// The name or identifier of the subject (e.g., scan ID, finding ID).
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Digests of the subject in algorithm:hex format.
|
||||
/// </summary>
|
||||
[JsonPropertyName("digest")]
|
||||
public required IReadOnlyDictionary<string, string> Digest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predicate payload for policy decision attestations.
|
||||
/// </summary>
|
||||
public sealed record PolicyDecisionPredicate
|
||||
{
|
||||
/// <summary>
|
||||
/// The finding ID this decision applies to (CVE@PURL format).
|
||||
/// </summary>
|
||||
[JsonPropertyName("finding_id")]
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The CVE identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cve")]
|
||||
public required string Cve { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The component PURL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("component_purl")]
|
||||
public required string ComponentPurl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The policy decision result.
|
||||
/// </summary>
|
||||
[JsonPropertyName("decision")]
|
||||
public required PolicyDecision Decision { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The reasoning behind the decision.
|
||||
/// </summary>
|
||||
[JsonPropertyName("reasoning")]
|
||||
public required PolicyDecisionReasoning Reasoning { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// References to evidence artifacts used in the decision.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidence_refs")]
|
||||
public required IReadOnlyList<string> EvidenceRefs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the decision was evaluated (UTC ISO 8601).
|
||||
/// </summary>
|
||||
[JsonPropertyName("evaluated_at")]
|
||||
public required DateTimeOffset EvaluatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the decision expires (UTC ISO 8601).
|
||||
/// </summary>
|
||||
[JsonPropertyName("expires_at")]
|
||||
public DateTimeOffset? ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of the policy used for evaluation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policy_version")]
|
||||
public required string PolicyVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of the policy configuration used.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policy_hash")]
|
||||
public string? PolicyHash { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Policy decision type.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum PolicyDecision
|
||||
{
|
||||
/// <summary>Finding is allowed (low risk or mitigated).</summary>
|
||||
Allow,
|
||||
|
||||
/// <summary>Finding requires review.</summary>
|
||||
Review,
|
||||
|
||||
/// <summary>Finding is blocked (high risk).</summary>
|
||||
Block,
|
||||
|
||||
/// <summary>Finding is suppressed by policy.</summary>
|
||||
Suppress,
|
||||
|
||||
/// <summary>Finding is escalated for immediate attention.</summary>
|
||||
Escalate
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reasoning details for a policy decision.
|
||||
/// </summary>
|
||||
public sealed record PolicyDecisionReasoning
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of policy rules evaluated.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rules_evaluated")]
|
||||
public required int RulesEvaluated { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Names of policy rules that matched.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rules_matched")]
|
||||
public required IReadOnlyList<string> RulesMatched { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Final computed risk score (0-100).
|
||||
/// </summary>
|
||||
[JsonPropertyName("final_score")]
|
||||
public required double FinalScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Risk multiplier applied (1.0 = no change, <1 = reduced, >1 = amplified).
|
||||
/// </summary>
|
||||
[JsonPropertyName("risk_multiplier")]
|
||||
public required double RiskMultiplier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reachability state used in decision.
|
||||
/// </summary>
|
||||
[JsonPropertyName("reachability_state")]
|
||||
public string? ReachabilityState { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// VEX status used in decision.
|
||||
/// </summary>
|
||||
[JsonPropertyName("vex_status")]
|
||||
public string? VexStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable summary of the decision rationale.
|
||||
/// </summary>
|
||||
[JsonPropertyName("summary")]
|
||||
public string? Summary { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// RichGraphStatement.cs
|
||||
// Sprint: SPRINT_3801_0001_0002_richgraph_attestation
|
||||
// Description: In-toto statement for RichGraph attestations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// In-toto statement for RichGraph computation attestations.
|
||||
/// Predicate type: stella.ops/richgraph@v1
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This statement attests that a RichGraph was computed from a specific
|
||||
/// SBOM and call graph, producing a content-addressed graph digest.
|
||||
/// </remarks>
|
||||
public sealed record RichGraphStatement
|
||||
{
|
||||
/// <summary>
|
||||
/// The statement type, always "https://in-toto.io/Statement/v1".
|
||||
/// </summary>
|
||||
[JsonPropertyName("_type")]
|
||||
public string Type => "https://in-toto.io/Statement/v1";
|
||||
|
||||
/// <summary>
|
||||
/// The subjects this statement is about (scan + graph artifacts).
|
||||
/// </summary>
|
||||
[JsonPropertyName("subject")]
|
||||
public required IReadOnlyList<RichGraphSubject> Subject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The predicate type URI.
|
||||
/// </summary>
|
||||
[JsonPropertyName("predicateType")]
|
||||
public string PredicateType => "stella.ops/richgraph@v1";
|
||||
|
||||
/// <summary>
|
||||
/// The RichGraph predicate payload.
|
||||
/// </summary>
|
||||
[JsonPropertyName("predicate")]
|
||||
public required RichGraphPredicate Predicate { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subject in a RichGraph statement.
|
||||
/// </summary>
|
||||
public sealed record RichGraphSubject
|
||||
{
|
||||
/// <summary>
|
||||
/// The name or identifier of the subject (e.g., scan ID, graph ID).
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Digests of the subject in algorithm:hex format.
|
||||
/// </summary>
|
||||
[JsonPropertyName("digest")]
|
||||
public required IReadOnlyDictionary<string, string> Digest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predicate payload for RichGraph attestations.
|
||||
/// </summary>
|
||||
public sealed record RichGraphPredicate
|
||||
{
|
||||
/// <summary>
|
||||
/// The RichGraph identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("graph_id")]
|
||||
public required string GraphId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content-addressed digest of the RichGraph.
|
||||
/// </summary>
|
||||
[JsonPropertyName("graph_digest")]
|
||||
public required string GraphDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of nodes in the graph.
|
||||
/// </summary>
|
||||
[JsonPropertyName("node_count")]
|
||||
public required int NodeCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of edges in the graph.
|
||||
/// </summary>
|
||||
[JsonPropertyName("edge_count")]
|
||||
public required int EdgeCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of root nodes (entrypoints) in the graph.
|
||||
/// </summary>
|
||||
[JsonPropertyName("root_count")]
|
||||
public required int RootCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Information about the analyzer that computed the graph.
|
||||
/// </summary>
|
||||
[JsonPropertyName("analyzer")]
|
||||
public required RichGraphAnalyzerInfo Analyzer { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the graph was computed (UTC ISO 8601).
|
||||
/// </summary>
|
||||
[JsonPropertyName("computed_at")]
|
||||
public required DateTimeOffset ComputedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the graph attestation expires (UTC ISO 8601).
|
||||
/// </summary>
|
||||
[JsonPropertyName("expires_at")]
|
||||
public DateTimeOffset? ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the source SBOM (digest).
|
||||
/// </summary>
|
||||
[JsonPropertyName("sbom_ref")]
|
||||
public string? SbomRef { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the source call graph (digest).
|
||||
/// </summary>
|
||||
[JsonPropertyName("callgraph_ref")]
|
||||
public string? CallgraphRef { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Language of the analyzed code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("language")]
|
||||
public string? Language { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Schema version of the RichGraph.
|
||||
/// </summary>
|
||||
[JsonPropertyName("schema")]
|
||||
public string Schema { get; init; } = "richgraph-v1";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about the analyzer that computed the RichGraph.
|
||||
/// </summary>
|
||||
public sealed record RichGraphAnalyzerInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the analyzer.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of the analyzer.
|
||||
/// </summary>
|
||||
[JsonPropertyName("version")]
|
||||
public required string Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Configuration hash used for the analysis.
|
||||
/// </summary>
|
||||
[JsonPropertyName("config_hash")]
|
||||
public string? ConfigHash { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user