feat: add security sink detection patterns for JavaScript/TypeScript

- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations).
- Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns.
- Added `package-lock.json` for dependency management.
This commit is contained in:
StellaOps Bot
2025-12-22 23:21:21 +02:00
parent 3ba7157b00
commit 5146204f1b
529 changed files with 73579 additions and 5985 deletions

View File

@@ -0,0 +1,228 @@
// -----------------------------------------------------------------------------
// BaselineContracts.cs
// Sprint: SPRINT_4200_0002_0006_delta_compare_api
// Description: DTOs for baseline selection API.
// -----------------------------------------------------------------------------
namespace StellaOps.Scanner.WebService.Contracts;
/// <summary>
/// A recommended baseline for comparison.
/// </summary>
public sealed record BaselineRecommendationDto
{
/// <summary>
/// Unique identifier for this recommendation.
/// </summary>
public required string Id { get; init; }
/// <summary>
/// Type of baseline: last-green, previous-release, main-branch, parent-commit, custom.
/// </summary>
public required string Type { get; init; }
/// <summary>
/// Human-readable label.
/// </summary>
public required string Label { get; init; }
/// <summary>
/// Artifact digest for this baseline.
/// </summary>
public required string Digest { get; init; }
/// <summary>
/// When this baseline was scanned.
/// </summary>
public DateTimeOffset? Timestamp { get; init; }
/// <summary>
/// Why this baseline was recommended.
/// </summary>
public required string Rationale { get; init; }
/// <summary>
/// Verdict status: allowed, blocked, warn, unknown.
/// </summary>
public string? VerdictStatus { get; init; }
/// <summary>
/// Policy version used for the baseline verdict.
/// </summary>
public string? PolicyVersion { get; init; }
/// <summary>
/// Whether this is the default/recommended baseline.
/// </summary>
public bool IsDefault { get; init; }
}
/// <summary>
/// Response containing baseline recommendations.
/// </summary>
public sealed record BaselineRecommendationsResponseDto
{
/// <summary>
/// The artifact being compared.
/// </summary>
public required string ArtifactDigest { get; init; }
/// <summary>
/// List of recommended baselines, ordered by relevance.
/// </summary>
public required IReadOnlyList<BaselineRecommendationDto> Recommendations { get; init; }
/// <summary>
/// When recommendations were generated.
/// </summary>
public required DateTimeOffset GeneratedAt { get; init; }
}
/// <summary>
/// Detailed rationale for a baseline selection.
/// </summary>
public sealed record BaselineRationaleResponseDto
{
/// <summary>
/// Base artifact digest.
/// </summary>
public required string BaseDigest { get; init; }
/// <summary>
/// Head/target artifact digest.
/// </summary>
public required string HeadDigest { get; init; }
/// <summary>
/// How this baseline was selected: last-green, previous-release, manual.
/// </summary>
public required string SelectionType { get; init; }
/// <summary>
/// Short rationale text.
/// </summary>
public required string Rationale { get; init; }
/// <summary>
/// Detailed explanation for auditors.
/// </summary>
public required string DetailedExplanation { get; init; }
/// <summary>
/// Criteria used for selection.
/// </summary>
public IReadOnlyList<string>? SelectionCriteria { get; init; }
/// <summary>
/// When the base was scanned.
/// </summary>
public DateTimeOffset? BaseTimestamp { get; init; }
/// <summary>
/// When the head was scanned.
/// </summary>
public DateTimeOffset? HeadTimestamp { get; init; }
}
/// <summary>
/// Actionable recommendation for remediation.
/// </summary>
public sealed record ActionableDto
{
/// <summary>
/// Unique identifier for this actionable.
/// </summary>
public required string Id { get; init; }
/// <summary>
/// Type: upgrade, patch, vex, config, investigate.
/// </summary>
public required string Type { get; init; }
/// <summary>
/// Priority: critical, high, medium, low.
/// </summary>
public required string Priority { get; init; }
/// <summary>
/// Short title.
/// </summary>
public required string Title { get; init; }
/// <summary>
/// Detailed description.
/// </summary>
public required string Description { get; init; }
/// <summary>
/// Affected component PURL.
/// </summary>
public string? Component { get; init; }
/// <summary>
/// Current version.
/// </summary>
public string? CurrentVersion { get; init; }
/// <summary>
/// Target version to upgrade to.
/// </summary>
public string? TargetVersion { get; init; }
/// <summary>
/// Related CVE IDs.
/// </summary>
public IReadOnlyList<string>? CveIds { get; init; }
/// <summary>
/// Estimated effort: trivial, low, medium, high.
/// </summary>
public string? EstimatedEffort { get; init; }
/// <summary>
/// Supporting evidence references.
/// </summary>
public ActionableEvidenceDto? Evidence { get; init; }
}
/// <summary>
/// Evidence supporting an actionable.
/// </summary>
public sealed record ActionableEvidenceDto
{
/// <summary>
/// Witness path ID for reachability evidence.
/// </summary>
public string? WitnessId { get; init; }
/// <summary>
/// VEX document ID.
/// </summary>
public string? VexDocumentId { get; init; }
/// <summary>
/// Policy rule ID that triggered this.
/// </summary>
public string? PolicyRuleId { get; init; }
}
/// <summary>
/// Response containing actionables for a delta.
/// </summary>
public sealed record ActionablesResponseDto
{
/// <summary>
/// Delta ID these actionables are for.
/// </summary>
public required string DeltaId { get; init; }
/// <summary>
/// List of actionables, sorted by priority.
/// </summary>
public required IReadOnlyList<ActionableDto> Actionables { get; init; }
/// <summary>
/// When actionables were generated.
/// </summary>
public required DateTimeOffset GeneratedAt { get; init; }
}

View File

@@ -0,0 +1,440 @@
// -----------------------------------------------------------------------------
// DeltaCompareContracts.cs
// Sprint: SPRINT_4200_0002_0006_delta_compare_api
// Description: DTOs for delta/compare view backend API.
// -----------------------------------------------------------------------------
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.WebService.Contracts;
/// <summary>
/// Request to compare two scan snapshots.
/// </summary>
public sealed record DeltaCompareRequestDto
{
/// <summary>
/// Base snapshot digest (the "before" state).
/// </summary>
public required string BaseDigest { get; init; }
/// <summary>
/// Target snapshot digest (the "after" state).
/// </summary>
public required string TargetDigest { get; init; }
/// <summary>
/// Optional filter for change types.
/// </summary>
public IReadOnlyList<string>? ChangeTypes { get; init; }
/// <summary>
/// Optional filter for severity levels.
/// </summary>
public IReadOnlyList<string>? Severities { get; init; }
/// <summary>
/// Include findings that are unchanged.
/// </summary>
public bool IncludeUnchanged { get; init; }
/// <summary>
/// Include component-level diff.
/// </summary>
public bool IncludeComponents { get; init; } = true;
/// <summary>
/// Include vulnerability-level diff.
/// </summary>
public bool IncludeVulnerabilities { get; init; } = true;
/// <summary>
/// Include policy verdict diff.
/// </summary>
public bool IncludePolicyDiff { get; init; } = true;
}
/// <summary>
/// Response containing the delta comparison results.
/// </summary>
public sealed record DeltaCompareResponseDto
{
/// <summary>
/// Base snapshot summary.
/// </summary>
public required DeltaSnapshotSummaryDto Base { get; init; }
/// <summary>
/// Target snapshot summary.
/// </summary>
public required DeltaSnapshotSummaryDto Target { get; init; }
/// <summary>
/// Summary of changes.
/// </summary>
public required DeltaChangeSummaryDto Summary { get; init; }
/// <summary>
/// Vulnerability changes.
/// </summary>
public IReadOnlyList<DeltaVulnerabilityDto>? Vulnerabilities { get; init; }
/// <summary>
/// Component changes.
/// </summary>
public IReadOnlyList<DeltaComponentDto>? Components { get; init; }
/// <summary>
/// Policy verdict changes.
/// </summary>
public DeltaPolicyDiffDto? PolicyDiff { get; init; }
/// <summary>
/// When this comparison was generated.
/// </summary>
public required DateTimeOffset GeneratedAt { get; init; }
/// <summary>
/// Deterministic comparison ID for caching.
/// </summary>
public required string ComparisonId { get; init; }
}
/// <summary>
/// Summary of a scan snapshot.
/// </summary>
public sealed record DeltaSnapshotSummaryDto
{
/// <summary>
/// Digest of the snapshot.
/// </summary>
public required string Digest { get; init; }
/// <summary>
/// When the snapshot was created.
/// </summary>
public DateTimeOffset? CreatedAt { get; init; }
/// <summary>
/// Total component count.
/// </summary>
public int ComponentCount { get; init; }
/// <summary>
/// Total vulnerability count.
/// </summary>
public int VulnerabilityCount { get; init; }
/// <summary>
/// Count by severity.
/// </summary>
public required DeltaSeverityCountsDto SeverityCounts { get; init; }
/// <summary>
/// Overall policy verdict.
/// </summary>
public string? PolicyVerdict { get; init; }
}
/// <summary>
/// Counts by severity level.
/// </summary>
public sealed record DeltaSeverityCountsDto
{
public int Critical { get; init; }
public int High { get; init; }
public int Medium { get; init; }
public int Low { get; init; }
public int Unknown { get; init; }
}
/// <summary>
/// Summary of changes between snapshots.
/// </summary>
public sealed record DeltaChangeSummaryDto
{
/// <summary>
/// Number of added findings.
/// </summary>
public int Added { get; init; }
/// <summary>
/// Number of removed findings.
/// </summary>
public int Removed { get; init; }
/// <summary>
/// Number of modified findings.
/// </summary>
public int Modified { get; init; }
/// <summary>
/// Number of unchanged findings.
/// </summary>
public int Unchanged { get; init; }
/// <summary>
/// Net change in vulnerability count.
/// </summary>
public int NetVulnerabilityChange { get; init; }
/// <summary>
/// Net change in component count.
/// </summary>
public int NetComponentChange { get; init; }
/// <summary>
/// Severity changes summary.
/// </summary>
public required DeltaSeverityChangesDto SeverityChanges { get; init; }
/// <summary>
/// Whether the policy verdict changed.
/// </summary>
public bool VerdictChanged { get; init; }
/// <summary>
/// Direction of risk change (improved, degraded, unchanged).
/// </summary>
public required string RiskDirection { get; init; }
}
/// <summary>
/// Changes in severity counts.
/// </summary>
public sealed record DeltaSeverityChangesDto
{
public int CriticalAdded { get; init; }
public int CriticalRemoved { get; init; }
public int HighAdded { get; init; }
public int HighRemoved { get; init; }
public int MediumAdded { get; init; }
public int MediumRemoved { get; init; }
public int LowAdded { get; init; }
public int LowRemoved { get; init; }
}
/// <summary>
/// Individual vulnerability change.
/// </summary>
public sealed record DeltaVulnerabilityDto
{
/// <summary>
/// Vulnerability ID (CVE).
/// </summary>
public required string VulnId { get; init; }
/// <summary>
/// Package URL of affected component.
/// </summary>
public required string Purl { get; init; }
/// <summary>
/// Type of change (Added, Removed, Modified, Unchanged).
/// </summary>
public required string ChangeType { get; init; }
/// <summary>
/// Severity level.
/// </summary>
public required string Severity { get; init; }
/// <summary>
/// Previous severity if changed.
/// </summary>
public string? PreviousSeverity { get; init; }
/// <summary>
/// VEX status.
/// </summary>
public string? VexStatus { get; init; }
/// <summary>
/// Previous VEX status if changed.
/// </summary>
public string? PreviousVexStatus { get; init; }
/// <summary>
/// Reachability status.
/// </summary>
public string? Reachability { get; init; }
/// <summary>
/// Previous reachability if changed.
/// </summary>
public string? PreviousReachability { get; init; }
/// <summary>
/// Policy verdict for this finding.
/// </summary>
public string? Verdict { get; init; }
/// <summary>
/// Previous verdict if changed.
/// </summary>
public string? PreviousVerdict { get; init; }
/// <summary>
/// Fixed version if available.
/// </summary>
public string? FixedVersion { get; init; }
/// <summary>
/// Detailed field-level changes.
/// </summary>
public IReadOnlyList<DeltaFieldChangeDto>? FieldChanges { get; init; }
}
/// <summary>
/// Individual component change.
/// </summary>
public sealed record DeltaComponentDto
{
/// <summary>
/// Package URL.
/// </summary>
public required string Purl { get; init; }
/// <summary>
/// Type of change (Added, Removed, VersionChanged, Unchanged).
/// </summary>
public required string ChangeType { get; init; }
/// <summary>
/// Previous version if changed.
/// </summary>
public string? PreviousVersion { get; init; }
/// <summary>
/// Current version.
/// </summary>
public string? CurrentVersion { get; init; }
/// <summary>
/// Vulnerabilities in base snapshot.
/// </summary>
public int VulnerabilitiesInBase { get; init; }
/// <summary>
/// Vulnerabilities in target snapshot.
/// </summary>
public int VulnerabilitiesInTarget { get; init; }
/// <summary>
/// License.
/// </summary>
public string? License { get; init; }
}
/// <summary>
/// Field-level change detail.
/// </summary>
public sealed record DeltaFieldChangeDto
{
/// <summary>
/// Field name.
/// </summary>
public required string Field { get; init; }
/// <summary>
/// Previous value.
/// </summary>
public string? PreviousValue { get; init; }
/// <summary>
/// Current value.
/// </summary>
public string? CurrentValue { get; init; }
}
/// <summary>
/// Policy diff between snapshots.
/// </summary>
public sealed record DeltaPolicyDiffDto
{
/// <summary>
/// Base verdict.
/// </summary>
public required string BaseVerdict { get; init; }
/// <summary>
/// Target verdict.
/// </summary>
public required string TargetVerdict { get; init; }
/// <summary>
/// Whether verdict changed.
/// </summary>
public bool VerdictChanged { get; init; }
/// <summary>
/// Findings that changed from Block to Ship.
/// </summary>
public int BlockToShipCount { get; init; }
/// <summary>
/// Findings that changed from Ship to Block.
/// </summary>
public int ShipToBlockCount { get; init; }
/// <summary>
/// Counterfactuals for blocking findings.
/// </summary>
public IReadOnlyList<string>? WouldPassIf { get; init; }
}
/// <summary>
/// Quick diff summary for header display (Can I Ship?).
/// </summary>
public sealed record QuickDiffSummaryDto
{
/// <summary>
/// Base digest.
/// </summary>
public required string BaseDigest { get; init; }
/// <summary>
/// Target digest.
/// </summary>
public required string TargetDigest { get; init; }
/// <summary>
/// Can the target ship? (true if policy verdict is Pass).
/// </summary>
public bool CanShip { get; init; }
/// <summary>
/// Risk direction (improved, degraded, unchanged).
/// </summary>
public required string RiskDirection { get; init; }
/// <summary>
/// Net change in blocking findings.
/// </summary>
public int NetBlockingChange { get; init; }
/// <summary>
/// Critical vulns added.
/// </summary>
public int CriticalAdded { get; init; }
/// <summary>
/// Critical vulns removed.
/// </summary>
public int CriticalRemoved { get; init; }
/// <summary>
/// High vulns added.
/// </summary>
public int HighAdded { get; init; }
/// <summary>
/// High vulns removed.
/// </summary>
public int HighRemoved { get; init; }
/// <summary>
/// Summary message.
/// </summary>
public required string Summary { get; init; }
}

View File

@@ -67,6 +67,15 @@ public sealed record ReportDocumentDto
[JsonPropertyOrder(9)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyList<LinksetSummaryDto>? Linksets { get; init; }
/// <summary>
/// Unknown budget status for this scan.
/// Sprint: SPRINT_4300_0002_0001 (BUDGET-017)
/// </summary>
[JsonPropertyName("unknownBudget")]
[JsonPropertyOrder(10)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public UnknownBudgetSectionDto? UnknownBudget { get; init; }
}
public sealed record ReportPolicyDto
@@ -102,3 +111,112 @@ public sealed record ReportSummaryDto
[JsonPropertyOrder(4)]
public int Quieted { get; init; }
}
/// <summary>
/// Unknown budget status section for scan reports.
/// Sprint: SPRINT_4300_0002_0001 (BUDGET-017)
/// </summary>
public sealed record UnknownBudgetSectionDto
{
/// <summary>
/// Environment against which budget was evaluated.
/// </summary>
[JsonPropertyName("environment")]
[JsonPropertyOrder(0)]
public string Environment { get; init; } = string.Empty;
/// <summary>
/// Whether the scan is within the budget limits.
/// </summary>
[JsonPropertyName("withinBudget")]
[JsonPropertyOrder(1)]
public bool WithinBudget { get; init; }
/// <summary>
/// Recommended action: "pass", "warn", or "block".
/// </summary>
[JsonPropertyName("action")]
[JsonPropertyOrder(2)]
public string Action { get; init; } = "pass";
/// <summary>
/// Total unknown count in this scan.
/// </summary>
[JsonPropertyName("totalUnknowns")]
[JsonPropertyOrder(3)]
public int TotalUnknowns { get; init; }
/// <summary>
/// Configured total limit for the environment.
/// </summary>
[JsonPropertyName("totalLimit")]
[JsonPropertyOrder(4)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? TotalLimit { get; init; }
/// <summary>
/// Percentage of budget used (0-100).
/// </summary>
[JsonPropertyName("percentageUsed")]
[JsonPropertyOrder(5)]
public decimal PercentageUsed { get; init; }
/// <summary>
/// Budget violations by reason code.
/// </summary>
[JsonPropertyName("violations")]
[JsonPropertyOrder(6)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyList<UnknownBudgetViolationDto>? Violations { get; init; }
/// <summary>
/// Message describing budget status.
/// </summary>
[JsonPropertyName("message")]
[JsonPropertyOrder(7)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Message { get; init; }
/// <summary>
/// Breakdown of unknowns by reason code.
/// </summary>
[JsonPropertyName("byReasonCode")]
[JsonPropertyOrder(8)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary<string, int>? ByReasonCode { get; init; }
}
/// <summary>
/// Details of a specific budget violation.
/// Sprint: SPRINT_4300_0002_0001 (BUDGET-017)
/// </summary>
public sealed record UnknownBudgetViolationDto
{
/// <summary>
/// Reason code that exceeded its limit.
/// </summary>
[JsonPropertyName("reasonCode")]
[JsonPropertyOrder(0)]
public string ReasonCode { get; init; } = string.Empty;
/// <summary>
/// Short code for the reason (e.g., "U-RCH").
/// </summary>
[JsonPropertyName("shortCode")]
[JsonPropertyOrder(1)]
public string ShortCode { get; init; } = string.Empty;
/// <summary>
/// Actual count of unknowns for this reason.
/// </summary>
[JsonPropertyName("count")]
[JsonPropertyOrder(2)]
public int Count { get; init; }
/// <summary>
/// Configured limit for this reason.
/// </summary>
[JsonPropertyName("limit")]
[JsonPropertyOrder(3)]
public int Limit { get; init; }
}

View File

@@ -0,0 +1,464 @@
// -----------------------------------------------------------------------------
// TriageContracts.cs
// Sprint: SPRINT_4200_0001_0001_triage_rest_api
// Description: DTOs for triage status REST API.
// -----------------------------------------------------------------------------
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.WebService.Contracts;
/// <summary>
/// Response DTO for finding triage status.
/// </summary>
public sealed record FindingTriageStatusDto
{
/// <summary>
/// Unique finding identifier.
/// </summary>
public required string FindingId { get; init; }
/// <summary>
/// Current triage lane.
/// </summary>
public required string Lane { get; init; }
/// <summary>
/// Final verdict (Ship/Block/Exception).
/// </summary>
public required string Verdict { get; init; }
/// <summary>
/// Human-readable reason for the current status.
/// </summary>
public string? Reason { get; init; }
/// <summary>
/// VEX status if applicable.
/// </summary>
public TriageVexStatusDto? VexStatus { get; init; }
/// <summary>
/// Reachability determination if applicable.
/// </summary>
public TriageReachabilityDto? Reachability { get; init; }
/// <summary>
/// Risk score information.
/// </summary>
public TriageRiskScoreDto? RiskScore { get; init; }
/// <summary>
/// Policy counterfactuals - what would flip this to Ship.
/// </summary>
public IReadOnlyList<string>? WouldPassIf { get; init; }
/// <summary>
/// Attached evidence artifacts.
/// </summary>
public IReadOnlyList<TriageEvidenceDto>? Evidence { get; init; }
/// <summary>
/// When this status was last computed.
/// </summary>
public DateTimeOffset? ComputedAt { get; init; }
/// <summary>
/// Link to proof bundle for this finding.
/// </summary>
public string? ProofBundleUri { get; init; }
}
/// <summary>
/// VEX status DTO.
/// </summary>
public sealed record TriageVexStatusDto
{
/// <summary>
/// Status value (Affected, NotAffected, UnderInvestigation, Unknown).
/// </summary>
public required string Status { get; init; }
/// <summary>
/// Justification category for NotAffected status.
/// </summary>
public string? Justification { get; init; }
/// <summary>
/// Impact statement explaining the decision.
/// </summary>
public string? ImpactStatement { get; init; }
/// <summary>
/// Who issued the VEX statement.
/// </summary>
public string? IssuedBy { get; init; }
/// <summary>
/// When the VEX statement was issued.
/// </summary>
public DateTimeOffset? IssuedAt { get; init; }
/// <summary>
/// Reference to the VEX document.
/// </summary>
public string? VexDocumentRef { get; init; }
}
/// <summary>
/// Reachability determination DTO.
/// </summary>
public sealed record TriageReachabilityDto
{
/// <summary>
/// Status (Yes, No, Unknown).
/// </summary>
public required string Status { get; init; }
/// <summary>
/// Confidence level (0-1).
/// </summary>
public double? Confidence { get; init; }
/// <summary>
/// Source of the reachability determination.
/// </summary>
public string? Source { get; init; }
/// <summary>
/// Entry points if reachable.
/// </summary>
public IReadOnlyList<string>? EntryPoints { get; init; }
/// <summary>
/// When the analysis was performed.
/// </summary>
public DateTimeOffset? AnalyzedAt { get; init; }
}
/// <summary>
/// Risk score DTO.
/// </summary>
public sealed record TriageRiskScoreDto
{
/// <summary>
/// Computed risk score (0-10).
/// </summary>
public double Score { get; init; }
/// <summary>
/// Critical severity count.
/// </summary>
public int CriticalCount { get; init; }
/// <summary>
/// High severity count.
/// </summary>
public int HighCount { get; init; }
/// <summary>
/// Medium severity count.
/// </summary>
public int MediumCount { get; init; }
/// <summary>
/// Low severity count.
/// </summary>
public int LowCount { get; init; }
/// <summary>
/// EPSS probability if available.
/// </summary>
public double? EpssScore { get; init; }
/// <summary>
/// EPSS percentile if available.
/// </summary>
public double? EpssPercentile { get; init; }
}
/// <summary>
/// Evidence artifact DTO.
/// </summary>
public sealed record TriageEvidenceDto
{
/// <summary>
/// Evidence type.
/// </summary>
public required string Type { get; init; }
/// <summary>
/// URI to retrieve the evidence.
/// </summary>
public required string Uri { get; init; }
/// <summary>
/// Content digest (sha256).
/// </summary>
public string? Digest { get; init; }
/// <summary>
/// When this evidence was created.
/// </summary>
public DateTimeOffset? CreatedAt { get; init; }
}
/// <summary>
/// Request to update finding triage status.
/// </summary>
public sealed record UpdateTriageStatusRequestDto
{
/// <summary>
/// New lane to move the finding to.
/// </summary>
public string? Lane { get; init; }
/// <summary>
/// Decision kind (MuteReach, MuteVex, Ack, Exception).
/// </summary>
public string? DecisionKind { get; init; }
/// <summary>
/// Reason/justification for the change.
/// </summary>
public string? Reason { get; init; }
/// <summary>
/// Exception details if DecisionKind is Exception.
/// </summary>
public TriageExceptionRequestDto? Exception { get; init; }
/// <summary>
/// Actor making the change.
/// </summary>
public string? Actor { get; init; }
}
/// <summary>
/// Exception request details.
/// </summary>
public sealed record TriageExceptionRequestDto
{
/// <summary>
/// When the exception expires.
/// </summary>
public DateTimeOffset? ExpiresAt { get; init; }
/// <summary>
/// Approver identifier.
/// </summary>
public string? ApprovedBy { get; init; }
/// <summary>
/// Ticket/reference for the exception.
/// </summary>
public string? TicketRef { get; init; }
/// <summary>
/// Compensating controls applied.
/// </summary>
public IReadOnlyList<string>? CompensatingControls { get; init; }
}
/// <summary>
/// Response after updating triage status.
/// </summary>
public sealed record UpdateTriageStatusResponseDto
{
/// <summary>
/// The finding ID.
/// </summary>
public required string FindingId { get; init; }
/// <summary>
/// Previous lane.
/// </summary>
public required string PreviousLane { get; init; }
/// <summary>
/// New lane.
/// </summary>
public required string NewLane { get; init; }
/// <summary>
/// Previous verdict.
/// </summary>
public required string PreviousVerdict { get; init; }
/// <summary>
/// New verdict.
/// </summary>
public required string NewVerdict { get; init; }
/// <summary>
/// Snapshot ID for audit trail.
/// </summary>
public string? SnapshotId { get; init; }
/// <summary>
/// When the change was applied.
/// </summary>
public required DateTimeOffset AppliedAt { get; init; }
}
/// <summary>
/// Request to submit a VEX statement for a finding.
/// </summary>
public sealed record SubmitVexStatementRequestDto
{
/// <summary>
/// VEX status (Affected, NotAffected, UnderInvestigation, Unknown).
/// </summary>
public required string Status { get; init; }
/// <summary>
/// Justification category for NotAffected.
/// Per OpenVEX: component_not_present, vulnerable_code_not_present,
/// vulnerable_code_not_in_execute_path, inline_mitigations_already_exist.
/// </summary>
public string? Justification { get; init; }
/// <summary>
/// Impact statement.
/// </summary>
public string? ImpactStatement { get; init; }
/// <summary>
/// Action statement for remediation.
/// </summary>
public string? ActionStatement { get; init; }
/// <summary>
/// When the VEX statement becomes effective.
/// </summary>
public DateTimeOffset? EffectiveAt { get; init; }
/// <summary>
/// Actor submitting the VEX statement.
/// </summary>
public string? IssuedBy { get; init; }
}
/// <summary>
/// Response after submitting VEX statement.
/// </summary>
public sealed record SubmitVexStatementResponseDto
{
/// <summary>
/// VEX statement ID.
/// </summary>
public required string VexStatementId { get; init; }
/// <summary>
/// Finding ID this applies to.
/// </summary>
public required string FindingId { get; init; }
/// <summary>
/// The applied status.
/// </summary>
public required string Status { get; init; }
/// <summary>
/// Whether this changed the triage verdict.
/// </summary>
public bool VerdictChanged { get; init; }
/// <summary>
/// New verdict if changed.
/// </summary>
public string? NewVerdict { get; init; }
/// <summary>
/// When the statement was recorded.
/// </summary>
public required DateTimeOffset RecordedAt { get; init; }
}
/// <summary>
/// Bulk triage query request.
/// </summary>
public sealed record BulkTriageQueryRequestDto
{
/// <summary>
/// Artifact digest (image or SBOM).
/// </summary>
public string? ArtifactDigest { get; init; }
/// <summary>
/// Filter by lane.
/// </summary>
public string? Lane { get; init; }
/// <summary>
/// Filter by verdict.
/// </summary>
public string? Verdict { get; init; }
/// <summary>
/// Filter by CVE ID prefix.
/// </summary>
public string? CvePrefix { get; init; }
/// <summary>
/// Maximum results.
/// </summary>
public int? Limit { get; init; }
/// <summary>
/// Pagination cursor.
/// </summary>
public string? Cursor { get; init; }
}
/// <summary>
/// Bulk triage query response.
/// </summary>
public sealed record BulkTriageQueryResponseDto
{
/// <summary>
/// The findings matching the query.
/// </summary>
public required IReadOnlyList<FindingTriageStatusDto> Findings { get; init; }
/// <summary>
/// Total count matching the query.
/// </summary>
public int TotalCount { get; init; }
/// <summary>
/// Next cursor for pagination.
/// </summary>
public string? NextCursor { get; init; }
/// <summary>
/// Summary statistics.
/// </summary>
public TriageSummaryDto? Summary { get; init; }
}
/// <summary>
/// Summary statistics for triage.
/// </summary>
public sealed record TriageSummaryDto
{
/// <summary>
/// Count by lane.
/// </summary>
public required IDictionary<string, int> ByLane { get; init; }
/// <summary>
/// Count by verdict.
/// </summary>
public required IDictionary<string, int> ByVerdict { get; init; }
/// <summary>
/// Findings that can ship.
/// </summary>
public int CanShipCount { get; init; }
/// <summary>
/// Findings that block shipment.
/// </summary>
public int BlockingCount { get; init; }
}