new advisories work and features gaps work
This commit is contained in:
@@ -41,6 +41,11 @@ public sealed record UnifiedEvidenceResponseDto
|
||||
/// <summary>Policy evaluation evidence.</summary>
|
||||
public PolicyEvidenceDto? Policy { get; init; }
|
||||
|
||||
// Sprint: SPRINT_20260112_009_SCANNER_binary_diff_bundle_export (BINDIFF-SCAN-001)
|
||||
|
||||
/// <summary>Binary diff evidence with semantic and structural changes.</summary>
|
||||
public BinaryDiffEvidenceDto? BinaryDiff { get; init; }
|
||||
|
||||
// === Manifest Hashes ===
|
||||
|
||||
/// <summary>Content-addressed hashes for determinism verification.</summary>
|
||||
@@ -388,3 +393,131 @@ public sealed record VerificationStatusDto
|
||||
/// <summary>Last verification timestamp.</summary>
|
||||
public DateTimeOffset? VerifiedAt { get; init; }
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_009_SCANNER_binary_diff_bundle_export (BINDIFF-SCAN-001)
|
||||
|
||||
/// <summary>
|
||||
/// Binary diff evidence for unified evidence response.
|
||||
/// </summary>
|
||||
public sealed record BinaryDiffEvidenceDto
|
||||
{
|
||||
/// <summary>Evidence status.</summary>
|
||||
public required string Status { get; init; }
|
||||
|
||||
/// <summary>SHA-256 hash of the evidence content.</summary>
|
||||
public string? Hash { get; init; }
|
||||
|
||||
/// <summary>Previous binary artifact digest.</summary>
|
||||
public string? PreviousBinaryDigest { get; init; }
|
||||
|
||||
/// <summary>Current binary artifact digest.</summary>
|
||||
public string? CurrentBinaryDigest { get; init; }
|
||||
|
||||
/// <summary>Type of diff (structural, semantic, hybrid).</summary>
|
||||
public string? DiffType { get; init; }
|
||||
|
||||
/// <summary>Binary format/ISA (e.g., elf-x86_64).</summary>
|
||||
public string? BinaryFormat { get; init; }
|
||||
|
||||
/// <summary>Tool and version used for diffing.</summary>
|
||||
public string? ToolVersion { get; init; }
|
||||
|
||||
/// <summary>Overall similarity score (0.0-1.0).</summary>
|
||||
public double? SimilarityScore { get; init; }
|
||||
|
||||
/// <summary>Number of function-level changes.</summary>
|
||||
public int FunctionChangeCount { get; init; }
|
||||
|
||||
/// <summary>Number of symbol-level changes.</summary>
|
||||
public int SymbolChangeCount { get; init; }
|
||||
|
||||
/// <summary>Number of section-level changes.</summary>
|
||||
public int SectionChangeCount { get; init; }
|
||||
|
||||
/// <summary>Number of security-relevant changes.</summary>
|
||||
public int SecurityChangeCount { get; init; }
|
||||
|
||||
/// <summary>Whether semantic diff is available.</summary>
|
||||
public bool HasSemanticDiff { get; init; }
|
||||
|
||||
/// <summary>Semantic similarity score (0.0-1.0).</summary>
|
||||
public double? SemanticSimilarity { get; init; }
|
||||
|
||||
/// <summary>Function-level changes.</summary>
|
||||
public IReadOnlyList<BinaryFunctionDiffDto>? FunctionChanges { get; init; }
|
||||
|
||||
/// <summary>Security-relevant changes.</summary>
|
||||
public IReadOnlyList<BinarySecurityChangeDto>? SecurityChanges { get; init; }
|
||||
|
||||
/// <summary>DSSE attestation reference for binary diff.</summary>
|
||||
public AttestationRefDto? Attestation { get; init; }
|
||||
|
||||
/// <summary>CAS URI for full binary diff evidence.</summary>
|
||||
public string? CasUri { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Function-level diff entry for binary diff.
|
||||
/// </summary>
|
||||
public sealed record BinaryFunctionDiffDto
|
||||
{
|
||||
/// <summary>Diff operation (added, removed, modified).</summary>
|
||||
public required string Operation { get; init; }
|
||||
|
||||
/// <summary>Function name.</summary>
|
||||
public required string FunctionName { get; init; }
|
||||
|
||||
/// <summary>Function signature (if available).</summary>
|
||||
public string? Signature { get; init; }
|
||||
|
||||
/// <summary>Semantic similarity score for modified functions.</summary>
|
||||
public double? Similarity { get; init; }
|
||||
|
||||
/// <summary>Node hash for reachability correlation.</summary>
|
||||
public string? NodeHash { get; init; }
|
||||
|
||||
/// <summary>Whether this function is security-sensitive.</summary>
|
||||
public bool SecuritySensitive { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Security-relevant change in binary.
|
||||
/// </summary>
|
||||
public sealed record BinarySecurityChangeDto
|
||||
{
|
||||
/// <summary>Type of security change.</summary>
|
||||
public required string ChangeType { get; init; }
|
||||
|
||||
/// <summary>Severity level (info, warning, critical).</summary>
|
||||
public required string Severity { get; init; }
|
||||
|
||||
/// <summary>Description of the change.</summary>
|
||||
public required string Description { get; init; }
|
||||
|
||||
/// <summary>Affected function name.</summary>
|
||||
public string? AffectedFunction { get; init; }
|
||||
|
||||
/// <summary>Suggested remediation.</summary>
|
||||
public string? Remediation { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attestation reference for evidence.
|
||||
/// </summary>
|
||||
public sealed record AttestationRefDto
|
||||
{
|
||||
/// <summary>Attestation ID.</summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>Predicate type URI.</summary>
|
||||
public required string PredicateType { get; init; }
|
||||
|
||||
/// <summary>DSSE envelope digest.</summary>
|
||||
public string? EnvelopeDigest { get; init; }
|
||||
|
||||
/// <summary>Rekor log index (if anchored).</summary>
|
||||
public long? RekorLogIndex { get; init; }
|
||||
|
||||
/// <summary>CAS URI for full attestation.</summary>
|
||||
public string? CasUri { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
// <copyright file="EpssChangeEvent.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_005_SCANNER_epss_reanalysis_events (SCAN-EPSS-001, SCAN-EPSS-003)
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Core.Epss;
|
||||
|
||||
/// <summary>
|
||||
/// Event emitted when EPSS scores change significantly, triggering policy reanalysis.
|
||||
/// </summary>
|
||||
public sealed record EpssChangeEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique event identifier (deterministic based on CVE and model date).
|
||||
/// </summary>
|
||||
[JsonPropertyName("eventId")]
|
||||
public required string EventId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Event type constant.
|
||||
/// </summary>
|
||||
[JsonPropertyName("eventType")]
|
||||
public string EventType { get; init; } = EpssEventTypes.Updated;
|
||||
|
||||
/// <summary>
|
||||
/// Tenant identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tenant")]
|
||||
public required string Tenant { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVE identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cveId")]
|
||||
public required string CveId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Previous EPSS score (null for new entries).
|
||||
/// </summary>
|
||||
[JsonPropertyName("previousScore")]
|
||||
public double? PreviousScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// New EPSS score.
|
||||
/// </summary>
|
||||
[JsonPropertyName("newScore")]
|
||||
public required double NewScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Score delta (absolute change).
|
||||
/// </summary>
|
||||
[JsonPropertyName("scoreDelta")]
|
||||
public required double ScoreDelta { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Previous percentile (null for new entries).
|
||||
/// </summary>
|
||||
[JsonPropertyName("previousPercentile")]
|
||||
public double? PreviousPercentile { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// New percentile.
|
||||
/// </summary>
|
||||
[JsonPropertyName("newPercentile")]
|
||||
public required double NewPercentile { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Percentile delta (absolute change).
|
||||
/// </summary>
|
||||
[JsonPropertyName("percentileDelta")]
|
||||
public required double PercentileDelta { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Previous priority band (null for new entries).
|
||||
/// </summary>
|
||||
[JsonPropertyName("previousBand")]
|
||||
public string? PreviousBand { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// New priority band.
|
||||
/// </summary>
|
||||
[JsonPropertyName("newBand")]
|
||||
public required string NewBand { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the priority band changed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("bandChanged")]
|
||||
public bool BandChanged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// EPSS model date for the new score.
|
||||
/// </summary>
|
||||
[JsonPropertyName("modelDate")]
|
||||
public required string ModelDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Previous model date (null for new entries).
|
||||
/// </summary>
|
||||
[JsonPropertyName("previousModelDate")]
|
||||
public string? PreviousModelDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this change exceeds the reanalysis threshold.
|
||||
/// </summary>
|
||||
[JsonPropertyName("exceedsThreshold")]
|
||||
public required bool ExceedsThreshold { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Threshold that was exceeded (e.g., 0.2 for score delta).
|
||||
/// </summary>
|
||||
[JsonPropertyName("thresholdExceeded")]
|
||||
public double? ThresholdExceeded { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source of the EPSS data.
|
||||
/// </summary>
|
||||
[JsonPropertyName("source")]
|
||||
public string? Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp when this event was created.
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdAtUtc")]
|
||||
public required DateTimeOffset CreatedAtUtc { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Correlation ID for tracing.
|
||||
/// </summary>
|
||||
[JsonPropertyName("traceId")]
|
||||
public string? TraceId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch of EPSS change events for bulk processing.
|
||||
/// </summary>
|
||||
public sealed record EpssChangeBatch
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique batch identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("batchId")]
|
||||
public required string BatchId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tenant")]
|
||||
public required string Tenant { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Model date for all changes in this batch.
|
||||
/// </summary>
|
||||
[JsonPropertyName("modelDate")]
|
||||
public required string ModelDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of CVEs processed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("totalProcessed")]
|
||||
public required int TotalProcessed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of CVEs with changes exceeding threshold.
|
||||
/// </summary>
|
||||
[JsonPropertyName("changesExceedingThreshold")]
|
||||
public required int ChangesExceedingThreshold { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Individual change events (only those exceeding threshold).
|
||||
/// </summary>
|
||||
[JsonPropertyName("changes")]
|
||||
public required ImmutableArray<EpssChangeEvent> Changes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp when this batch was created.
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdAtUtc")]
|
||||
public required DateTimeOffset CreatedAtUtc { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Well-known EPSS event types.
|
||||
/// </summary>
|
||||
public static class EpssEventTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// EPSS score updated for a CVE.
|
||||
/// </summary>
|
||||
public const string Updated = "epss.updated";
|
||||
|
||||
/// <summary>
|
||||
/// Versioned event type alias for routing.
|
||||
/// </summary>
|
||||
public const string UpdatedV1 = "epss.updated@1";
|
||||
|
||||
/// <summary>
|
||||
/// EPSS delta exceeded threshold (triggers reanalysis).
|
||||
/// </summary>
|
||||
public const string DeltaExceeded = "epss.delta.exceeded";
|
||||
|
||||
/// <summary>
|
||||
/// New CVE added to EPSS data.
|
||||
/// </summary>
|
||||
public const string NewCve = "epss.cve.new";
|
||||
|
||||
/// <summary>
|
||||
/// Batch processing completed.
|
||||
/// </summary>
|
||||
public const string BatchCompleted = "epss.batch.completed";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EPSS change thresholds for reanalysis triggers.
|
||||
/// </summary>
|
||||
public static class EpssThresholds
|
||||
{
|
||||
/// <summary>
|
||||
/// Default score delta threshold for reanalysis (0.2 = 20% probability change).
|
||||
/// </summary>
|
||||
public const double DefaultScoreDelta = 0.2;
|
||||
|
||||
/// <summary>
|
||||
/// Default percentile delta threshold for reanalysis (0.1 = 10 percentile points).
|
||||
/// </summary>
|
||||
public const double DefaultPercentileDelta = 0.1;
|
||||
|
||||
/// <summary>
|
||||
/// High priority score threshold (above this triggers immediate reanalysis).
|
||||
/// </summary>
|
||||
public const double HighPriorityScore = 0.7;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating deterministic EPSS change events.
|
||||
/// </summary>
|
||||
public static class EpssChangeEventFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an EPSS change event with deterministic event ID.
|
||||
/// </summary>
|
||||
public static EpssChangeEvent Create(
|
||||
string tenant,
|
||||
string cveId,
|
||||
EpssEvidence? previous,
|
||||
EpssEvidence current,
|
||||
DateTimeOffset createdAtUtc,
|
||||
double scoreDeltaThreshold = EpssThresholds.DefaultScoreDelta,
|
||||
string? traceId = null)
|
||||
{
|
||||
var scoreDelta = previous is not null
|
||||
? Math.Abs(current.Score - previous.Score)
|
||||
: current.Score;
|
||||
|
||||
var percentileDelta = previous is not null
|
||||
? Math.Abs(current.Percentile - previous.Percentile)
|
||||
: current.Percentile;
|
||||
|
||||
var newBand = ComputePriorityBand(current.Score, current.Percentile);
|
||||
var previousBand = previous is not null
|
||||
? ComputePriorityBand(previous.Score, previous.Percentile)
|
||||
: null;
|
||||
|
||||
var bandChanged = previousBand is not null && !string.Equals(previousBand, newBand, StringComparison.Ordinal);
|
||||
|
||||
var exceedsThreshold = scoreDelta >= scoreDeltaThreshold
|
||||
|| current.Score >= EpssThresholds.HighPriorityScore
|
||||
|| bandChanged;
|
||||
|
||||
var eventType = previous is null
|
||||
? EpssEventTypes.NewCve
|
||||
: exceedsThreshold
|
||||
? EpssEventTypes.DeltaExceeded
|
||||
: EpssEventTypes.Updated;
|
||||
|
||||
var eventId = ComputeEventId(
|
||||
cveId,
|
||||
current.ModelDate,
|
||||
current.Score);
|
||||
|
||||
return new EpssChangeEvent
|
||||
{
|
||||
EventId = eventId,
|
||||
EventType = eventType,
|
||||
Tenant = tenant,
|
||||
CveId = cveId,
|
||||
PreviousScore = previous?.Score,
|
||||
NewScore = current.Score,
|
||||
ScoreDelta = scoreDelta,
|
||||
PreviousPercentile = previous?.Percentile,
|
||||
NewPercentile = current.Percentile,
|
||||
PercentileDelta = percentileDelta,
|
||||
PreviousBand = previousBand,
|
||||
NewBand = newBand,
|
||||
BandChanged = bandChanged,
|
||||
ModelDate = current.ModelDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
PreviousModelDate = previous?.ModelDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
ExceedsThreshold = exceedsThreshold,
|
||||
ThresholdExceeded = exceedsThreshold ? scoreDeltaThreshold : null,
|
||||
Source = current.Source,
|
||||
CreatedAtUtc = createdAtUtc,
|
||||
TraceId = traceId
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a batch of EPSS change events.
|
||||
/// </summary>
|
||||
public static EpssChangeBatch CreateBatch(
|
||||
string tenant,
|
||||
DateOnly modelDate,
|
||||
IEnumerable<EpssChangeEvent> allChanges,
|
||||
DateTimeOffset createdAtUtc)
|
||||
{
|
||||
var changesList = allChanges.ToList();
|
||||
var thresholdChanges = changesList
|
||||
.Where(c => c.ExceedsThreshold)
|
||||
.OrderBy(c => c.CveId, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
var batchId = ComputeBatchId(tenant, modelDate, thresholdChanges.Length);
|
||||
|
||||
return new EpssChangeBatch
|
||||
{
|
||||
BatchId = batchId,
|
||||
Tenant = tenant,
|
||||
ModelDate = modelDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
TotalProcessed = changesList.Count,
|
||||
ChangesExceedingThreshold = thresholdChanges.Length,
|
||||
Changes = thresholdChanges,
|
||||
CreatedAtUtc = createdAtUtc
|
||||
};
|
||||
}
|
||||
|
||||
private static string ComputeEventId(string cveId, DateOnly modelDate, double score)
|
||||
{
|
||||
var input = $"{cveId}|{modelDate:yyyy-MM-dd}|{score:F6}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"epss-evt-{Convert.ToHexStringLower(hash)[..16]}";
|
||||
}
|
||||
|
||||
private static string ComputeBatchId(string tenant, DateOnly modelDate, int changeCount)
|
||||
{
|
||||
var input = $"{tenant}|{modelDate:yyyy-MM-dd}|{changeCount}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"epss-batch-{Convert.ToHexStringLower(hash)[..16]}";
|
||||
}
|
||||
|
||||
private static string ComputePriorityBand(double score, double percentile)
|
||||
{
|
||||
// Critical: Top 1% by percentile or score > 0.8
|
||||
if (percentile >= 0.99 || score > 0.8)
|
||||
{
|
||||
return "critical";
|
||||
}
|
||||
|
||||
// High: Top 5% by percentile or score > 0.5
|
||||
if (percentile >= 0.95 || score > 0.5)
|
||||
{
|
||||
return "high";
|
||||
}
|
||||
|
||||
// Medium: Top 25% by percentile
|
||||
if (percentile >= 0.75)
|
||||
{
|
||||
return "medium";
|
||||
}
|
||||
|
||||
// Low: Below top 25%
|
||||
return "low";
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ namespace StellaOps.Scanner.Core;
|
||||
/// <param name="Deterministic">Whether the scan was run in deterministic mode.</param>
|
||||
/// <param name="Seed">32-byte seed for deterministic replay.</param>
|
||||
/// <param name="Knobs">Configuration knobs affecting the scan (depth limits, etc.).</param>
|
||||
/// <param name="ToolVersions">Version information for all tools used in the scan pipeline.</param>
|
||||
/// <param name="EvidenceDigests">Content-addressed digests of evidence artifacts for policy fingerprinting.</param>
|
||||
public sealed record ScanManifest(
|
||||
[property: JsonPropertyName("scanId")] string ScanId,
|
||||
[property: JsonPropertyName("createdAtUtc")] DateTimeOffset CreatedAtUtc,
|
||||
@@ -40,7 +42,10 @@ public sealed record ScanManifest(
|
||||
[property: JsonPropertyName("latticePolicyHash")] string LatticePolicyHash,
|
||||
[property: JsonPropertyName("deterministic")] bool Deterministic,
|
||||
[property: JsonPropertyName("seed")] byte[] Seed,
|
||||
[property: JsonPropertyName("knobs")] IReadOnlyDictionary<string, string> Knobs)
|
||||
[property: JsonPropertyName("knobs")] IReadOnlyDictionary<string, string> Knobs,
|
||||
// Sprint: SPRINT_20260112_005_SCANNER_epss_reanalysis_events (SCAN-EPSS-002)
|
||||
[property: JsonPropertyName("toolVersions")] ScanToolVersions? ToolVersions = null,
|
||||
[property: JsonPropertyName("evidenceDigests")] ScanEvidenceDigests? EvidenceDigests = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// Default JSON serializer options for canonical output.
|
||||
@@ -92,6 +97,90 @@ public sealed record ScanManifest(
|
||||
}
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_005_SCANNER_epss_reanalysis_events (SCAN-EPSS-002)
|
||||
|
||||
/// <summary>
|
||||
/// Version information for all tools used in the scan pipeline.
|
||||
/// Used for policy fingerprinting and offline replay validation.
|
||||
/// </summary>
|
||||
public sealed record ScanToolVersions
|
||||
{
|
||||
/// <summary>Scanner core version.</summary>
|
||||
[JsonPropertyName("scannerCore")]
|
||||
public string? ScannerCore { get; init; }
|
||||
|
||||
/// <summary>SBOM generator version (e.g., Syft).</summary>
|
||||
[JsonPropertyName("sbomGenerator")]
|
||||
public string? SbomGenerator { get; init; }
|
||||
|
||||
/// <summary>Vulnerability matcher version (e.g., Grype).</summary>
|
||||
[JsonPropertyName("vulnerabilityMatcher")]
|
||||
public string? VulnerabilityMatcher { get; init; }
|
||||
|
||||
/// <summary>Reachability analyzer version.</summary>
|
||||
[JsonPropertyName("reachabilityAnalyzer")]
|
||||
public string? ReachabilityAnalyzer { get; init; }
|
||||
|
||||
/// <summary>Binary indexer version.</summary>
|
||||
[JsonPropertyName("binaryIndexer")]
|
||||
public string? BinaryIndexer { get; init; }
|
||||
|
||||
/// <summary>EPSS model version (e.g., "v2024.01.15").</summary>
|
||||
[JsonPropertyName("epssModel")]
|
||||
public string? EpssModel { get; init; }
|
||||
|
||||
/// <summary>VEX evaluator version.</summary>
|
||||
[JsonPropertyName("vexEvaluator")]
|
||||
public string? VexEvaluator { get; init; }
|
||||
|
||||
/// <summary>Policy engine version.</summary>
|
||||
[JsonPropertyName("policyEngine")]
|
||||
public string? PolicyEngine { get; init; }
|
||||
|
||||
/// <summary>Additional tool versions as key-value pairs.</summary>
|
||||
[JsonPropertyName("additional")]
|
||||
public IReadOnlyDictionary<string, string>? Additional { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Content-addressed digests of evidence artifacts for policy fingerprinting.
|
||||
/// Used to detect when reanalysis is required due to evidence changes.
|
||||
/// </summary>
|
||||
public sealed record ScanEvidenceDigests
|
||||
{
|
||||
/// <summary>Digest of the SBOM artifact.</summary>
|
||||
[JsonPropertyName("sbomDigest")]
|
||||
public string? SbomDigest { get; init; }
|
||||
|
||||
/// <summary>Digest of the vulnerability findings.</summary>
|
||||
[JsonPropertyName("findingsDigest")]
|
||||
public string? FindingsDigest { get; init; }
|
||||
|
||||
/// <summary>Digest of the reachability graph.</summary>
|
||||
[JsonPropertyName("reachabilityDigest")]
|
||||
public string? ReachabilityDigest { get; init; }
|
||||
|
||||
/// <summary>Digest of aggregated VEX claims.</summary>
|
||||
[JsonPropertyName("vexDigest")]
|
||||
public string? VexDigest { get; init; }
|
||||
|
||||
/// <summary>Digest of runtime signals.</summary>
|
||||
[JsonPropertyName("runtimeDigest")]
|
||||
public string? RuntimeDigest { get; init; }
|
||||
|
||||
/// <summary>Digest of binary diff evidence.</summary>
|
||||
[JsonPropertyName("binaryDiffDigest")]
|
||||
public string? BinaryDiffDigest { get; init; }
|
||||
|
||||
/// <summary>Digest of EPSS scores used.</summary>
|
||||
[JsonPropertyName("epssDigest")]
|
||||
public string? EpssDigest { get; init; }
|
||||
|
||||
/// <summary>Combined fingerprint of all evidence (for quick comparison).</summary>
|
||||
[JsonPropertyName("combinedFingerprint")]
|
||||
public string? CombinedFingerprint { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builder for creating ScanManifest instances.
|
||||
/// </summary>
|
||||
@@ -110,6 +199,9 @@ public sealed class ScanManifestBuilder
|
||||
private bool _deterministic = true;
|
||||
private byte[] _seed = new byte[32];
|
||||
private readonly Dictionary<string, string> _knobs = [];
|
||||
// Sprint: SPRINT_20260112_005_SCANNER_epss_reanalysis_events (SCAN-EPSS-002)
|
||||
private ScanToolVersions? _toolVersions;
|
||||
private ScanEvidenceDigests? _evidenceDigests;
|
||||
|
||||
internal ScanManifestBuilder(string scanId, string artifactDigest, TimeProvider? timeProvider = null)
|
||||
{
|
||||
@@ -187,6 +279,26 @@ public sealed class ScanManifestBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the tool versions for policy fingerprinting.
|
||||
/// Sprint: SPRINT_20260112_005_SCANNER_epss_reanalysis_events (SCAN-EPSS-002)
|
||||
/// </summary>
|
||||
public ScanManifestBuilder WithToolVersions(ScanToolVersions toolVersions)
|
||||
{
|
||||
_toolVersions = toolVersions;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the evidence digests for policy fingerprinting.
|
||||
/// Sprint: SPRINT_20260112_005_SCANNER_epss_reanalysis_events (SCAN-EPSS-002)
|
||||
/// </summary>
|
||||
public ScanManifestBuilder WithEvidenceDigests(ScanEvidenceDigests evidenceDigests)
|
||||
{
|
||||
_evidenceDigests = evidenceDigests;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ScanManifest Build() => new(
|
||||
ScanId: _scanId,
|
||||
CreatedAtUtc: _createdAtUtc ?? _timeProvider.GetUtcNow(),
|
||||
@@ -199,5 +311,7 @@ public sealed class ScanManifestBuilder
|
||||
LatticePolicyHash: _latticePolicyHash,
|
||||
Deterministic: _deterministic,
|
||||
Seed: _seed,
|
||||
Knobs: _knobs.AsReadOnly());
|
||||
Knobs: _knobs.AsReadOnly(),
|
||||
ToolVersions: _toolVersions,
|
||||
EvidenceDigests: _evidenceDigests);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,9 @@ public sealed record RichGraphNode(
|
||||
IReadOnlyDictionary<string, string>? Attributes,
|
||||
string? SymbolDigest,
|
||||
ReachabilitySymbol? Symbol = null,
|
||||
string? CodeBlockHash = null)
|
||||
string? CodeBlockHash = null,
|
||||
// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-002)
|
||||
string? NodeHash = null)
|
||||
{
|
||||
public RichGraphNode Trimmed()
|
||||
{
|
||||
@@ -71,6 +73,7 @@ public sealed record RichGraphNode(
|
||||
BuildId = string.IsNullOrWhiteSpace(BuildId) ? null : BuildId.Trim(),
|
||||
CodeBlockHash = string.IsNullOrWhiteSpace(CodeBlockHash) ? null : CodeBlockHash.Trim(),
|
||||
SymbolDigest = string.IsNullOrWhiteSpace(SymbolDigest) ? null : SymbolDigest.Trim(),
|
||||
NodeHash = string.IsNullOrWhiteSpace(NodeHash) ? null : NodeHash.Trim(),
|
||||
Symbol = Symbol?.Trimmed(),
|
||||
Evidence = Evidence is null
|
||||
? Array.Empty<string>()
|
||||
|
||||
@@ -53,6 +53,14 @@ public sealed record ReachabilitySubgraphNode
|
||||
|
||||
[JsonPropertyName("attributes")]
|
||||
public IReadOnlyDictionary<string, string>? Attributes { get; init; }
|
||||
|
||||
// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-002)
|
||||
|
||||
/// <summary>
|
||||
/// Canonical node hash computed from PURL and symbol using NodeHashRecipe.
|
||||
/// </summary>
|
||||
[JsonPropertyName("nodeHash")]
|
||||
public string? NodeHash { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user