Add comprehensive security tests for OWASP A02, A05, A07, and A08 categories
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
- Implemented tests for Cryptographic Failures (A02) to ensure proper handling of sensitive data, secure algorithms, and key management. - Added tests for Security Misconfiguration (A05) to validate production configurations, security headers, CORS settings, and feature management. - Developed tests for Authentication Failures (A07) to enforce strong password policies, rate limiting, session management, and MFA support. - Created tests for Software and Data Integrity Failures (A08) to verify artifact signatures, SBOM integrity, attestation chains, and feed updates.
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.SmartDiff.Detection;
|
||||
|
||||
/// <summary>
|
||||
/// Detects material risk changes between two scan snapshots.
|
||||
/// Implements rules R1-R4 from the Smart-Diff advisory.
|
||||
/// Per Sprint 3500.3 - Smart-Diff Detection Rules.
|
||||
/// </summary>
|
||||
public sealed class MaterialRiskChangeDetector
|
||||
{
|
||||
private readonly MaterialRiskChangeOptions _options;
|
||||
|
||||
public MaterialRiskChangeDetector(MaterialRiskChangeOptions? options = null)
|
||||
{
|
||||
_options = options ?? MaterialRiskChangeOptions.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two snapshots and returns all material changes.
|
||||
/// </summary>
|
||||
public MaterialRiskChangeResult Compare(
|
||||
RiskStateSnapshot previous,
|
||||
RiskStateSnapshot current)
|
||||
{
|
||||
if (previous.FindingKey != current.FindingKey)
|
||||
throw new ArgumentException("FindingKey mismatch between snapshots");
|
||||
|
||||
var changes = new List<DetectedChange>();
|
||||
|
||||
// Rule R1: Reachability Flip
|
||||
var r1 = EvaluateReachabilityFlip(previous, current);
|
||||
if (r1 is not null) changes.Add(r1);
|
||||
|
||||
// Rule R2: VEX Status Flip
|
||||
var r2 = EvaluateVexFlip(previous, current);
|
||||
if (r2 is not null) changes.Add(r2);
|
||||
|
||||
// Rule R3: Affected Range Boundary
|
||||
var r3 = EvaluateRangeBoundary(previous, current);
|
||||
if (r3 is not null) changes.Add(r3);
|
||||
|
||||
// Rule R4: Intelligence/Policy Flip
|
||||
var r4Changes = EvaluateIntelligenceFlip(previous, current);
|
||||
changes.AddRange(r4Changes);
|
||||
|
||||
var hasMaterialChange = changes.Count > 0;
|
||||
var priorityScore = hasMaterialChange ? ComputePriorityScore(changes, current) : 0;
|
||||
|
||||
return new MaterialRiskChangeResult(
|
||||
FindingKey: current.FindingKey,
|
||||
HasMaterialChange: hasMaterialChange,
|
||||
Changes: [.. changes],
|
||||
PriorityScore: priorityScore,
|
||||
PreviousStateHash: previous.ComputeStateHash(),
|
||||
CurrentStateHash: current.ComputeStateHash());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R1: Reachability Flip - reachable changes false→true or true→false
|
||||
/// </summary>
|
||||
private DetectedChange? EvaluateReachabilityFlip(
|
||||
RiskStateSnapshot prev,
|
||||
RiskStateSnapshot curr)
|
||||
{
|
||||
if (prev.Reachable == curr.Reachable)
|
||||
return null;
|
||||
|
||||
// Skip if either is unknown
|
||||
if (prev.Reachable is null || curr.Reachable is null)
|
||||
return null;
|
||||
|
||||
var direction = curr.Reachable.Value
|
||||
? RiskDirection.Increased
|
||||
: RiskDirection.Decreased;
|
||||
|
||||
return new DetectedChange(
|
||||
Rule: DetectionRule.R1_ReachabilityFlip,
|
||||
ChangeType: MaterialChangeType.ReachabilityFlip,
|
||||
Direction: direction,
|
||||
Reason: $"Reachability changed from {prev.Reachable} to {curr.Reachable}",
|
||||
PreviousValue: prev.Reachable.ToString()!,
|
||||
CurrentValue: curr.Reachable.ToString()!,
|
||||
Weight: direction == RiskDirection.Increased
|
||||
? _options.ReachabilityFlipUpWeight
|
||||
: _options.ReachabilityFlipDownWeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R2: VEX Status Flip - meaningful status transitions
|
||||
/// </summary>
|
||||
private DetectedChange? EvaluateVexFlip(
|
||||
RiskStateSnapshot prev,
|
||||
RiskStateSnapshot curr)
|
||||
{
|
||||
if (prev.VexStatus == curr.VexStatus)
|
||||
return null;
|
||||
|
||||
// Determine if this is a meaningful flip
|
||||
var (isMeaningful, direction) = ClassifyVexTransition(prev.VexStatus, curr.VexStatus);
|
||||
|
||||
if (!isMeaningful)
|
||||
return null;
|
||||
|
||||
return new DetectedChange(
|
||||
Rule: DetectionRule.R2_VexFlip,
|
||||
ChangeType: MaterialChangeType.VexFlip,
|
||||
Direction: direction,
|
||||
Reason: $"VEX status changed from {prev.VexStatus} to {curr.VexStatus}",
|
||||
PreviousValue: prev.VexStatus.ToString(),
|
||||
CurrentValue: curr.VexStatus.ToString(),
|
||||
Weight: direction == RiskDirection.Increased
|
||||
? _options.VexFlipToAffectedWeight
|
||||
: _options.VexFlipToNotAffectedWeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classifies VEX status transitions as meaningful or not.
|
||||
/// </summary>
|
||||
private static (bool IsMeaningful, RiskDirection Direction) ClassifyVexTransition(
|
||||
VexStatusType from,
|
||||
VexStatusType to)
|
||||
{
|
||||
return (from, to) switch
|
||||
{
|
||||
// Risk increases
|
||||
(VexStatusType.NotAffected, VexStatusType.Affected) => (true, RiskDirection.Increased),
|
||||
(VexStatusType.Fixed, VexStatusType.Affected) => (true, RiskDirection.Increased),
|
||||
(VexStatusType.UnderInvestigation, VexStatusType.Affected) => (true, RiskDirection.Increased),
|
||||
|
||||
// Risk decreases
|
||||
(VexStatusType.Affected, VexStatusType.NotAffected) => (true, RiskDirection.Decreased),
|
||||
(VexStatusType.Affected, VexStatusType.Fixed) => (true, RiskDirection.Decreased),
|
||||
(VexStatusType.UnderInvestigation, VexStatusType.NotAffected) => (true, RiskDirection.Decreased),
|
||||
(VexStatusType.UnderInvestigation, VexStatusType.Fixed) => (true, RiskDirection.Decreased),
|
||||
|
||||
// Under investigation transitions (noteworthy but not scored)
|
||||
(VexStatusType.Affected, VexStatusType.UnderInvestigation) => (true, RiskDirection.Neutral),
|
||||
(VexStatusType.NotAffected, VexStatusType.UnderInvestigation) => (true, RiskDirection.Neutral),
|
||||
|
||||
// Unknown transitions (from unknown to known)
|
||||
(VexStatusType.Unknown, VexStatusType.Affected) => (true, RiskDirection.Increased),
|
||||
(VexStatusType.Unknown, VexStatusType.NotAffected) => (true, RiskDirection.Decreased),
|
||||
|
||||
// All other transitions are not meaningful
|
||||
_ => (false, RiskDirection.Neutral)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3: Affected Range Boundary - component enters or exits affected version range
|
||||
/// </summary>
|
||||
private DetectedChange? EvaluateRangeBoundary(
|
||||
RiskStateSnapshot prev,
|
||||
RiskStateSnapshot curr)
|
||||
{
|
||||
if (prev.InAffectedRange == curr.InAffectedRange)
|
||||
return null;
|
||||
|
||||
// Skip if either is unknown
|
||||
if (prev.InAffectedRange is null || curr.InAffectedRange is null)
|
||||
return null;
|
||||
|
||||
var direction = curr.InAffectedRange.Value
|
||||
? RiskDirection.Increased
|
||||
: RiskDirection.Decreased;
|
||||
|
||||
return new DetectedChange(
|
||||
Rule: DetectionRule.R3_RangeBoundary,
|
||||
ChangeType: MaterialChangeType.RangeBoundary,
|
||||
Direction: direction,
|
||||
Reason: curr.InAffectedRange.Value
|
||||
? "Component version entered affected range"
|
||||
: "Component version exited affected range",
|
||||
PreviousValue: prev.InAffectedRange.ToString()!,
|
||||
CurrentValue: curr.InAffectedRange.ToString()!,
|
||||
Weight: direction == RiskDirection.Increased
|
||||
? _options.RangeEntryWeight
|
||||
: _options.RangeExitWeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R4: Intelligence/Policy Flip - KEV, EPSS threshold, or policy decision changes
|
||||
/// </summary>
|
||||
private List<DetectedChange> EvaluateIntelligenceFlip(
|
||||
RiskStateSnapshot prev,
|
||||
RiskStateSnapshot curr)
|
||||
{
|
||||
var changes = new List<DetectedChange>();
|
||||
|
||||
// KEV change
|
||||
if (prev.Kev != curr.Kev)
|
||||
{
|
||||
var direction = curr.Kev ? RiskDirection.Increased : RiskDirection.Decreased;
|
||||
changes.Add(new DetectedChange(
|
||||
Rule: DetectionRule.R4_IntelligenceFlip,
|
||||
ChangeType: curr.Kev ? MaterialChangeType.KevAdded : MaterialChangeType.KevRemoved,
|
||||
Direction: direction,
|
||||
Reason: curr.Kev ? "Added to KEV catalog" : "Removed from KEV catalog",
|
||||
PreviousValue: prev.Kev.ToString(),
|
||||
CurrentValue: curr.Kev.ToString(),
|
||||
Weight: curr.Kev ? _options.KevAddedWeight : _options.KevRemovedWeight));
|
||||
}
|
||||
|
||||
// EPSS threshold crossing
|
||||
var epssChange = EvaluateEpssThreshold(prev.EpssScore, curr.EpssScore);
|
||||
if (epssChange is not null)
|
||||
{
|
||||
changes.Add(epssChange);
|
||||
}
|
||||
|
||||
// Policy decision flip
|
||||
if (prev.PolicyDecision != curr.PolicyDecision)
|
||||
{
|
||||
var policyChange = EvaluatePolicyFlip(prev.PolicyDecision, curr.PolicyDecision);
|
||||
if (policyChange is not null)
|
||||
{
|
||||
changes.Add(policyChange);
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private DetectedChange? EvaluateEpssThreshold(double? prevScore, double? currScore)
|
||||
{
|
||||
if (prevScore is null || currScore is null)
|
||||
return null;
|
||||
|
||||
var prevAbove = prevScore.Value >= _options.EpssThreshold;
|
||||
var currAbove = currScore.Value >= _options.EpssThreshold;
|
||||
|
||||
if (prevAbove == currAbove)
|
||||
return null;
|
||||
|
||||
var direction = currAbove ? RiskDirection.Increased : RiskDirection.Decreased;
|
||||
|
||||
return new DetectedChange(
|
||||
Rule: DetectionRule.R4_IntelligenceFlip,
|
||||
ChangeType: MaterialChangeType.EpssThreshold,
|
||||
Direction: direction,
|
||||
Reason: currAbove
|
||||
? $"EPSS score crossed above threshold ({_options.EpssThreshold:P0})"
|
||||
: $"EPSS score dropped below threshold ({_options.EpssThreshold:P0})",
|
||||
PreviousValue: prevScore.Value.ToString("F4"),
|
||||
CurrentValue: currScore.Value.ToString("F4"),
|
||||
Weight: _options.EpssThresholdWeight);
|
||||
}
|
||||
|
||||
private DetectedChange? EvaluatePolicyFlip(PolicyDecisionType? prev, PolicyDecisionType? curr)
|
||||
{
|
||||
if (prev is null || curr is null)
|
||||
return null;
|
||||
|
||||
// Determine direction based on severity ordering: Allow < Warn < Block
|
||||
var direction = (prev.Value, curr.Value) switch
|
||||
{
|
||||
(PolicyDecisionType.Allow, PolicyDecisionType.Warn) => RiskDirection.Increased,
|
||||
(PolicyDecisionType.Allow, PolicyDecisionType.Block) => RiskDirection.Increased,
|
||||
(PolicyDecisionType.Warn, PolicyDecisionType.Block) => RiskDirection.Increased,
|
||||
(PolicyDecisionType.Block, PolicyDecisionType.Warn) => RiskDirection.Decreased,
|
||||
(PolicyDecisionType.Block, PolicyDecisionType.Allow) => RiskDirection.Decreased,
|
||||
(PolicyDecisionType.Warn, PolicyDecisionType.Allow) => RiskDirection.Decreased,
|
||||
_ => RiskDirection.Neutral
|
||||
};
|
||||
|
||||
if (direction == RiskDirection.Neutral)
|
||||
return null;
|
||||
|
||||
return new DetectedChange(
|
||||
Rule: DetectionRule.R4_IntelligenceFlip,
|
||||
ChangeType: MaterialChangeType.PolicyFlip,
|
||||
Direction: direction,
|
||||
Reason: $"Policy decision changed from {prev} to {curr}",
|
||||
PreviousValue: prev.Value.ToString(),
|
||||
CurrentValue: curr.Value.ToString(),
|
||||
Weight: _options.PolicyFlipWeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes priority score for a set of changes.
|
||||
/// Formula: base_severity × Σ(weight_i × direction_i) × confidence_factor
|
||||
/// </summary>
|
||||
private double ComputePriorityScore(List<DetectedChange> changes, RiskStateSnapshot current)
|
||||
{
|
||||
if (changes.Count == 0)
|
||||
return 0;
|
||||
|
||||
// Sum weighted changes
|
||||
var weightedSum = 0.0;
|
||||
foreach (var change in changes)
|
||||
{
|
||||
var directionMultiplier = change.Direction switch
|
||||
{
|
||||
RiskDirection.Increased => 1.0,
|
||||
RiskDirection.Decreased => -0.5,
|
||||
RiskDirection.Neutral => 0.0,
|
||||
_ => 0.0
|
||||
};
|
||||
weightedSum += change.Weight * directionMultiplier;
|
||||
}
|
||||
|
||||
// Base severity from EPSS or default
|
||||
var baseSeverity = current.EpssScore ?? 0.5;
|
||||
|
||||
// KEV boost
|
||||
var kevBoost = current.Kev ? 1.5 : 1.0;
|
||||
|
||||
// Confidence factor from lattice state
|
||||
var confidence = current.LatticeState switch
|
||||
{
|
||||
"certain_reachable" => 1.0,
|
||||
"likely_reachable" => 0.9,
|
||||
"uncertain" => 0.7,
|
||||
"likely_unreachable" => 0.5,
|
||||
"certain_unreachable" => 0.3,
|
||||
_ => 0.7
|
||||
};
|
||||
|
||||
var score = baseSeverity * weightedSum * kevBoost * confidence;
|
||||
|
||||
// Clamp to [-1, 1]
|
||||
return Math.Clamp(score, -1.0, 1.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.SmartDiff.Detection;
|
||||
|
||||
/// <summary>
|
||||
/// Result of material risk change detection.
|
||||
/// </summary>
|
||||
public sealed record MaterialRiskChangeResult(
|
||||
[property: JsonPropertyName("findingKey")] FindingKey FindingKey,
|
||||
[property: JsonPropertyName("hasMaterialChange")] bool HasMaterialChange,
|
||||
[property: JsonPropertyName("changes")] ImmutableArray<DetectedChange> Changes,
|
||||
[property: JsonPropertyName("priorityScore")] double PriorityScore,
|
||||
[property: JsonPropertyName("previousStateHash")] string PreviousStateHash,
|
||||
[property: JsonPropertyName("currentStateHash")] string CurrentStateHash);
|
||||
|
||||
/// <summary>
|
||||
/// A detected material change.
|
||||
/// </summary>
|
||||
public sealed record DetectedChange(
|
||||
[property: JsonPropertyName("rule")] DetectionRule Rule,
|
||||
[property: JsonPropertyName("changeType")] MaterialChangeType ChangeType,
|
||||
[property: JsonPropertyName("direction")] RiskDirection Direction,
|
||||
[property: JsonPropertyName("reason")] string Reason,
|
||||
[property: JsonPropertyName("previousValue")] string PreviousValue,
|
||||
[property: JsonPropertyName("currentValue")] string CurrentValue,
|
||||
[property: JsonPropertyName("weight")] double Weight);
|
||||
|
||||
/// <summary>
|
||||
/// Detection rule identifiers (R1-R4).
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<DetectionRule>))]
|
||||
public enum DetectionRule
|
||||
{
|
||||
[JsonStringEnumMemberName("R1")]
|
||||
R1_ReachabilityFlip,
|
||||
|
||||
[JsonStringEnumMemberName("R2")]
|
||||
R2_VexFlip,
|
||||
|
||||
[JsonStringEnumMemberName("R3")]
|
||||
R3_RangeBoundary,
|
||||
|
||||
[JsonStringEnumMemberName("R4")]
|
||||
R4_IntelligenceFlip
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of material change.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<MaterialChangeType>))]
|
||||
public enum MaterialChangeType
|
||||
{
|
||||
[JsonStringEnumMemberName("reachability_flip")]
|
||||
ReachabilityFlip,
|
||||
|
||||
[JsonStringEnumMemberName("vex_flip")]
|
||||
VexFlip,
|
||||
|
||||
[JsonStringEnumMemberName("range_boundary")]
|
||||
RangeBoundary,
|
||||
|
||||
[JsonStringEnumMemberName("kev_added")]
|
||||
KevAdded,
|
||||
|
||||
[JsonStringEnumMemberName("kev_removed")]
|
||||
KevRemoved,
|
||||
|
||||
[JsonStringEnumMemberName("epss_threshold")]
|
||||
EpssThreshold,
|
||||
|
||||
[JsonStringEnumMemberName("policy_flip")]
|
||||
PolicyFlip
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direction of risk change.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<RiskDirection>))]
|
||||
public enum RiskDirection
|
||||
{
|
||||
[JsonStringEnumMemberName("increased")]
|
||||
Increased,
|
||||
|
||||
[JsonStringEnumMemberName("decreased")]
|
||||
Decreased,
|
||||
|
||||
[JsonStringEnumMemberName("neutral")]
|
||||
Neutral
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for material risk change detection.
|
||||
/// </summary>
|
||||
public sealed class MaterialRiskChangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options instance.
|
||||
/// </summary>
|
||||
public static readonly MaterialRiskChangeOptions Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// Weight for reachability flip (unreachable → reachable).
|
||||
/// </summary>
|
||||
public double ReachabilityFlipUpWeight { get; init; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Weight for reachability flip (reachable → unreachable).
|
||||
/// </summary>
|
||||
public double ReachabilityFlipDownWeight { get; init; } = 0.8;
|
||||
|
||||
/// <summary>
|
||||
/// Weight for VEX flip to affected.
|
||||
/// </summary>
|
||||
public double VexFlipToAffectedWeight { get; init; } = 0.9;
|
||||
|
||||
/// <summary>
|
||||
/// Weight for VEX flip to not_affected.
|
||||
/// </summary>
|
||||
public double VexFlipToNotAffectedWeight { get; init; } = 0.7;
|
||||
|
||||
/// <summary>
|
||||
/// Weight for entering affected range.
|
||||
/// </summary>
|
||||
public double RangeEntryWeight { get; init; } = 0.8;
|
||||
|
||||
/// <summary>
|
||||
/// Weight for exiting affected range.
|
||||
/// </summary>
|
||||
public double RangeExitWeight { get; init; } = 0.6;
|
||||
|
||||
/// <summary>
|
||||
/// Weight for KEV addition.
|
||||
/// </summary>
|
||||
public double KevAddedWeight { get; init; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Weight for KEV removal.
|
||||
/// </summary>
|
||||
public double KevRemovedWeight { get; init; } = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Weight for EPSS threshold crossing.
|
||||
/// </summary>
|
||||
public double EpssThresholdWeight { get; init; } = 0.6;
|
||||
|
||||
/// <summary>
|
||||
/// EPSS score threshold for R4 detection.
|
||||
/// </summary>
|
||||
public double EpssThreshold { get; init; } = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Weight for policy decision flip.
|
||||
/// </summary>
|
||||
public double PolicyFlipWeight { get; init; } = 0.7;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.SmartDiff.Detection;
|
||||
|
||||
/// <summary>
|
||||
/// Captures the complete risk state for a finding at a point in time.
|
||||
/// Used for cross-scan comparison.
|
||||
/// Per Sprint 3500.3 - Smart-Diff Detection Rules.
|
||||
/// </summary>
|
||||
public sealed record RiskStateSnapshot(
|
||||
[property: JsonPropertyName("findingKey")] FindingKey FindingKey,
|
||||
[property: JsonPropertyName("scanId")] string ScanId,
|
||||
[property: JsonPropertyName("capturedAt")] DateTimeOffset CapturedAt,
|
||||
[property: JsonPropertyName("reachable")] bool? Reachable,
|
||||
[property: JsonPropertyName("latticeState")] string? LatticeState,
|
||||
[property: JsonPropertyName("vexStatus")] VexStatusType VexStatus,
|
||||
[property: JsonPropertyName("inAffectedRange")] bool? InAffectedRange,
|
||||
[property: JsonPropertyName("kev")] bool Kev,
|
||||
[property: JsonPropertyName("epssScore")] double? EpssScore,
|
||||
[property: JsonPropertyName("policyFlags")] ImmutableArray<string> PolicyFlags,
|
||||
[property: JsonPropertyName("policyDecision")] PolicyDecisionType? PolicyDecision,
|
||||
[property: JsonPropertyName("evidenceLinks")] ImmutableArray<EvidenceLink>? EvidenceLinks = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes a deterministic hash for this snapshot (excluding timestamp).
|
||||
/// </summary>
|
||||
public string ComputeStateHash()
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(FindingKey.ToString());
|
||||
builder.Append(':');
|
||||
builder.Append(Reachable?.ToString() ?? "null");
|
||||
builder.Append(':');
|
||||
builder.Append(VexStatus.ToString());
|
||||
builder.Append(':');
|
||||
builder.Append(InAffectedRange?.ToString() ?? "null");
|
||||
builder.Append(':');
|
||||
builder.Append(Kev.ToString());
|
||||
builder.Append(':');
|
||||
builder.Append(EpssScore?.ToString("F4", CultureInfo.InvariantCulture) ?? "null");
|
||||
builder.Append(':');
|
||||
builder.Append(PolicyDecision?.ToString() ?? "null");
|
||||
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString()));
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Key identifying a unique finding.
|
||||
/// </summary>
|
||||
public sealed record FindingKey(
|
||||
[property: JsonPropertyName("vulnId")] string VulnId,
|
||||
[property: JsonPropertyName("componentPurl")] string ComponentPurl)
|
||||
{
|
||||
public override string ToString() => $"{VulnId}@{ComponentPurl}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Link to evidence supporting a state.
|
||||
/// </summary>
|
||||
public sealed record EvidenceLink(
|
||||
[property: JsonPropertyName("type")] string Type,
|
||||
[property: JsonPropertyName("uri")] string Uri,
|
||||
[property: JsonPropertyName("digest")] string? Digest = null);
|
||||
|
||||
/// <summary>
|
||||
/// VEX status values.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<VexStatusType>))]
|
||||
public enum VexStatusType
|
||||
{
|
||||
[JsonStringEnumMemberName("unknown")]
|
||||
Unknown,
|
||||
|
||||
[JsonStringEnumMemberName("affected")]
|
||||
Affected,
|
||||
|
||||
[JsonStringEnumMemberName("not_affected")]
|
||||
NotAffected,
|
||||
|
||||
[JsonStringEnumMemberName("fixed")]
|
||||
Fixed,
|
||||
|
||||
[JsonStringEnumMemberName("under_investigation")]
|
||||
UnderInvestigation
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Policy decision type.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<PolicyDecisionType>))]
|
||||
public enum PolicyDecisionType
|
||||
{
|
||||
[JsonStringEnumMemberName("allow")]
|
||||
Allow,
|
||||
|
||||
[JsonStringEnumMemberName("warn")]
|
||||
Warn,
|
||||
|
||||
[JsonStringEnumMemberName("block")]
|
||||
Block
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.SmartDiff.Output;
|
||||
|
||||
/// <summary>
|
||||
/// SARIF 2.1.0 log model for Smart-Diff output.
|
||||
/// Per Sprint 3500.4 - Smart-Diff Binary Analysis.
|
||||
/// </summary>
|
||||
public sealed record SarifLog(
|
||||
[property: JsonPropertyName("version")] string Version,
|
||||
[property: JsonPropertyName("$schema")] string Schema,
|
||||
[property: JsonPropertyName("runs")] ImmutableArray<SarifRun> Runs);
|
||||
|
||||
/// <summary>
|
||||
/// A single SARIF run representing one analysis execution.
|
||||
/// </summary>
|
||||
public sealed record SarifRun(
|
||||
[property: JsonPropertyName("tool")] SarifTool Tool,
|
||||
[property: JsonPropertyName("results")] ImmutableArray<SarifResult> Results,
|
||||
[property: JsonPropertyName("invocations")] ImmutableArray<SarifInvocation>? Invocations = null,
|
||||
[property: JsonPropertyName("artifacts")] ImmutableArray<SarifArtifact>? Artifacts = null,
|
||||
[property: JsonPropertyName("versionControlProvenance")] ImmutableArray<SarifVersionControlDetails>? VersionControlProvenance = null);
|
||||
|
||||
/// <summary>
|
||||
/// Tool information for the SARIF run.
|
||||
/// </summary>
|
||||
public sealed record SarifTool(
|
||||
[property: JsonPropertyName("driver")] SarifToolComponent Driver,
|
||||
[property: JsonPropertyName("extensions")] ImmutableArray<SarifToolComponent>? Extensions = null);
|
||||
|
||||
/// <summary>
|
||||
/// Tool component (driver or extension).
|
||||
/// </summary>
|
||||
public sealed record SarifToolComponent(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("version")] string Version,
|
||||
[property: JsonPropertyName("informationUri")] string? InformationUri = null,
|
||||
[property: JsonPropertyName("rules")] ImmutableArray<SarifReportingDescriptor>? Rules = null,
|
||||
[property: JsonPropertyName("supportedTaxonomies")] ImmutableArray<SarifToolComponentReference>? SupportedTaxonomies = null);
|
||||
|
||||
/// <summary>
|
||||
/// Reference to a tool component.
|
||||
/// </summary>
|
||||
public sealed record SarifToolComponentReference(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("guid")] string? Guid = null);
|
||||
|
||||
/// <summary>
|
||||
/// Rule definition.
|
||||
/// </summary>
|
||||
public sealed record SarifReportingDescriptor(
|
||||
[property: JsonPropertyName("id")] string Id,
|
||||
[property: JsonPropertyName("name")] string? Name = null,
|
||||
[property: JsonPropertyName("shortDescription")] SarifMessage? ShortDescription = null,
|
||||
[property: JsonPropertyName("fullDescription")] SarifMessage? FullDescription = null,
|
||||
[property: JsonPropertyName("defaultConfiguration")] SarifReportingConfiguration? DefaultConfiguration = null,
|
||||
[property: JsonPropertyName("helpUri")] string? HelpUri = null);
|
||||
|
||||
/// <summary>
|
||||
/// Rule configuration.
|
||||
/// </summary>
|
||||
public sealed record SarifReportingConfiguration(
|
||||
[property: JsonPropertyName("level")] SarifLevel Level = SarifLevel.Warning,
|
||||
[property: JsonPropertyName("enabled")] bool Enabled = true);
|
||||
|
||||
/// <summary>
|
||||
/// SARIF message with text.
|
||||
/// </summary>
|
||||
public sealed record SarifMessage(
|
||||
[property: JsonPropertyName("text")] string Text,
|
||||
[property: JsonPropertyName("markdown")] string? Markdown = null);
|
||||
|
||||
/// <summary>
|
||||
/// SARIF result level.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<SarifLevel>))]
|
||||
public enum SarifLevel
|
||||
{
|
||||
[JsonStringEnumMemberName("none")]
|
||||
None,
|
||||
|
||||
[JsonStringEnumMemberName("note")]
|
||||
Note,
|
||||
|
||||
[JsonStringEnumMemberName("warning")]
|
||||
Warning,
|
||||
|
||||
[JsonStringEnumMemberName("error")]
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single result/finding.
|
||||
/// </summary>
|
||||
public sealed record SarifResult(
|
||||
[property: JsonPropertyName("ruleId")] string RuleId,
|
||||
[property: JsonPropertyName("level")] SarifLevel Level,
|
||||
[property: JsonPropertyName("message")] SarifMessage Message,
|
||||
[property: JsonPropertyName("locations")] ImmutableArray<SarifLocation>? Locations = null,
|
||||
[property: JsonPropertyName("fingerprints")] ImmutableDictionary<string, string>? Fingerprints = null,
|
||||
[property: JsonPropertyName("partialFingerprints")] ImmutableDictionary<string, string>? PartialFingerprints = null,
|
||||
[property: JsonPropertyName("properties")] ImmutableDictionary<string, object>? Properties = null);
|
||||
|
||||
/// <summary>
|
||||
/// Location of a result.
|
||||
/// </summary>
|
||||
public sealed record SarifLocation(
|
||||
[property: JsonPropertyName("physicalLocation")] SarifPhysicalLocation? PhysicalLocation = null,
|
||||
[property: JsonPropertyName("logicalLocations")] ImmutableArray<SarifLogicalLocation>? LogicalLocations = null);
|
||||
|
||||
/// <summary>
|
||||
/// Physical file location.
|
||||
/// </summary>
|
||||
public sealed record SarifPhysicalLocation(
|
||||
[property: JsonPropertyName("artifactLocation")] SarifArtifactLocation ArtifactLocation,
|
||||
[property: JsonPropertyName("region")] SarifRegion? Region = null);
|
||||
|
||||
/// <summary>
|
||||
/// Artifact location (file path).
|
||||
/// </summary>
|
||||
public sealed record SarifArtifactLocation(
|
||||
[property: JsonPropertyName("uri")] string Uri,
|
||||
[property: JsonPropertyName("uriBaseId")] string? UriBaseId = null,
|
||||
[property: JsonPropertyName("index")] int? Index = null);
|
||||
|
||||
/// <summary>
|
||||
/// Region within a file.
|
||||
/// </summary>
|
||||
public sealed record SarifRegion(
|
||||
[property: JsonPropertyName("startLine")] int? StartLine = null,
|
||||
[property: JsonPropertyName("startColumn")] int? StartColumn = null,
|
||||
[property: JsonPropertyName("endLine")] int? EndLine = null,
|
||||
[property: JsonPropertyName("endColumn")] int? EndColumn = null);
|
||||
|
||||
/// <summary>
|
||||
/// Logical location (namespace, class, function).
|
||||
/// </summary>
|
||||
public sealed record SarifLogicalLocation(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("fullyQualifiedName")] string? FullyQualifiedName = null,
|
||||
[property: JsonPropertyName("kind")] string? Kind = null);
|
||||
|
||||
/// <summary>
|
||||
/// Invocation information.
|
||||
/// </summary>
|
||||
public sealed record SarifInvocation(
|
||||
[property: JsonPropertyName("executionSuccessful")] bool ExecutionSuccessful,
|
||||
[property: JsonPropertyName("startTimeUtc")] DateTimeOffset? StartTimeUtc = null,
|
||||
[property: JsonPropertyName("endTimeUtc")] DateTimeOffset? EndTimeUtc = null,
|
||||
[property: JsonPropertyName("workingDirectory")] SarifArtifactLocation? WorkingDirectory = null,
|
||||
[property: JsonPropertyName("commandLine")] string? CommandLine = null);
|
||||
|
||||
/// <summary>
|
||||
/// Artifact (file) information.
|
||||
/// </summary>
|
||||
public sealed record SarifArtifact(
|
||||
[property: JsonPropertyName("location")] SarifArtifactLocation Location,
|
||||
[property: JsonPropertyName("mimeType")] string? MimeType = null,
|
||||
[property: JsonPropertyName("hashes")] ImmutableDictionary<string, string>? Hashes = null);
|
||||
|
||||
/// <summary>
|
||||
/// Version control information.
|
||||
/// </summary>
|
||||
public sealed record SarifVersionControlDetails(
|
||||
[property: JsonPropertyName("repositoryUri")] string RepositoryUri,
|
||||
[property: JsonPropertyName("revisionId")] string? RevisionId = null,
|
||||
[property: JsonPropertyName("branch")] string? Branch = null);
|
||||
@@ -0,0 +1,393 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Scanner.SmartDiff.Output;
|
||||
|
||||
/// <summary>
|
||||
/// Options for SARIF output generation.
|
||||
/// </summary>
|
||||
public sealed class SarifOutputOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options instance.
|
||||
/// </summary>
|
||||
public static readonly SarifOutputOptions Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include VEX candidates in output.
|
||||
/// </summary>
|
||||
public bool IncludeVexCandidates { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include hardening regressions in output.
|
||||
/// </summary>
|
||||
public bool IncludeHardeningRegressions { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include reachability changes in output.
|
||||
/// </summary>
|
||||
public bool IncludeReachabilityChanges { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to pretty-print JSON output.
|
||||
/// </summary>
|
||||
public bool IndentedJson { get; init; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input for SARIF generation.
|
||||
/// </summary>
|
||||
public sealed record SmartDiffSarifInput(
|
||||
string ScannerVersion,
|
||||
DateTimeOffset ScanTime,
|
||||
string? BaseDigest,
|
||||
string? TargetDigest,
|
||||
IReadOnlyList<MaterialRiskChange> MaterialChanges,
|
||||
IReadOnlyList<HardeningRegression> HardeningRegressions,
|
||||
IReadOnlyList<VexCandidate> VexCandidates,
|
||||
IReadOnlyList<ReachabilityChange> ReachabilityChanges,
|
||||
VcsInfo? VcsInfo = null);
|
||||
|
||||
/// <summary>
|
||||
/// VCS information for SARIF provenance.
|
||||
/// </summary>
|
||||
public sealed record VcsInfo(
|
||||
string RepositoryUri,
|
||||
string? RevisionId,
|
||||
string? Branch);
|
||||
|
||||
/// <summary>
|
||||
/// A material risk change finding.
|
||||
/// </summary>
|
||||
public sealed record MaterialRiskChange(
|
||||
string VulnId,
|
||||
string ComponentPurl,
|
||||
RiskDirection Direction,
|
||||
string Reason,
|
||||
string? FilePath = null);
|
||||
|
||||
/// <summary>
|
||||
/// Direction of risk change.
|
||||
/// </summary>
|
||||
public enum RiskDirection
|
||||
{
|
||||
/// <summary>Risk increased (worse).</summary>
|
||||
Increased,
|
||||
|
||||
/// <summary>Risk decreased (better).</summary>
|
||||
Decreased,
|
||||
|
||||
/// <summary>Risk status changed but severity unclear.</summary>
|
||||
Changed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A hardening regression finding.
|
||||
/// </summary>
|
||||
public sealed record HardeningRegression(
|
||||
string BinaryPath,
|
||||
string FlagName,
|
||||
bool WasEnabled,
|
||||
bool IsEnabled,
|
||||
double ScoreImpact);
|
||||
|
||||
/// <summary>
|
||||
/// A VEX candidate finding.
|
||||
/// </summary>
|
||||
public sealed record VexCandidate(
|
||||
string VulnId,
|
||||
string ComponentPurl,
|
||||
string Justification,
|
||||
string? ImpactStatement);
|
||||
|
||||
/// <summary>
|
||||
/// A reachability status change.
|
||||
/// </summary>
|
||||
public sealed record ReachabilityChange(
|
||||
string VulnId,
|
||||
string ComponentPurl,
|
||||
bool WasReachable,
|
||||
bool IsReachable,
|
||||
string? Evidence);
|
||||
|
||||
/// <summary>
|
||||
/// Generates SARIF 2.1.0 output for Smart-Diff findings.
|
||||
/// Per Sprint 3500.4 - Smart-Diff Binary Analysis.
|
||||
/// </summary>
|
||||
public sealed class SarifOutputGenerator
|
||||
{
|
||||
private const string SarifVersion = "2.1.0";
|
||||
private const string SchemaUri = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json";
|
||||
private const string ToolName = "StellaOps.Scanner.SmartDiff";
|
||||
private const string ToolInfoUri = "https://stellaops.dev/docs/scanner/smart-diff";
|
||||
|
||||
private static readonly JsonSerializerOptions SarifJsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Generate a SARIF log from Smart-Diff input.
|
||||
/// </summary>
|
||||
public SarifLog Generate(SmartDiffSarifInput input, SarifOutputOptions? options = null)
|
||||
{
|
||||
options ??= SarifOutputOptions.Default;
|
||||
|
||||
var tool = CreateTool(input);
|
||||
var results = CreateResults(input, options);
|
||||
var invocation = CreateInvocation(input);
|
||||
var artifacts = CreateArtifacts(input);
|
||||
var vcsProvenance = CreateVcsProvenance(input);
|
||||
|
||||
var run = new SarifRun(
|
||||
Tool: tool,
|
||||
Results: results,
|
||||
Invocations: [invocation],
|
||||
Artifacts: artifacts.Length > 0 ? artifacts : null,
|
||||
VersionControlProvenance: vcsProvenance);
|
||||
|
||||
return new SarifLog(
|
||||
Version: SarifVersion,
|
||||
Schema: SchemaUri,
|
||||
Runs: [run]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate SARIF JSON string.
|
||||
/// </summary>
|
||||
public string GenerateJson(SmartDiffSarifInput input, SarifOutputOptions? options = null)
|
||||
{
|
||||
var log = Generate(input, options);
|
||||
var jsonOptions = options?.IndentedJson == true
|
||||
? new JsonSerializerOptions(SarifJsonOptions) { WriteIndented = true }
|
||||
: SarifJsonOptions;
|
||||
return JsonSerializer.Serialize(log, jsonOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write SARIF to a stream.
|
||||
/// </summary>
|
||||
public async Task WriteAsync(
|
||||
SmartDiffSarifInput input,
|
||||
Stream outputStream,
|
||||
SarifOutputOptions? options = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var log = Generate(input, options);
|
||||
var jsonOptions = options?.IndentedJson == true
|
||||
? new JsonSerializerOptions(SarifJsonOptions) { WriteIndented = true }
|
||||
: SarifJsonOptions;
|
||||
await JsonSerializer.SerializeAsync(outputStream, log, jsonOptions, ct);
|
||||
}
|
||||
|
||||
private static SarifTool CreateTool(SmartDiffSarifInput input)
|
||||
{
|
||||
var rules = CreateRules();
|
||||
|
||||
return new SarifTool(
|
||||
Driver: new SarifToolComponent(
|
||||
Name: ToolName,
|
||||
Version: input.ScannerVersion,
|
||||
InformationUri: ToolInfoUri,
|
||||
Rules: rules,
|
||||
SupportedTaxonomies: [
|
||||
new SarifToolComponentReference(
|
||||
Name: "CWE",
|
||||
Guid: "25F72D7E-8A92-459D-AD67-64853F788765")
|
||||
]));
|
||||
}
|
||||
|
||||
private static ImmutableArray<SarifReportingDescriptor> CreateRules()
|
||||
{
|
||||
return
|
||||
[
|
||||
new SarifReportingDescriptor(
|
||||
Id: "SDIFF001",
|
||||
Name: "MaterialRiskChange",
|
||||
ShortDescription: new SarifMessage("Material risk change detected"),
|
||||
FullDescription: new SarifMessage("A vulnerability finding has undergone a material risk state change between scans."),
|
||||
DefaultConfiguration: new SarifReportingConfiguration(Level: SarifLevel.Warning),
|
||||
HelpUri: $"{ToolInfoUri}/rules/SDIFF001"),
|
||||
|
||||
new SarifReportingDescriptor(
|
||||
Id: "SDIFF002",
|
||||
Name: "HardeningRegression",
|
||||
ShortDescription: new SarifMessage("Binary hardening regression detected"),
|
||||
FullDescription: new SarifMessage("A binary has lost security hardening flags compared to the previous scan."),
|
||||
DefaultConfiguration: new SarifReportingConfiguration(Level: SarifLevel.Error),
|
||||
HelpUri: $"{ToolInfoUri}/rules/SDIFF002"),
|
||||
|
||||
new SarifReportingDescriptor(
|
||||
Id: "SDIFF003",
|
||||
Name: "VexCandidateGenerated",
|
||||
ShortDescription: new SarifMessage("VEX candidate auto-generated"),
|
||||
FullDescription: new SarifMessage("A VEX 'not_affected' candidate was generated because vulnerable APIs are no longer present."),
|
||||
DefaultConfiguration: new SarifReportingConfiguration(Level: SarifLevel.Note),
|
||||
HelpUri: $"{ToolInfoUri}/rules/SDIFF003"),
|
||||
|
||||
new SarifReportingDescriptor(
|
||||
Id: "SDIFF004",
|
||||
Name: "ReachabilityFlip",
|
||||
ShortDescription: new SarifMessage("Reachability status changed"),
|
||||
FullDescription: new SarifMessage("The reachability of a vulnerability has flipped between scans."),
|
||||
DefaultConfiguration: new SarifReportingConfiguration(Level: SarifLevel.Warning),
|
||||
HelpUri: $"{ToolInfoUri}/rules/SDIFF004")
|
||||
];
|
||||
}
|
||||
|
||||
private static ImmutableArray<SarifResult> CreateResults(SmartDiffSarifInput input, SarifOutputOptions options)
|
||||
{
|
||||
var results = new List<SarifResult>();
|
||||
|
||||
// Material risk changes
|
||||
foreach (var change in input.MaterialChanges)
|
||||
{
|
||||
results.Add(CreateMaterialChangeResult(change));
|
||||
}
|
||||
|
||||
// Hardening regressions
|
||||
if (options.IncludeHardeningRegressions)
|
||||
{
|
||||
foreach (var regression in input.HardeningRegressions)
|
||||
{
|
||||
results.Add(CreateHardeningRegressionResult(regression));
|
||||
}
|
||||
}
|
||||
|
||||
// VEX candidates
|
||||
if (options.IncludeVexCandidates)
|
||||
{
|
||||
foreach (var candidate in input.VexCandidates)
|
||||
{
|
||||
results.Add(CreateVexCandidateResult(candidate));
|
||||
}
|
||||
}
|
||||
|
||||
// Reachability changes
|
||||
if (options.IncludeReachabilityChanges)
|
||||
{
|
||||
foreach (var change in input.ReachabilityChanges)
|
||||
{
|
||||
results.Add(CreateReachabilityChangeResult(change));
|
||||
}
|
||||
}
|
||||
|
||||
return [.. results];
|
||||
}
|
||||
|
||||
private static SarifResult CreateMaterialChangeResult(MaterialRiskChange change)
|
||||
{
|
||||
var level = change.Direction == RiskDirection.Increased ? SarifLevel.Warning : SarifLevel.Note;
|
||||
var message = $"Material risk change for {change.VulnId} in {change.ComponentPurl}: {change.Reason}";
|
||||
|
||||
var locations = change.FilePath is not null
|
||||
? ImmutableArray.Create(new SarifLocation(
|
||||
PhysicalLocation: new SarifPhysicalLocation(
|
||||
ArtifactLocation: new SarifArtifactLocation(Uri: change.FilePath))))
|
||||
: (ImmutableArray<SarifLocation>?)null;
|
||||
|
||||
return new SarifResult(
|
||||
RuleId: "SDIFF001",
|
||||
Level: level,
|
||||
Message: new SarifMessage(message),
|
||||
Locations: locations,
|
||||
Fingerprints: ImmutableDictionary.CreateRange(new[]
|
||||
{
|
||||
KeyValuePair.Create("vulnId", change.VulnId),
|
||||
KeyValuePair.Create("purl", change.ComponentPurl)
|
||||
}));
|
||||
}
|
||||
|
||||
private static SarifResult CreateHardeningRegressionResult(HardeningRegression regression)
|
||||
{
|
||||
var message = $"Hardening flag '{regression.FlagName}' was {(regression.WasEnabled ? "enabled" : "disabled")} " +
|
||||
$"but is now {(regression.IsEnabled ? "enabled" : "disabled")} in {regression.BinaryPath}";
|
||||
|
||||
return new SarifResult(
|
||||
RuleId: "SDIFF002",
|
||||
Level: SarifLevel.Error,
|
||||
Message: new SarifMessage(message),
|
||||
Locations: [new SarifLocation(
|
||||
PhysicalLocation: new SarifPhysicalLocation(
|
||||
ArtifactLocation: new SarifArtifactLocation(Uri: regression.BinaryPath)))]);
|
||||
}
|
||||
|
||||
private static SarifResult CreateVexCandidateResult(VexCandidate candidate)
|
||||
{
|
||||
var message = $"VEX not_affected candidate for {candidate.VulnId} in {candidate.ComponentPurl}: {candidate.Justification}";
|
||||
|
||||
return new SarifResult(
|
||||
RuleId: "SDIFF003",
|
||||
Level: SarifLevel.Note,
|
||||
Message: new SarifMessage(message),
|
||||
Fingerprints: ImmutableDictionary.CreateRange(new[]
|
||||
{
|
||||
KeyValuePair.Create("vulnId", candidate.VulnId),
|
||||
KeyValuePair.Create("purl", candidate.ComponentPurl)
|
||||
}));
|
||||
}
|
||||
|
||||
private static SarifResult CreateReachabilityChangeResult(ReachabilityChange change)
|
||||
{
|
||||
var direction = change.IsReachable ? "became reachable" : "became unreachable";
|
||||
var message = $"Vulnerability {change.VulnId} in {change.ComponentPurl} {direction}";
|
||||
|
||||
return new SarifResult(
|
||||
RuleId: "SDIFF004",
|
||||
Level: SarifLevel.Warning,
|
||||
Message: new SarifMessage(message),
|
||||
Fingerprints: ImmutableDictionary.CreateRange(new[]
|
||||
{
|
||||
KeyValuePair.Create("vulnId", change.VulnId),
|
||||
KeyValuePair.Create("purl", change.ComponentPurl)
|
||||
}));
|
||||
}
|
||||
|
||||
private static SarifInvocation CreateInvocation(SmartDiffSarifInput input)
|
||||
{
|
||||
return new SarifInvocation(
|
||||
ExecutionSuccessful: true,
|
||||
StartTimeUtc: input.ScanTime,
|
||||
EndTimeUtc: DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
private static ImmutableArray<SarifArtifact> CreateArtifacts(SmartDiffSarifInput input)
|
||||
{
|
||||
var artifacts = new List<SarifArtifact>();
|
||||
|
||||
// Collect unique file paths from results
|
||||
var paths = new HashSet<string>();
|
||||
|
||||
foreach (var change in input.MaterialChanges)
|
||||
{
|
||||
if (change.FilePath is not null)
|
||||
paths.Add(change.FilePath);
|
||||
}
|
||||
|
||||
foreach (var regression in input.HardeningRegressions)
|
||||
{
|
||||
paths.Add(regression.BinaryPath);
|
||||
}
|
||||
|
||||
foreach (var path in paths)
|
||||
{
|
||||
artifacts.Add(new SarifArtifact(
|
||||
Location: new SarifArtifactLocation(Uri: path)));
|
||||
}
|
||||
|
||||
return [.. artifacts];
|
||||
}
|
||||
|
||||
private static ImmutableArray<SarifVersionControlDetails>? CreateVcsProvenance(SmartDiffSarifInput input)
|
||||
{
|
||||
if (input.VcsInfo is null)
|
||||
return null;
|
||||
|
||||
return [new SarifVersionControlDetails(
|
||||
RepositoryUri: input.VcsInfo.RepositoryUri,
|
||||
RevisionId: input.VcsInfo.RevisionId,
|
||||
Branch: input.VcsInfo.Branch)];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user