304 lines
9.3 KiB
C#
304 lines
9.3 KiB
C#
// SPDX-License-Identifier: BUSL-1.1
|
|
// Sprint: SPRINT_20260102_001_BE
|
|
// Task: DS-041 - VEX evidence emission for backport detection
|
|
|
|
using System.Collections.Immutable;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace StellaOps.Scanner.Evidence.Models;
|
|
|
|
/// <summary>
|
|
/// Evidence from binary delta signature analysis.
|
|
/// Provides cryptographic proof of backport status by comparing
|
|
/// normalized binary code against known patched/vulnerable signatures.
|
|
/// </summary>
|
|
public sealed record DeltaSignatureEvidence
|
|
{
|
|
/// <summary>
|
|
/// Schema version for this evidence type.
|
|
/// </summary>
|
|
[JsonPropertyName("schema")]
|
|
public string Schema { get; init; } = "stellaops.evidence.deltasig.v1";
|
|
|
|
/// <summary>
|
|
/// Overall result: patched, vulnerable, inconclusive.
|
|
/// </summary>
|
|
[JsonPropertyName("result")]
|
|
public required DeltaSigResult Result { get; init; }
|
|
|
|
/// <summary>
|
|
/// CVE identifier(s) that were evaluated.
|
|
/// </summary>
|
|
[JsonPropertyName("cveIds")]
|
|
public required ImmutableArray<string> CveIds { get; init; }
|
|
|
|
/// <summary>
|
|
/// Package reference (PURL) being analyzed.
|
|
/// </summary>
|
|
[JsonPropertyName("packagePurl")]
|
|
public required string PackagePurl { get; init; }
|
|
|
|
/// <summary>
|
|
/// Binary identifier (path or Build-ID).
|
|
/// </summary>
|
|
[JsonPropertyName("binaryId")]
|
|
public required string BinaryId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Target architecture (x86_64, aarch64, etc.).
|
|
/// </summary>
|
|
[JsonPropertyName("architecture")]
|
|
public required string Architecture { get; init; }
|
|
|
|
/// <summary>
|
|
/// Symbol-level match results.
|
|
/// </summary>
|
|
[JsonPropertyName("symbolMatches")]
|
|
public required ImmutableArray<SymbolMatchEvidence> SymbolMatches { get; init; }
|
|
|
|
/// <summary>
|
|
/// Confidence score [0, 1] of the overall determination.
|
|
/// </summary>
|
|
[JsonPropertyName("confidence")]
|
|
public required decimal Confidence { get; init; }
|
|
|
|
/// <summary>
|
|
/// Normalization recipe used for comparison.
|
|
/// </summary>
|
|
[JsonPropertyName("normalizationRecipe")]
|
|
public required NormalizationRecipeRef NormalizationRecipe { get; init; }
|
|
|
|
/// <summary>
|
|
/// When this evidence was generated.
|
|
/// </summary>
|
|
[JsonPropertyName("generatedAt")]
|
|
public required DateTimeOffset GeneratedAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// Human-readable summary of the determination.
|
|
/// </summary>
|
|
[JsonPropertyName("summary")]
|
|
public required string Summary { get; init; }
|
|
|
|
/// <summary>
|
|
/// Optional DSSE envelope URI containing the signed attestation.
|
|
/// </summary>
|
|
[JsonPropertyName("attestationUri")]
|
|
public string? AttestationUri { get; init; }
|
|
|
|
/// <summary>
|
|
/// Creates evidence for a patched binary.
|
|
/// </summary>
|
|
public static DeltaSignatureEvidence CreatePatched(
|
|
ImmutableArray<string> cveIds,
|
|
string packagePurl,
|
|
string binaryId,
|
|
string architecture,
|
|
ImmutableArray<SymbolMatchEvidence> symbolMatches,
|
|
decimal confidence,
|
|
NormalizationRecipeRef recipe,
|
|
TimeProvider? timeProvider = null)
|
|
{
|
|
var matchSummary = symbolMatches.IsDefaultOrEmpty
|
|
? "No symbols analyzed"
|
|
: $"{symbolMatches.Count(m => m.State == SignatureState.Patched)} patched, " +
|
|
$"{symbolMatches.Count(m => m.State == SignatureState.Vulnerable)} vulnerable";
|
|
|
|
return new DeltaSignatureEvidence
|
|
{
|
|
Result = DeltaSigResult.Patched,
|
|
CveIds = cveIds,
|
|
PackagePurl = packagePurl,
|
|
BinaryId = binaryId,
|
|
Architecture = architecture,
|
|
SymbolMatches = symbolMatches,
|
|
Confidence = confidence,
|
|
NormalizationRecipe = recipe,
|
|
GeneratedAt = (timeProvider ?? TimeProvider.System).GetUtcNow(),
|
|
Summary = $"Binary confirmed PATCHED with {confidence:P0} confidence. {matchSummary}."
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates evidence for a vulnerable binary.
|
|
/// </summary>
|
|
public static DeltaSignatureEvidence CreateVulnerable(
|
|
ImmutableArray<string> cveIds,
|
|
string packagePurl,
|
|
string binaryId,
|
|
string architecture,
|
|
ImmutableArray<SymbolMatchEvidence> symbolMatches,
|
|
decimal confidence,
|
|
NormalizationRecipeRef recipe,
|
|
TimeProvider? timeProvider = null)
|
|
{
|
|
var matchSummary = symbolMatches.IsDefaultOrEmpty
|
|
? "No symbols analyzed"
|
|
: $"{symbolMatches.Count(m => m.State == SignatureState.Vulnerable)} vulnerable symbols matched";
|
|
|
|
return new DeltaSignatureEvidence
|
|
{
|
|
Result = DeltaSigResult.Vulnerable,
|
|
CveIds = cveIds,
|
|
PackagePurl = packagePurl,
|
|
BinaryId = binaryId,
|
|
Architecture = architecture,
|
|
SymbolMatches = symbolMatches,
|
|
Confidence = confidence,
|
|
NormalizationRecipe = recipe,
|
|
GeneratedAt = (timeProvider ?? TimeProvider.System).GetUtcNow(),
|
|
Summary = $"Binary confirmed VULNERABLE with {confidence:P0} confidence. {matchSummary}."
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates evidence for an inconclusive analysis.
|
|
/// </summary>
|
|
public static DeltaSignatureEvidence CreateInconclusive(
|
|
ImmutableArray<string> cveIds,
|
|
string packagePurl,
|
|
string binaryId,
|
|
string architecture,
|
|
string reason,
|
|
ImmutableArray<SymbolMatchEvidence> symbolMatches,
|
|
NormalizationRecipeRef recipe,
|
|
TimeProvider? timeProvider = null)
|
|
{
|
|
return new DeltaSignatureEvidence
|
|
{
|
|
Result = DeltaSigResult.Inconclusive,
|
|
CveIds = cveIds,
|
|
PackagePurl = packagePurl,
|
|
BinaryId = binaryId,
|
|
Architecture = architecture,
|
|
SymbolMatches = symbolMatches,
|
|
Confidence = 0m,
|
|
NormalizationRecipe = recipe,
|
|
GeneratedAt = (timeProvider ?? TimeProvider.System).GetUtcNow(),
|
|
Summary = $"Analysis INCONCLUSIVE: {reason}"
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of delta signature analysis.
|
|
/// </summary>
|
|
[JsonConverter(typeof(JsonStringEnumConverter<DeltaSigResult>))]
|
|
public enum DeltaSigResult
|
|
{
|
|
/// <summary>
|
|
/// Binary contains patched code - security fix is applied.
|
|
/// </summary>
|
|
[JsonStringEnumMemberName("patched")]
|
|
Patched,
|
|
|
|
/// <summary>
|
|
/// Binary contains vulnerable code - security fix is NOT applied.
|
|
/// </summary>
|
|
[JsonStringEnumMemberName("vulnerable")]
|
|
Vulnerable,
|
|
|
|
/// <summary>
|
|
/// Unable to determine patch status (symbols not found, LTO obfuscation, etc.).
|
|
/// </summary>
|
|
[JsonStringEnumMemberName("inconclusive")]
|
|
Inconclusive
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evidence for a single symbol match.
|
|
/// </summary>
|
|
public sealed record SymbolMatchEvidence
|
|
{
|
|
/// <summary>
|
|
/// Symbol/function name.
|
|
/// </summary>
|
|
[JsonPropertyName("symbolName")]
|
|
public required string SymbolName { get; init; }
|
|
|
|
/// <summary>
|
|
/// Normalized hash of the symbol (SHA-256).
|
|
/// </summary>
|
|
[JsonPropertyName("hashHex")]
|
|
public required string HashHex { get; init; }
|
|
|
|
/// <summary>
|
|
/// Matched signature state (patched or vulnerable).
|
|
/// </summary>
|
|
[JsonPropertyName("state")]
|
|
public required SignatureState State { get; init; }
|
|
|
|
/// <summary>
|
|
/// Match confidence [0, 1].
|
|
/// </summary>
|
|
[JsonPropertyName("confidence")]
|
|
public required decimal Confidence { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether this was an exact full-hash match.
|
|
/// </summary>
|
|
[JsonPropertyName("exactMatch")]
|
|
public required bool ExactMatch { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of chunks matched (for partial matching).
|
|
/// </summary>
|
|
[JsonPropertyName("chunksMatched")]
|
|
public int? ChunksMatched { get; init; }
|
|
|
|
/// <summary>
|
|
/// Total chunks in signature.
|
|
/// </summary>
|
|
[JsonPropertyName("chunksTotal")]
|
|
public int? ChunksTotal { get; init; }
|
|
|
|
/// <summary>
|
|
/// Reference to the matched signature (PURL of the signature source package).
|
|
/// </summary>
|
|
[JsonPropertyName("signatureSourcePurl")]
|
|
public string? SignatureSourcePurl { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// State of a signature: vulnerable or patched.
|
|
/// </summary>
|
|
[JsonConverter(typeof(JsonStringEnumConverter<SignatureState>))]
|
|
public enum SignatureState
|
|
{
|
|
/// <summary>
|
|
/// Signature represents vulnerable code (before fix).
|
|
/// </summary>
|
|
[JsonStringEnumMemberName("vulnerable")]
|
|
Vulnerable,
|
|
|
|
/// <summary>
|
|
/// Signature represents patched code (after fix).
|
|
/// </summary>
|
|
[JsonStringEnumMemberName("patched")]
|
|
Patched
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reference to the normalization recipe used.
|
|
/// </summary>
|
|
public sealed record NormalizationRecipeRef
|
|
{
|
|
/// <summary>
|
|
/// Recipe identifier (e.g., "stellaops.normalize.x64.v1").
|
|
/// </summary>
|
|
[JsonPropertyName("recipeId")]
|
|
public required string RecipeId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Recipe version.
|
|
/// </summary>
|
|
[JsonPropertyName("version")]
|
|
public required string Version { get; init; }
|
|
|
|
/// <summary>
|
|
/// Normalization steps applied.
|
|
/// </summary>
|
|
[JsonPropertyName("steps")]
|
|
public ImmutableArray<string> Steps { get; init; } = [];
|
|
}
|