Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
@@ -0,0 +1,443 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// FuncProofBuilder.cs
|
||||
// Sprint: SPRINT_20251226_009_SCANNER_funcproof
|
||||
// Tasks: FUNC-05, FUNC-07, FUNC-10, FUNC-11 — Symbol/function hashing and trace serialization
|
||||
// Description: Builds FuncProof documents from binary analysis results.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Evidence.Models;
|
||||
|
||||
namespace StellaOps.Scanner.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Builds FuncProof documents from binary analysis results.
|
||||
/// </summary>
|
||||
public sealed class FuncProofBuilder
|
||||
{
|
||||
private static readonly JsonSerializerOptions CanonicalJsonOptions = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private ICryptoHash? _cryptoHash;
|
||||
private FuncProofGenerationOptions _options = new();
|
||||
private string? _buildId;
|
||||
private string? _buildIdType;
|
||||
private string? _fileSha256;
|
||||
private string? _binaryFormat;
|
||||
private string? _architecture;
|
||||
private bool _isStripped;
|
||||
private readonly Dictionary<string, FuncProofSection> _sections = new();
|
||||
private readonly List<FuncProofFunctionBuilder> _functions = [];
|
||||
private readonly List<FuncProofTrace> _traces = [];
|
||||
private FuncProofMetadata? _metadata;
|
||||
private string _generatorVersion = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cryptographic hash provider for regional compliance.
|
||||
/// If not set, defaults to SHA-256 for backward compatibility.
|
||||
/// </summary>
|
||||
public FuncProofBuilder WithCryptoHash(ICryptoHash cryptoHash)
|
||||
{
|
||||
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the generation options for configurable parameters.
|
||||
/// </summary>
|
||||
public FuncProofBuilder WithOptions(FuncProofGenerationOptions options)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the binary identity information.
|
||||
/// </summary>
|
||||
public FuncProofBuilder WithBinaryIdentity(
|
||||
string buildId,
|
||||
string buildIdType,
|
||||
string fileSha256,
|
||||
string binaryFormat,
|
||||
string architecture,
|
||||
bool isStripped)
|
||||
{
|
||||
_buildId = buildId;
|
||||
_buildIdType = buildIdType;
|
||||
_fileSha256 = fileSha256;
|
||||
_binaryFormat = binaryFormat;
|
||||
_architecture = architecture;
|
||||
_isStripped = isStripped;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a section with hash.
|
||||
/// </summary>
|
||||
public FuncProofBuilder AddSection(string name, byte[] content, long offset, long? virtualAddress = null)
|
||||
{
|
||||
var hash = ComputeBlake3Hash(content);
|
||||
_sections[name] = new FuncProofSection
|
||||
{
|
||||
Hash = $"blake3:{hash}",
|
||||
Offset = offset,
|
||||
Size = content.Length,
|
||||
VirtualAddress = virtualAddress
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a section with pre-computed hash.
|
||||
/// </summary>
|
||||
public FuncProofBuilder AddSection(string name, string hash, long offset, long size, long? virtualAddress = null)
|
||||
{
|
||||
_sections[name] = new FuncProofSection
|
||||
{
|
||||
Hash = hash.StartsWith("blake3:") ? hash : $"blake3:{hash}",
|
||||
Offset = offset,
|
||||
Size = size,
|
||||
VirtualAddress = virtualAddress
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a function definition.
|
||||
/// </summary>
|
||||
public FuncProofFunctionBuilder AddFunction(string symbol, long startAddress, long endAddress)
|
||||
{
|
||||
var builder = new FuncProofFunctionBuilder(this, symbol, startAddress, endAddress);
|
||||
_functions.Add(builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an entry→sink trace.
|
||||
/// </summary>
|
||||
public FuncProofBuilder AddTrace(
|
||||
string entrySymbolDigest,
|
||||
string sinkSymbolDigest,
|
||||
IReadOnlyList<(string callerDigest, string calleeDigest)> edges,
|
||||
IReadOnlyList<string>? path = null)
|
||||
{
|
||||
var edgeListHash = ComputeEdgeListHash(edges);
|
||||
var hopCount = edges.Count;
|
||||
var maxHops = _options.MaxTraceHops;
|
||||
var truncated = hopCount > maxHops;
|
||||
|
||||
var effectivePath = path ?? edges.Select(e => e.calleeDigest).Prepend(entrySymbolDigest).ToList();
|
||||
if (effectivePath.Count > maxHops + 1)
|
||||
{
|
||||
effectivePath = effectivePath.Take(maxHops + 1).ToList();
|
||||
truncated = true;
|
||||
}
|
||||
|
||||
var trace = new FuncProofTrace
|
||||
{
|
||||
TraceId = $"trace-{_traces.Count + 1}",
|
||||
EdgeListHash = $"blake3:{edgeListHash}",
|
||||
HopCount = Math.Min(hopCount, maxHops),
|
||||
EntrySymbolDigest = entrySymbolDigest,
|
||||
SinkSymbolDigest = sinkSymbolDigest,
|
||||
Path = effectivePath.ToImmutableArray(),
|
||||
Truncated = truncated
|
||||
};
|
||||
|
||||
_traces.Add(trace);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets build metadata.
|
||||
/// </summary>
|
||||
public FuncProofBuilder WithMetadata(FuncProofMetadata metadata)
|
||||
{
|
||||
_metadata = metadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the generator version.
|
||||
/// </summary>
|
||||
public FuncProofBuilder WithGeneratorVersion(string version)
|
||||
{
|
||||
_generatorVersion = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the FuncProof document.
|
||||
/// </summary>
|
||||
public FuncProof Build()
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(_buildId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(_buildIdType);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(_fileSha256);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(_binaryFormat);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(_architecture);
|
||||
|
||||
var functions = _functions
|
||||
.Select(f => f.Build())
|
||||
.OrderBy(f => f.Start, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
var sections = _sections
|
||||
.OrderBy(kvp => kvp.Key, StringComparer.Ordinal)
|
||||
.ToImmutableDictionary();
|
||||
|
||||
var traces = _traces
|
||||
.OrderBy(t => t.TraceId, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
// Build initial proof without proofId
|
||||
var proof = new FuncProof
|
||||
{
|
||||
ProofId = string.Empty, // Placeholder
|
||||
BuildId = _buildId,
|
||||
BuildIdType = _buildIdType,
|
||||
FileSha256 = _fileSha256,
|
||||
BinaryFormat = _binaryFormat,
|
||||
Architecture = _architecture,
|
||||
IsStripped = _isStripped,
|
||||
Sections = sections,
|
||||
Functions = functions,
|
||||
Traces = traces,
|
||||
Meta = _metadata,
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
GeneratorVersion = _generatorVersion
|
||||
};
|
||||
|
||||
// Compute content-addressable ID
|
||||
var proofId = ComputeProofId(proof, _cryptoHash);
|
||||
|
||||
return proof with { ProofId = proofId };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the content-addressable proof ID.
|
||||
/// Uses ICryptoHash for regional compliance (defaults to BLAKE3 in "world" profile).
|
||||
/// </summary>
|
||||
public static string ComputeProofId(FuncProof proof, ICryptoHash? cryptoHash = null)
|
||||
{
|
||||
// Create a version without proofId for hashing
|
||||
var forHashing = proof with { ProofId = string.Empty };
|
||||
var json = JsonSerializer.Serialize(forHashing, CanonicalJsonOptions);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
var hash = ComputeHashForGraph(bytes, cryptoHash);
|
||||
|
||||
// Prefix indicates algorithm used (determined by compliance profile)
|
||||
var algorithmPrefix = cryptoHash is not null ? "graph" : "sha256";
|
||||
return $"{algorithmPrefix}:{hash}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes symbol digest: BLAKE3(symbol_name + "|" + start + "|" + end).
|
||||
/// Uses ICryptoHash for regional compliance (defaults to BLAKE3 in "world" profile).
|
||||
/// </summary>
|
||||
public static string ComputeSymbolDigest(string symbol, long start, long end, ICryptoHash? cryptoHash = null)
|
||||
{
|
||||
var input = $"{symbol}|{start:x}|{end:x}";
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
return ComputeHashForGraph(bytes, cryptoHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes function range hash over the function bytes.
|
||||
/// Uses ICryptoHash for regional compliance (defaults to BLAKE3 in "world" profile).
|
||||
/// </summary>
|
||||
public static string ComputeFunctionHash(byte[] functionBytes, ICryptoHash? cryptoHash = null)
|
||||
{
|
||||
return ComputeHashForGraph(functionBytes, cryptoHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes edge list hash: hash of sorted edge pairs.
|
||||
/// Uses ICryptoHash for regional compliance (defaults to BLAKE3 in "world" profile).
|
||||
/// </summary>
|
||||
private static string ComputeEdgeListHash(IReadOnlyList<(string callerDigest, string calleeDigest)> edges, ICryptoHash? cryptoHash = null)
|
||||
{
|
||||
var sortedEdges = edges
|
||||
.Select(e => $"{e.callerDigest}→{e.calleeDigest}")
|
||||
.OrderBy(e => e, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
var edgeList = string.Join("\n", sortedEdges);
|
||||
var bytes = Encoding.UTF8.GetBytes(edgeList);
|
||||
return ComputeHashForGraph(bytes, cryptoHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes hash using the Graph purpose from ICryptoHash.
|
||||
/// Falls back to SHA-256 if no crypto hash provider is available.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Default algorithm by compliance profile:
|
||||
/// - world: BLAKE3-256
|
||||
/// - fips/kcmvp/eidas: SHA-256
|
||||
/// - gost: GOST3411-2012-256
|
||||
/// - sm: SM3
|
||||
/// </remarks>
|
||||
private static string ComputeHashForGraph(byte[] data, ICryptoHash? cryptoHash)
|
||||
{
|
||||
if (cryptoHash is not null)
|
||||
{
|
||||
// Use purpose-based hashing for compliance-aware algorithm selection
|
||||
return cryptoHash.ComputeHashHexForPurpose(data, HashPurpose.Graph);
|
||||
}
|
||||
|
||||
// Fallback: use SHA-256 when no ICryptoHash provider is available
|
||||
// This maintains backward compatibility for tests and standalone usage
|
||||
var hash = SHA256.HashData(data);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builder for individual function entries.
|
||||
/// </summary>
|
||||
public sealed class FuncProofFunctionBuilder
|
||||
{
|
||||
private readonly FuncProofBuilder _parent;
|
||||
private readonly string _symbol;
|
||||
private readonly long _startAddress;
|
||||
private readonly long _endAddress;
|
||||
private string? _mangledName;
|
||||
private byte[]? _functionBytes;
|
||||
private string? _precomputedHash;
|
||||
private double _confidence = 1.0;
|
||||
private string? _sourceFile;
|
||||
private int? _sourceLine;
|
||||
private bool _isEntrypoint;
|
||||
private string? _entrypointType;
|
||||
private bool _isSink;
|
||||
private string? _sinkVulnId;
|
||||
|
||||
internal FuncProofFunctionBuilder(FuncProofBuilder parent, string symbol, long startAddress, long endAddress)
|
||||
{
|
||||
_parent = parent;
|
||||
_symbol = symbol;
|
||||
_startAddress = startAddress;
|
||||
_endAddress = endAddress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the mangled name if different from symbol.
|
||||
/// </summary>
|
||||
public FuncProofFunctionBuilder WithMangledName(string mangledName)
|
||||
{
|
||||
_mangledName = mangledName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the function bytes for hash computation.
|
||||
/// </summary>
|
||||
public FuncProofFunctionBuilder WithBytes(byte[] bytes)
|
||||
{
|
||||
_functionBytes = bytes;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a pre-computed hash.
|
||||
/// </summary>
|
||||
public FuncProofFunctionBuilder WithHash(string hash)
|
||||
{
|
||||
_precomputedHash = hash;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the confidence level for boundary detection.
|
||||
/// </summary>
|
||||
public FuncProofFunctionBuilder WithConfidence(double confidence)
|
||||
{
|
||||
_confidence = confidence;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets source location from DWARF info.
|
||||
/// </summary>
|
||||
public FuncProofFunctionBuilder WithSourceLocation(string file, int line)
|
||||
{
|
||||
_sourceFile = file;
|
||||
_sourceLine = line;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks this function as an entrypoint.
|
||||
/// </summary>
|
||||
public FuncProofFunctionBuilder AsEntrypoint(string? type = null)
|
||||
{
|
||||
_isEntrypoint = true;
|
||||
_entrypointType = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks this function as a vulnerable sink.
|
||||
/// </summary>
|
||||
public FuncProofFunctionBuilder AsSink(string? vulnId = null)
|
||||
{
|
||||
_isSink = true;
|
||||
_sinkVulnId = vulnId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns to the parent builder.
|
||||
/// </summary>
|
||||
public FuncProofBuilder Done() => _parent;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the function entry.
|
||||
/// </summary>
|
||||
internal FuncProofFunction Build()
|
||||
{
|
||||
var symbolDigest = FuncProofBuilder.ComputeSymbolDigest(_symbol, _startAddress, _endAddress);
|
||||
|
||||
string hash;
|
||||
if (_precomputedHash != null)
|
||||
{
|
||||
hash = _precomputedHash.StartsWith("blake3:") ? _precomputedHash : $"blake3:{_precomputedHash}";
|
||||
}
|
||||
else if (_functionBytes != null)
|
||||
{
|
||||
hash = $"blake3:{FuncProofBuilder.ComputeFunctionHash(_functionBytes)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use symbol digest as fallback hash
|
||||
hash = $"blake3:{symbolDigest}";
|
||||
}
|
||||
|
||||
return new FuncProofFunction
|
||||
{
|
||||
Symbol = _symbol,
|
||||
MangledName = _mangledName,
|
||||
SymbolDigest = symbolDigest,
|
||||
Start = $"0x{_startAddress:x}",
|
||||
End = $"0x{_endAddress:x}",
|
||||
Size = _endAddress - _startAddress,
|
||||
Hash = hash,
|
||||
Confidence = _confidence,
|
||||
SourceFile = _sourceFile,
|
||||
SourceLine = _sourceLine,
|
||||
IsEntrypoint = _isEntrypoint,
|
||||
EntrypointType = _entrypointType,
|
||||
IsSink = _isSink,
|
||||
SinkVulnId = _sinkVulnId
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user