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,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();
|
||||
}
|
||||
Reference in New Issue
Block a user