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:
StellaOps Bot
2025-12-20 01:26:42 +02:00
parent edc91ea96f
commit 5fc469ad98
159 changed files with 41116 additions and 2305 deletions

View File

@@ -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; }
}

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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, &lt;1 = reduced, &gt;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; }
}

View File

@@ -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; }
}