feat: add Attestation Chain and Triage Evidence API clients and models

- Implemented Attestation Chain API client with methods for verifying, fetching, and managing attestation chains.
- Created models for Attestation Chain, including DSSE envelope structures and verification results.
- Developed Triage Evidence API client for fetching finding evidence, including methods for evidence retrieval by CVE and component.
- Added models for Triage Evidence, encapsulating evidence responses, entry points, boundary proofs, and VEX evidence.
- Introduced mock implementations for both API clients to facilitate testing and development.
This commit is contained in:
master
2025-12-18 13:15:13 +02:00
parent 7d5250238c
commit 00d2c99af9
118 changed files with 13463 additions and 151 deletions

View File

@@ -0,0 +1,162 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace StellaOps.Scanner.Triage.Entities;
/// <summary>
/// Read-only view representing the current state of a triage case,
/// combining the latest risk, reachability, and VEX data.
/// </summary>
[Keyless]
public sealed class TriageCaseCurrent
{
/// <summary>
/// The case/finding ID.
/// </summary>
[Column("case_id")]
public Guid CaseId { get; init; }
/// <summary>
/// The asset ID.
/// </summary>
[Column("asset_id")]
public Guid AssetId { get; init; }
/// <summary>
/// Optional environment ID.
/// </summary>
[Column("environment_id")]
public Guid? EnvironmentId { get; init; }
/// <summary>
/// Human-readable asset label.
/// </summary>
[Column("asset_label")]
public string AssetLabel { get; init; } = string.Empty;
/// <summary>
/// Package URL of the affected component.
/// </summary>
[Column("purl")]
public string Purl { get; init; } = string.Empty;
/// <summary>
/// CVE identifier (if vulnerability finding).
/// </summary>
[Column("cve_id")]
public string? CveId { get; init; }
/// <summary>
/// Rule identifier (if policy rule finding).
/// </summary>
[Column("rule_id")]
public string? RuleId { get; init; }
/// <summary>
/// When this finding was first seen.
/// </summary>
[Column("first_seen_at")]
public DateTimeOffset FirstSeenAt { get; init; }
/// <summary>
/// When this finding was last seen.
/// </summary>
[Column("last_seen_at")]
public DateTimeOffset LastSeenAt { get; init; }
// Latest risk result fields
/// <summary>
/// Policy ID from latest risk evaluation.
/// </summary>
[Column("policy_id")]
public string? PolicyId { get; init; }
/// <summary>
/// Policy version from latest risk evaluation.
/// </summary>
[Column("policy_version")]
public string? PolicyVersion { get; init; }
/// <summary>
/// Inputs hash from latest risk evaluation.
/// </summary>
[Column("inputs_hash")]
public string? InputsHash { get; init; }
/// <summary>
/// Risk score (0-100).
/// </summary>
[Column("score")]
public int? Score { get; init; }
/// <summary>
/// Final verdict.
/// </summary>
[Column("verdict")]
public TriageVerdict? Verdict { get; init; }
/// <summary>
/// Current triage lane.
/// </summary>
[Column("lane")]
public TriageLane? Lane { get; init; }
/// <summary>
/// Short narrative explaining the current state.
/// </summary>
[Column("why")]
public string? Why { get; init; }
/// <summary>
/// When the risk was last computed.
/// </summary>
[Column("risk_computed_at")]
public DateTimeOffset? RiskComputedAt { get; init; }
// Latest reachability fields
/// <summary>
/// Reachability determination.
/// </summary>
[Column("reachable")]
public TriageReachability Reachable { get; init; }
/// <summary>
/// Reachability confidence (0-100).
/// </summary>
[Column("reach_confidence")]
public short? ReachConfidence { get; init; }
// Latest VEX fields
/// <summary>
/// VEX status.
/// </summary>
[Column("vex_status")]
public TriageVexStatus? VexStatus { get; init; }
/// <summary>
/// VEX issuer.
/// </summary>
[Column("vex_issuer")]
public string? VexIssuer { get; init; }
/// <summary>
/// VEX signature reference.
/// </summary>
[Column("vex_signature_ref")]
public string? VexSignatureRef { get; init; }
/// <summary>
/// VEX source domain.
/// </summary>
[Column("vex_source_domain")]
public string? VexSourceDomain { get; init; }
/// <summary>
/// VEX source reference.
/// </summary>
[Column("vex_source_ref")]
public string? VexSourceRef { get; init; }
}

View File

@@ -0,0 +1,120 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace StellaOps.Scanner.Triage.Entities;
/// <summary>
/// Signed triage decision (mute, ack, exception). Decisions are reversible via revocation.
/// </summary>
[Table("triage_decision")]
public sealed class TriageDecision
{
/// <summary>
/// Unique identifier.
/// </summary>
[Key]
[Column("id")]
public Guid Id { get; init; } = Guid.NewGuid();
/// <summary>
/// The finding this decision applies to.
/// </summary>
[Column("finding_id")]
public Guid FindingId { get; init; }
/// <summary>
/// Type of decision.
/// </summary>
[Column("kind")]
public TriageDecisionKind Kind { get; init; }
/// <summary>
/// Reason code for the decision (from a controlled vocabulary).
/// </summary>
[Required]
[Column("reason_code")]
public required string ReasonCode { get; init; }
/// <summary>
/// Optional freeform note from the decision maker.
/// </summary>
[Column("note")]
public string? Note { get; init; }
/// <summary>
/// Reference to the policy that allowed this decision.
/// </summary>
[Column("policy_ref")]
public string? PolicyRef { get; init; }
/// <summary>
/// Time-to-live for the decision (null = indefinite).
/// </summary>
[Column("ttl")]
public DateTimeOffset? Ttl { get; init; }
/// <summary>
/// Authority subject (sub) of the actor who made the decision.
/// </summary>
[Required]
[Column("actor_subject")]
public required string ActorSubject { get; init; }
/// <summary>
/// Display name of the actor.
/// </summary>
[Column("actor_display")]
public string? ActorDisplay { get; init; }
/// <summary>
/// Reference to DSSE signature.
/// </summary>
[Column("signature_ref")]
public string? SignatureRef { get; init; }
/// <summary>
/// Hash of the DSSE envelope.
/// </summary>
[Column("dsse_hash")]
public string? DsseHash { get; init; }
/// <summary>
/// When the decision was created.
/// </summary>
[Column("created_at")]
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
/// <summary>
/// When the decision was revoked (null = active).
/// </summary>
[Column("revoked_at")]
public DateTimeOffset? RevokedAt { get; set; }
/// <summary>
/// Reason for revocation.
/// </summary>
[Column("revoke_reason")]
public string? RevokeReason { get; set; }
/// <summary>
/// Signature reference for revocation.
/// </summary>
[Column("revoke_signature_ref")]
public string? RevokeSignatureRef { get; set; }
/// <summary>
/// DSSE hash for revocation.
/// </summary>
[Column("revoke_dsse_hash")]
public string? RevokeDsseHash { get; set; }
/// <summary>
/// Whether this decision is currently active.
/// </summary>
[NotMapped]
public bool IsActive => RevokedAt is null;
// Navigation property
[ForeignKey(nameof(FindingId))]
public TriageFinding? Finding { get; init; }
}

View File

@@ -0,0 +1,91 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace StellaOps.Scanner.Triage.Entities;
/// <summary>
/// Effective VEX status for a finding after merging multiple VEX sources.
/// Preserves provenance pointers for auditability.
/// </summary>
[Table("triage_effective_vex")]
public sealed class TriageEffectiveVex
{
/// <summary>
/// Unique identifier.
/// </summary>
[Key]
[Column("id")]
public Guid Id { get; init; } = Guid.NewGuid();
/// <summary>
/// The finding this VEX status applies to.
/// </summary>
[Column("finding_id")]
public Guid FindingId { get; init; }
/// <summary>
/// The effective VEX status after merging.
/// </summary>
[Column("status")]
public TriageVexStatus Status { get; init; }
/// <summary>
/// Source domain that provided this VEX (e.g., "excititor").
/// </summary>
[Required]
[Column("source_domain")]
public required string SourceDomain { get; init; }
/// <summary>
/// Stable reference string to the source document.
/// </summary>
[Required]
[Column("source_ref")]
public required string SourceRef { get; init; }
/// <summary>
/// Array of pruned VEX sources with reasons (for merge transparency).
/// </summary>
[Column("pruned_sources", TypeName = "jsonb")]
public string? PrunedSourcesJson { get; init; }
/// <summary>
/// Hash of the DSSE envelope if signed.
/// </summary>
[Column("dsse_envelope_hash")]
public string? DsseEnvelopeHash { get; init; }
/// <summary>
/// Reference to Rekor/ledger entry for signature verification.
/// </summary>
[Column("signature_ref")]
public string? SignatureRef { get; init; }
/// <summary>
/// Issuer of the VEX document.
/// </summary>
[Column("issuer")]
public string? Issuer { get; init; }
/// <summary>
/// When this VEX status became valid.
/// </summary>
[Column("valid_from")]
public DateTimeOffset ValidFrom { get; init; } = DateTimeOffset.UtcNow;
/// <summary>
/// When this VEX status expires (null = indefinite).
/// </summary>
[Column("valid_to")]
public DateTimeOffset? ValidTo { get; init; }
/// <summary>
/// When this record was collected.
/// </summary>
[Column("collected_at")]
public DateTimeOffset CollectedAt { get; init; } = DateTimeOffset.UtcNow;
// Navigation property
[ForeignKey(nameof(FindingId))]
public TriageFinding? Finding { get; init; }
}

View File

@@ -0,0 +1,151 @@
namespace StellaOps.Scanner.Triage.Entities;
/// <summary>
/// Triage lane indicating the current workflow state of a finding.
/// </summary>
public enum TriageLane
{
/// <summary>Finding is actively being evaluated.</summary>
Active,
/// <summary>Finding is blocking shipment.</summary>
Blocked,
/// <summary>Finding requires a security exception to proceed.</summary>
NeedsException,
/// <summary>Finding is muted due to reachability analysis (not reachable).</summary>
MutedReach,
/// <summary>Finding is muted due to VEX status (not affected).</summary>
MutedVex,
/// <summary>Finding is mitigated by compensating controls.</summary>
Compensated
}
/// <summary>
/// Final verdict for a triage case.
/// </summary>
public enum TriageVerdict
{
/// <summary>Can ship - no blocking issues.</summary>
Ship,
/// <summary>Cannot ship - blocking issues present.</summary>
Block,
/// <summary>Exception granted - can ship with documented exception.</summary>
Exception
}
/// <summary>
/// Reachability determination result.
/// </summary>
public enum TriageReachability
{
/// <summary>Vulnerable code is reachable.</summary>
Yes,
/// <summary>Vulnerable code is not reachable.</summary>
No,
/// <summary>Reachability cannot be determined.</summary>
Unknown
}
/// <summary>
/// VEX status per OpenVEX specification.
/// </summary>
public enum TriageVexStatus
{
/// <summary>Product is affected by the vulnerability.</summary>
Affected,
/// <summary>Product is not affected by the vulnerability.</summary>
NotAffected,
/// <summary>Investigation is ongoing.</summary>
UnderInvestigation,
/// <summary>Status is unknown.</summary>
Unknown
}
/// <summary>
/// Type of triage decision.
/// </summary>
public enum TriageDecisionKind
{
/// <summary>Mute based on reachability analysis.</summary>
MuteReach,
/// <summary>Mute based on VEX status.</summary>
MuteVex,
/// <summary>Acknowledge the finding without action.</summary>
Ack,
/// <summary>Grant a security exception.</summary>
Exception
}
/// <summary>
/// Trigger that caused a triage snapshot to be created.
/// </summary>
public enum TriageSnapshotTrigger
{
/// <summary>Vulnerability feed was updated.</summary>
FeedUpdate,
/// <summary>VEX document was updated.</summary>
VexUpdate,
/// <summary>SBOM was updated.</summary>
SbomUpdate,
/// <summary>Runtime trace was received.</summary>
RuntimeTrace,
/// <summary>Policy was updated.</summary>
PolicyUpdate,
/// <summary>A triage decision was made.</summary>
Decision,
/// <summary>Manual rescan was triggered.</summary>
Rescan
}
/// <summary>
/// Type of evidence artifact attached to a finding.
/// </summary>
public enum TriageEvidenceType
{
/// <summary>Slice of the SBOM relevant to the finding.</summary>
SbomSlice,
/// <summary>VEX document.</summary>
VexDoc,
/// <summary>Build provenance attestation.</summary>
Provenance,
/// <summary>Callstack or callgraph slice.</summary>
CallstackSlice,
/// <summary>Reachability proof document.</summary>
ReachabilityProof,
/// <summary>Replay manifest for deterministic reproduction.</summary>
ReplayManifest,
/// <summary>Policy document that was applied.</summary>
Policy,
/// <summary>Scan log output.</summary>
ScanLog,
/// <summary>Other evidence type.</summary>
Other
}

View File

@@ -0,0 +1,103 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace StellaOps.Scanner.Triage.Entities;
/// <summary>
/// Evidence artifact attached to a finding. Hash-addressed and optionally signed.
/// </summary>
[Table("triage_evidence_artifact")]
public sealed class TriageEvidenceArtifact
{
/// <summary>
/// Unique identifier.
/// </summary>
[Key]
[Column("id")]
public Guid Id { get; init; } = Guid.NewGuid();
/// <summary>
/// The finding this evidence applies to.
/// </summary>
[Column("finding_id")]
public Guid FindingId { get; init; }
/// <summary>
/// Type of evidence.
/// </summary>
[Column("type")]
public TriageEvidenceType Type { get; init; }
/// <summary>
/// Human-readable title for the evidence.
/// </summary>
[Required]
[Column("title")]
public required string Title { get; init; }
/// <summary>
/// Issuer of the evidence (if applicable).
/// </summary>
[Column("issuer")]
public string? Issuer { get; init; }
/// <summary>
/// Whether the evidence is cryptographically signed.
/// </summary>
[Column("signed")]
public bool Signed { get; init; }
/// <summary>
/// Entity that signed the evidence.
/// </summary>
[Column("signed_by")]
public string? SignedBy { get; init; }
/// <summary>
/// Content-addressable hash of the artifact.
/// </summary>
[Required]
[Column("content_hash")]
public required string ContentHash { get; init; }
/// <summary>
/// Reference to the signature.
/// </summary>
[Column("signature_ref")]
public string? SignatureRef { get; init; }
/// <summary>
/// MIME type of the artifact.
/// </summary>
[Column("media_type")]
public string? MediaType { get; init; }
/// <summary>
/// URI to the artifact (object store, file path, or inline reference).
/// </summary>
[Required]
[Column("uri")]
public required string Uri { get; init; }
/// <summary>
/// Size of the artifact in bytes.
/// </summary>
[Column("size_bytes")]
public long? SizeBytes { get; init; }
/// <summary>
/// Additional metadata (JSON).
/// </summary>
[Column("metadata", TypeName = "jsonb")]
public string? MetadataJson { get; init; }
/// <summary>
/// When this artifact was created.
/// </summary>
[Column("created_at")]
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
// Navigation property
[ForeignKey(nameof(FindingId))]
public TriageFinding? Finding { get; init; }
}

View File

@@ -0,0 +1,78 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace StellaOps.Scanner.Triage.Entities;
/// <summary>
/// Represents a triage finding (case). This is the core entity that ties
/// together all triage-related data for a specific vulnerability/rule
/// on a specific asset.
/// </summary>
[Table("triage_finding")]
public sealed class TriageFinding
{
/// <summary>
/// Unique identifier for the finding (also serves as the case ID).
/// </summary>
[Key]
[Column("id")]
public Guid Id { get; init; } = Guid.NewGuid();
/// <summary>
/// The asset this finding applies to.
/// </summary>
[Column("asset_id")]
public Guid AssetId { get; init; }
/// <summary>
/// Optional environment identifier (e.g., prod, staging).
/// </summary>
[Column("environment_id")]
public Guid? EnvironmentId { get; init; }
/// <summary>
/// Human-readable asset label (e.g., "prod/api-gateway:1.2.3").
/// </summary>
[Required]
[Column("asset_label")]
public required string AssetLabel { get; init; }
/// <summary>
/// Package URL identifying the affected component.
/// </summary>
[Required]
[Column("purl")]
public required string Purl { get; init; }
/// <summary>
/// CVE identifier if this is a vulnerability finding.
/// </summary>
[Column("cve_id")]
public string? CveId { get; init; }
/// <summary>
/// Rule identifier if this is a policy rule finding.
/// </summary>
[Column("rule_id")]
public string? RuleId { get; init; }
/// <summary>
/// When this finding was first observed.
/// </summary>
[Column("first_seen_at")]
public DateTimeOffset FirstSeenAt { get; init; } = DateTimeOffset.UtcNow;
/// <summary>
/// When this finding was last observed.
/// </summary>
[Column("last_seen_at")]
public DateTimeOffset LastSeenAt { get; set; } = DateTimeOffset.UtcNow;
// Navigation properties
public ICollection<TriageEffectiveVex> EffectiveVexRecords { get; init; } = new List<TriageEffectiveVex>();
public ICollection<TriageReachabilityResult> ReachabilityResults { get; init; } = new List<TriageReachabilityResult>();
public ICollection<TriageRiskResult> RiskResults { get; init; } = new List<TriageRiskResult>();
public ICollection<TriageDecision> Decisions { get; init; } = new List<TriageDecision>();
public ICollection<TriageEvidenceArtifact> EvidenceArtifacts { get; init; } = new List<TriageEvidenceArtifact>();
public ICollection<TriageSnapshot> Snapshots { get; init; } = new List<TriageSnapshot>();
}

View File

@@ -0,0 +1,66 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace StellaOps.Scanner.Triage.Entities;
/// <summary>
/// Reachability analysis result for a finding.
/// </summary>
[Table("triage_reachability_result")]
public sealed class TriageReachabilityResult
{
/// <summary>
/// Unique identifier.
/// </summary>
[Key]
[Column("id")]
public Guid Id { get; init; } = Guid.NewGuid();
/// <summary>
/// The finding this reachability result applies to.
/// </summary>
[Column("finding_id")]
public Guid FindingId { get; init; }
/// <summary>
/// Reachability determination (Yes, No, Unknown).
/// </summary>
[Column("reachable")]
public TriageReachability Reachable { get; init; }
/// <summary>
/// Confidence level (0-100).
/// </summary>
[Column("confidence")]
[Range(0, 100)]
public short Confidence { get; init; }
/// <summary>
/// Reference to static analysis proof (callgraph slice, CFG slice).
/// </summary>
[Column("static_proof_ref")]
public string? StaticProofRef { get; init; }
/// <summary>
/// Reference to runtime proof (runtime trace hits).
/// </summary>
[Column("runtime_proof_ref")]
public string? RuntimeProofRef { get; init; }
/// <summary>
/// Hash of the inputs used to compute reachability (for caching/diffing).
/// </summary>
[Required]
[Column("inputs_hash")]
public required string InputsHash { get; init; }
/// <summary>
/// When this result was computed.
/// </summary>
[Column("computed_at")]
public DateTimeOffset ComputedAt { get; init; } = DateTimeOffset.UtcNow;
// Navigation property
[ForeignKey(nameof(FindingId))]
public TriageFinding? Finding { get; init; }
}

View File

@@ -0,0 +1,87 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace StellaOps.Scanner.Triage.Entities;
/// <summary>
/// Risk/lattice result from the scanner's policy evaluation.
/// </summary>
[Table("triage_risk_result")]
public sealed class TriageRiskResult
{
/// <summary>
/// Unique identifier.
/// </summary>
[Key]
[Column("id")]
public Guid Id { get; init; } = Guid.NewGuid();
/// <summary>
/// The finding this risk result applies to.
/// </summary>
[Column("finding_id")]
public Guid FindingId { get; init; }
/// <summary>
/// The policy that was applied.
/// </summary>
[Required]
[Column("policy_id")]
public required string PolicyId { get; init; }
/// <summary>
/// Version of the policy that was applied.
/// </summary>
[Required]
[Column("policy_version")]
public required string PolicyVersion { get; init; }
/// <summary>
/// Hash of the inputs used for this evaluation.
/// </summary>
[Required]
[Column("inputs_hash")]
public required string InputsHash { get; init; }
/// <summary>
/// Computed risk score (0-100).
/// </summary>
[Column("score")]
[Range(0, 100)]
public int Score { get; init; }
/// <summary>
/// Final verdict (Ship, Block, Exception).
/// </summary>
[Column("verdict")]
public TriageVerdict Verdict { get; init; }
/// <summary>
/// Current lane based on policy evaluation.
/// </summary>
[Column("lane")]
public TriageLane Lane { get; init; }
/// <summary>
/// Short narrative explaining the decision.
/// </summary>
[Required]
[Column("why")]
public required string Why { get; init; }
/// <summary>
/// Structured lattice explanation for UI diffing (JSON).
/// </summary>
[Column("explanation", TypeName = "jsonb")]
public string? ExplanationJson { get; init; }
/// <summary>
/// When this result was computed.
/// </summary>
[Column("computed_at")]
public DateTimeOffset ComputedAt { get; init; } = DateTimeOffset.UtcNow;
// Navigation property
[ForeignKey(nameof(FindingId))]
public TriageFinding? Finding { get; init; }
}

View File

@@ -0,0 +1,66 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace StellaOps.Scanner.Triage.Entities;
/// <summary>
/// Immutable snapshot record for Smart-Diff, capturing input/output changes.
/// </summary>
[Table("triage_snapshot")]
public sealed class TriageSnapshot
{
/// <summary>
/// Unique identifier.
/// </summary>
[Key]
[Column("id")]
public Guid Id { get; init; } = Guid.NewGuid();
/// <summary>
/// The finding this snapshot applies to.
/// </summary>
[Column("finding_id")]
public Guid FindingId { get; init; }
/// <summary>
/// What triggered this snapshot.
/// </summary>
[Column("trigger")]
public TriageSnapshotTrigger Trigger { get; init; }
/// <summary>
/// Previous inputs hash (null for first snapshot).
/// </summary>
[Column("from_inputs_hash")]
public string? FromInputsHash { get; init; }
/// <summary>
/// New inputs hash.
/// </summary>
[Required]
[Column("to_inputs_hash")]
public required string ToInputsHash { get; init; }
/// <summary>
/// Human-readable summary of what changed.
/// </summary>
[Required]
[Column("summary")]
public required string Summary { get; init; }
/// <summary>
/// Precomputed diff in JSON format (optional).
/// </summary>
[Column("diff_json", TypeName = "jsonb")]
public string? DiffJson { get; init; }
/// <summary>
/// When this snapshot was created.
/// </summary>
[Column("created_at")]
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
// Navigation property
[ForeignKey(nameof(FindingId))]
public TriageFinding? Finding { get; init; }
}