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,404 @@
|
||||
namespace StellaOps.Signals.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Symbol canonicalization service interface.
|
||||
/// Sprint: SPRINT_20251226_010_SIGNALS_runtime_stack
|
||||
/// Task: STACK-06
|
||||
///
|
||||
/// Resolves program counter addresses to canonical (Build-ID, function, offset) tuples.
|
||||
/// </summary>
|
||||
public interface ISymbolCanonicalizationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves a program counter address to a canonical symbol.
|
||||
/// </summary>
|
||||
/// <param name="address">The program counter address.</param>
|
||||
/// <param name="buildId">The ELF Build-ID of the binary.</param>
|
||||
/// <param name="binaryPath">Optional path to the binary for symbol lookup.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The resolved symbol, or null if resolution failed.</returns>
|
||||
Task<CanonicalSymbol?> ResolveAsync(
|
||||
ulong address,
|
||||
string? buildId,
|
||||
string? binaryPath = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves multiple addresses in batch for efficiency.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<CanonicalSymbol?>> ResolveBatchAsync(
|
||||
IReadOnlyList<SymbolResolutionRequest> requests,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a Java frame using JVMTI metadata.
|
||||
/// </summary>
|
||||
Task<CanonicalSymbol?> ResolveJavaFrameAsync(
|
||||
ulong address,
|
||||
JavaFrameMetadata metadata,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a .NET frame using DAC (Data Access Component).
|
||||
/// </summary>
|
||||
Task<CanonicalSymbol?> ResolveDotNetFrameAsync(
|
||||
ulong address,
|
||||
DotNetFrameMetadata metadata,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a Python frame using interpreter symbols.
|
||||
/// </summary>
|
||||
Task<CanonicalSymbol?> ResolvePythonFrameAsync(
|
||||
ulong address,
|
||||
PythonFrameMetadata metadata,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a Build-ID is in the local symbol cache.
|
||||
/// </summary>
|
||||
Task<bool> IsInCacheAsync(string buildId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Adds symbols for a Build-ID to the cache.
|
||||
/// </summary>
|
||||
Task CacheSymbolsAsync(
|
||||
string buildId,
|
||||
IReadOnlyList<SymbolEntry> symbols,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request for symbol resolution.
|
||||
/// </summary>
|
||||
public sealed record SymbolResolutionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Program counter address.
|
||||
/// </summary>
|
||||
public required ulong Address { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// ELF Build-ID of the binary.
|
||||
/// </summary>
|
||||
public string? BuildId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to the binary file.
|
||||
/// </summary>
|
||||
public string? BinaryPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime type hint for managed runtimes.
|
||||
/// </summary>
|
||||
public RuntimeType RuntimeType { get; init; } = RuntimeType.Native;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime type for symbol resolution hints.
|
||||
/// </summary>
|
||||
public enum RuntimeType
|
||||
{
|
||||
/// <summary>
|
||||
/// Native code (C, C++, Rust, Go, etc.).
|
||||
/// </summary>
|
||||
Native,
|
||||
|
||||
/// <summary>
|
||||
/// Java Virtual Machine.
|
||||
/// </summary>
|
||||
Java,
|
||||
|
||||
/// <summary>
|
||||
/// .NET Common Language Runtime.
|
||||
/// </summary>
|
||||
DotNet,
|
||||
|
||||
/// <summary>
|
||||
/// Python interpreter.
|
||||
/// </summary>
|
||||
Python,
|
||||
|
||||
/// <summary>
|
||||
/// Node.js / V8.
|
||||
/// </summary>
|
||||
NodeJs,
|
||||
|
||||
/// <summary>
|
||||
/// Ruby interpreter.
|
||||
/// </summary>
|
||||
Ruby,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonical symbol representation.
|
||||
/// </summary>
|
||||
public sealed record CanonicalSymbol
|
||||
{
|
||||
/// <summary>
|
||||
/// Original address that was resolved.
|
||||
/// </summary>
|
||||
public required ulong Address { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// ELF Build-ID of the containing binary.
|
||||
/// </summary>
|
||||
public required string BuildId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Demangled function name.
|
||||
/// </summary>
|
||||
public required string FunctionName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Offset within the function.
|
||||
/// </summary>
|
||||
public required ulong Offset { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Module or binary name.
|
||||
/// </summary>
|
||||
public string? ModuleName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source file path (if debug info available).
|
||||
/// </summary>
|
||||
public string? SourceFile { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source line number.
|
||||
/// </summary>
|
||||
public int? SourceLine { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this symbol is from a trusted source.
|
||||
/// </summary>
|
||||
public bool IsTrusted { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolution method used.
|
||||
/// </summary>
|
||||
public SymbolResolutionMethod ResolutionMethod { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the canonical string format.
|
||||
/// </summary>
|
||||
public string ToCanonicalString()
|
||||
{
|
||||
return $"{BuildId[..Math.Min(16, BuildId.Length)]}:{FunctionName}+0x{Offset:x}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a canonical string format.
|
||||
/// </summary>
|
||||
public static CanonicalSymbol? Parse(string canonical)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(canonical))
|
||||
return null;
|
||||
|
||||
// Format: "buildid:function+0xoffset"
|
||||
var colonIdx = canonical.IndexOf(':');
|
||||
if (colonIdx < 0)
|
||||
return null;
|
||||
|
||||
var buildId = canonical[..colonIdx];
|
||||
var rest = canonical[(colonIdx + 1)..];
|
||||
|
||||
var plusIdx = rest.LastIndexOf('+');
|
||||
if (plusIdx < 0)
|
||||
return null;
|
||||
|
||||
var functionName = rest[..plusIdx];
|
||||
var offsetStr = rest[(plusIdx + 1)..];
|
||||
|
||||
if (!offsetStr.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
if (!ulong.TryParse(offsetStr[2..], System.Globalization.NumberStyles.HexNumber, null, out var offset))
|
||||
return null;
|
||||
|
||||
return new CanonicalSymbol
|
||||
{
|
||||
Address = 0, // Not recoverable from canonical string
|
||||
BuildId = buildId,
|
||||
FunctionName = functionName,
|
||||
Offset = offset,
|
||||
ResolutionMethod = SymbolResolutionMethod.Parsed,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method used to resolve the symbol.
|
||||
/// </summary>
|
||||
public enum SymbolResolutionMethod
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolved from ELF symbol table.
|
||||
/// </summary>
|
||||
ElfSymtab,
|
||||
|
||||
/// <summary>
|
||||
/// Resolved from DWARF debug info.
|
||||
/// </summary>
|
||||
DwarfDebugInfo,
|
||||
|
||||
/// <summary>
|
||||
/// Resolved from local symbol cache.
|
||||
/// </summary>
|
||||
LocalCache,
|
||||
|
||||
/// <summary>
|
||||
/// Resolved from debuginfod server.
|
||||
/// </summary>
|
||||
Debuginfod,
|
||||
|
||||
/// <summary>
|
||||
/// Resolved from JIT metadata (Java/V8/etc).
|
||||
/// </summary>
|
||||
JitMetadata,
|
||||
|
||||
/// <summary>
|
||||
/// Resolved from runtime-specific mechanism.
|
||||
/// </summary>
|
||||
RuntimeSpecific,
|
||||
|
||||
/// <summary>
|
||||
/// Parsed from canonical string format.
|
||||
/// </summary>
|
||||
Parsed,
|
||||
|
||||
/// <summary>
|
||||
/// Could not resolve, using address only.
|
||||
/// </summary>
|
||||
Unresolved,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Symbol entry for cache storage.
|
||||
/// </summary>
|
||||
public sealed record SymbolEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Start address of the symbol.
|
||||
/// </summary>
|
||||
public required ulong StartAddress { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the symbol in bytes.
|
||||
/// </summary>
|
||||
public required ulong Size { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol name (demangled).
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol type.
|
||||
/// </summary>
|
||||
public SymbolType Type { get; init; } = SymbolType.Function;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of symbol.
|
||||
/// </summary>
|
||||
public enum SymbolType
|
||||
{
|
||||
Function,
|
||||
Object,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata for Java frame resolution.
|
||||
/// </summary>
|
||||
public sealed record JavaFrameMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Class name.
|
||||
/// </summary>
|
||||
public string? ClassName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Method name.
|
||||
/// </summary>
|
||||
public string? MethodName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Method signature.
|
||||
/// </summary>
|
||||
public string? Signature { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Bytecode index.
|
||||
/// </summary>
|
||||
public int? BytecodeIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is a JIT-compiled frame.
|
||||
/// </summary>
|
||||
public bool IsJit { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata for .NET frame resolution.
|
||||
/// </summary>
|
||||
public sealed record DotNetFrameMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Type name.
|
||||
/// </summary>
|
||||
public string? TypeName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Method name.
|
||||
/// </summary>
|
||||
public string? MethodName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Method token.
|
||||
/// </summary>
|
||||
public uint? MethodToken { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// IL offset.
|
||||
/// </summary>
|
||||
public int? IlOffset { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Assembly name.
|
||||
/// </summary>
|
||||
public string? AssemblyName { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata for Python frame resolution.
|
||||
/// </summary>
|
||||
public sealed record PythonFrameMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Module name.
|
||||
/// </summary>
|
||||
public string? ModuleName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Function name.
|
||||
/// </summary>
|
||||
public string? FunctionName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source file path.
|
||||
/// </summary>
|
||||
public string? SourceFile { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Line number.
|
||||
/// </summary>
|
||||
public int? LineNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Python version.
|
||||
/// </summary>
|
||||
public string? PythonVersion { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user