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:
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user