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:
StellaOps Bot
2025-12-26 15:17:15 +02:00
parent 7792749bb4
commit 907783f625
354 changed files with 79727 additions and 1346 deletions

View File

@@ -0,0 +1,435 @@
using System.Runtime.Versioning;
namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture;
/// <summary>
/// Stack trace capture configuration and models.
/// Sprint: SPRINT_20251226_010_SIGNALS_runtime_stack
/// Tasks: STACK-01 to STACK-05
/// </summary>
/// <summary>
/// Configuration for stack trace sampling.
/// </summary>
public sealed record StackTraceCaptureOptions
{
/// <summary>
/// Sampling frequency in Hz. Default: 49 Hz (prime number to avoid aliasing).
/// </summary>
public int SamplingFrequencyHz { get; init; } = 49;
/// <summary>
/// Maximum stack depth to capture. Default: 64 frames.
/// </summary>
public int MaxStackDepth { get; init; } = 64;
/// <summary>
/// Duration to sample for each workload. Default: 30 seconds.
/// </summary>
public TimeSpan SamplingDuration { get; init; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Whether to capture kernel stack frames.
/// </summary>
public bool CaptureKernelStacks { get; init; } = false;
/// <summary>
/// Whether to capture user stack frames.
/// </summary>
public bool CaptureUserStacks { get; init; } = true;
/// <summary>
/// Target P99 overhead percentage. Sampling will throttle if exceeded.
/// </summary>
public double MaxOverheadPercent { get; init; } = 1.0;
/// <summary>
/// Privacy settings for stack trace redaction.
/// </summary>
public StackTracePrivacyOptions Privacy { get; init; } = new();
/// <summary>
/// Validates the options and returns any errors.
/// </summary>
public IReadOnlyList<string> Validate()
{
var errors = new List<string>();
if (SamplingFrequencyHz < 1 || SamplingFrequencyHz > 999)
errors.Add("SamplingFrequencyHz must be between 1 and 999 Hz");
if (MaxStackDepth < 1 || MaxStackDepth > 256)
errors.Add("MaxStackDepth must be between 1 and 256");
if (SamplingDuration < TimeSpan.FromSeconds(1) || SamplingDuration > TimeSpan.FromMinutes(10))
errors.Add("SamplingDuration must be between 1 second and 10 minutes");
if (MaxOverheadPercent < 0.1 || MaxOverheadPercent > 10.0)
errors.Add("MaxOverheadPercent must be between 0.1 and 10.0");
return errors;
}
}
/// <summary>
/// Privacy options for stack trace redaction.
/// </summary>
public sealed record StackTracePrivacyOptions
{
/// <summary>
/// Whether to redact file paths.
/// </summary>
public bool RedactPaths { get; init; } = true;
/// <summary>
/// Whether to hash short-lived local variable names.
/// </summary>
public bool HashLocalVariables { get; init; } = true;
/// <summary>
/// Patterns to always redact from stack traces.
/// </summary>
public IReadOnlyList<string> RedactionPatterns { get; init; } = new[]
{
"/home/*",
"/tmp/*",
"password",
"secret",
"token",
"key",
};
/// <summary>
/// Default retention period for stack traces. Default: 24 hours.
/// </summary>
public TimeSpan RetentionPeriod { get; init; } = TimeSpan.FromHours(24);
}
/// <summary>
/// Represents a captured stack trace sample.
/// </summary>
public sealed record StackTraceSample
{
/// <summary>
/// Timestamp when the sample was captured (UTC).
/// </summary>
public required DateTime Timestamp { get; init; }
/// <summary>
/// Process ID of the sampled process.
/// </summary>
public required int ProcessId { get; init; }
/// <summary>
/// Thread ID that was sampled.
/// </summary>
public required int ThreadId { get; init; }
/// <summary>
/// Container ID if running in a container.
/// </summary>
public string? ContainerId { get; init; }
/// <summary>
/// Container image digest.
/// </summary>
public string? ImageDigest { get; init; }
/// <summary>
/// User-space stack frames (caller first).
/// </summary>
public required IReadOnlyList<StackFrame> UserFrames { get; init; }
/// <summary>
/// Kernel-space stack frames (caller first).
/// </summary>
public IReadOnlyList<StackFrame>? KernelFrames { get; init; }
/// <summary>
/// CPU core on which the sample was taken.
/// </summary>
public int? CpuCore { get; init; }
/// <summary>
/// Observation count (for folded format aggregation).
/// </summary>
public int Count { get; init; } = 1;
}
/// <summary>
/// Represents a single stack frame.
/// </summary>
public sealed record StackFrame
{
/// <summary>
/// Program counter / instruction pointer address.
/// </summary>
public required ulong Address { get; init; }
/// <summary>
/// ELF Build-ID of the binary containing this address.
/// </summary>
public string? BuildId { get; init; }
/// <summary>
/// Path to the binary (may be redacted).
/// </summary>
public string? BinaryPath { get; init; }
/// <summary>
/// Resolved symbol name (if available).
/// </summary>
public string? Symbol { get; init; }
/// <summary>
/// Offset within the symbol.
/// </summary>
public ulong? SymbolOffset { get; init; }
/// <summary>
/// Whether this is a kernel-space frame.
/// </summary>
public bool IsKernel { get; init; }
/// <summary>
/// Whether the symbol resolution is reliable.
/// </summary>
public bool IsSymbolResolved { get; init; }
/// <summary>
/// Source file path (if debug info available).
/// </summary>
public string? SourceFile { get; init; }
/// <summary>
/// Source line number (if debug info available).
/// </summary>
public int? SourceLine { get; init; }
/// <summary>
/// Returns the frame in canonical format: "buildid=xxx;symbol+offset".
/// </summary>
public string ToCanonicalString()
{
var parts = new List<string>();
if (!string.IsNullOrEmpty(BuildId))
parts.Add($"buildid={BuildId[..Math.Min(16, BuildId.Length)]}");
if (!string.IsNullOrEmpty(Symbol))
{
var symbolPart = Symbol;
if (SymbolOffset.HasValue)
symbolPart += $"+0x{SymbolOffset.Value:x}";
parts.Add(symbolPart);
}
else
{
parts.Add($"0x{Address:x}");
}
return string.Join(";", parts);
}
}
/// <summary>
/// Aggregated stack trace in collapsed/folded format (flamegraph compatible).
/// </summary>
public sealed record CollapsedStack
{
/// <summary>
/// Container identifier with image digest.
/// Format: "container@sha256:abc123"
/// </summary>
public required string ContainerIdentifier { get; init; }
/// <summary>
/// Semi-colon separated stack frames from leaf to root.
/// Format: "buildid=xxx;func_a;buildid=yyy;func_b;main"
/// </summary>
public required string StackString { get; init; }
/// <summary>
/// Number of times this exact stack was observed.
/// </summary>
public required int Count { get; init; }
/// <summary>
/// Build-ID tuples present in this stack.
/// </summary>
public required IReadOnlyList<string> BuildIds { get; init; }
/// <summary>
/// Time window during which these observations occurred.
/// </summary>
public required DateTime FirstSeen { get; init; }
/// <summary>
/// Last observation time.
/// </summary>
public required DateTime LastSeen { get; init; }
/// <summary>
/// Parses a collapsed stack line.
/// Format: "container@digest;buildid=xxx;func;... count"
/// </summary>
public static CollapsedStack? Parse(string line)
{
if (string.IsNullOrWhiteSpace(line))
return null;
var lastSpace = line.LastIndexOf(' ');
if (lastSpace < 0)
return null;
var stackPart = line[..lastSpace];
var countPart = line[(lastSpace + 1)..];
if (!int.TryParse(countPart, out var count))
return null;
var firstSemi = stackPart.IndexOf(';');
if (firstSemi < 0)
return null;
var container = stackPart[..firstSemi];
var frames = stackPart[(firstSemi + 1)..];
// Extract Build-IDs
var buildIds = new List<string>();
foreach (var part in frames.Split(';'))
{
if (part.StartsWith("buildid=", StringComparison.OrdinalIgnoreCase))
{
buildIds.Add(part[8..]);
}
}
var now = DateTime.UtcNow;
return new CollapsedStack
{
ContainerIdentifier = container,
StackString = frames,
Count = count,
BuildIds = buildIds,
FirstSeen = now,
LastSeen = now,
};
}
/// <summary>
/// Converts to flamegraph-compatible format.
/// </summary>
public override string ToString() => $"{ContainerIdentifier};{StackString} {Count}";
}
/// <summary>
/// Result of a stack trace capture session.
/// </summary>
public sealed record StackTraceCaptureSession
{
/// <summary>
/// Session identifier.
/// </summary>
public required string SessionId { get; init; }
/// <summary>
/// When the capture started.
/// </summary>
public required DateTime StartTime { get; init; }
/// <summary>
/// When the capture ended.
/// </summary>
public required DateTime EndTime { get; init; }
/// <summary>
/// Target process ID (if specific process was targeted).
/// </summary>
public int? TargetProcessId { get; init; }
/// <summary>
/// Container ID (if specific container was targeted).
/// </summary>
public string? TargetContainerId { get; init; }
/// <summary>
/// Raw samples collected.
/// </summary>
public required IReadOnlyList<StackTraceSample> Samples { get; init; }
/// <summary>
/// Collapsed/folded stacks for analysis.
/// </summary>
public required IReadOnlyList<CollapsedStack> CollapsedStacks { get; init; }
/// <summary>
/// Total samples attempted.
/// </summary>
public required long TotalSamplesAttempted { get; init; }
/// <summary>
/// Samples dropped due to overflow or errors.
/// </summary>
public required long DroppedSamples { get; init; }
/// <summary>
/// Paths that were redacted for privacy.
/// </summary>
public required int RedactedPaths { get; init; }
/// <summary>
/// Measured CPU overhead percentage.
/// </summary>
public double? MeasuredOverheadPercent { get; init; }
/// <summary>
/// Options used for capture.
/// </summary>
public required StackTraceCaptureOptions Options { get; init; }
}
/// <summary>
/// Interface for stack trace capture adapters.
/// </summary>
public interface IStackTraceCaptureAdapter
{
/// <summary>
/// Adapter identifier.
/// </summary>
string AdapterId { get; }
/// <summary>
/// Human-readable name.
/// </summary>
string DisplayName { get; }
/// <summary>
/// Platform this adapter supports.
/// </summary>
string Platform { get; }
/// <summary>
/// Checks if stack trace capture is available on this system.
/// </summary>
Task<bool> IsAvailableAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Starts stack trace capture.
/// </summary>
Task<string> StartCaptureAsync(
StackTraceCaptureOptions options,
int? targetPid = null,
string? containerId = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Stops capture and returns the session.
/// </summary>
Task<StackTraceCaptureSession> StopCaptureAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Gets current capture statistics.
/// </summary>
(long SampleCount, long DroppedCount, double? OverheadPercent) GetStatistics();
}