old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions

This commit is contained in:
master
2026-01-15 18:37:59 +02:00
parent c631bacee2
commit 88a85cdd92
208 changed files with 32271 additions and 2287 deletions

View File

@@ -2,6 +2,38 @@ using System.Text.Json.Serialization;
namespace StellaOps.Scanner.Reachability.Witnesses;
/// <summary>
/// Well-known predicate types for path witness attestations.
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-003)
/// </summary>
public static class WitnessPredicateTypes
{
/// <summary>
/// Canonical path witness predicate type URI.
/// </summary>
public const string PathWitnessCanonical = "https://stella.ops/predicates/path-witness/v1";
/// <summary>
/// Alias 1: stella.ops format for backward compatibility.
/// </summary>
public const string PathWitnessAlias1 = "stella.ops/pathWitness@v1";
/// <summary>
/// Alias 2: HTTPS URL format variant.
/// </summary>
public const string PathWitnessAlias2 = "https://stella.ops/pathWitness/v1";
/// <summary>
/// Returns true if the predicate type is a recognized path witness type.
/// </summary>
public static bool IsPathWitnessType(string predicateType)
{
return predicateType == PathWitnessCanonical
|| predicateType == PathWitnessAlias1
|| predicateType == PathWitnessAlias2;
}
}
/// <summary>
/// A DSSE-signable path witness documenting the call path from entrypoint to vulnerable sink.
/// Conforms to stellaops.witness.v1 schema.
@@ -67,6 +99,34 @@ public sealed record PathWitness
/// </summary>
[JsonPropertyName("observed_at")]
public required DateTimeOffset ObservedAt { get; init; }
/// <summary>
/// Canonical path hash computed from node hashes along the path.
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-003)
/// </summary>
[JsonPropertyName("path_hash")]
public string? PathHash { get; init; }
/// <summary>
/// Top-K node hashes along the path (deterministically ordered).
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-003)
/// </summary>
[JsonPropertyName("node_hashes")]
public IReadOnlyList<string>? NodeHashes { get; init; }
/// <summary>
/// Evidence URIs for tracing back to source artifacts.
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-003)
/// </summary>
[JsonPropertyName("evidence_uris")]
public IReadOnlyList<string>? EvidenceUris { get; init; }
/// <summary>
/// Canonical predicate type URI for this witness.
/// Default: https://stella.ops/predicates/path-witness/v1
/// </summary>
[JsonPropertyName("predicate_type")]
public string PredicateType { get; init; } = WitnessPredicateTypes.PathWitnessCanonical;
}
/// <summary>

View File

@@ -62,6 +62,13 @@ public sealed class PathWitnessBuilder : IPathWitnessBuilder
var sinkNode = request.CallGraph.Nodes?.FirstOrDefault(n => n.SymbolId == request.SinkSymbolId);
var sinkSymbol = sinkNode?.Display ?? sinkNode?.Symbol?.Demangled ?? request.SinkSymbolId;
// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-003)
// Compute node hashes and path hash for deterministic joining with runtime evidence
var (nodeHashes, pathHash) = ComputePathHashes(request.ComponentPurl, path);
// Build evidence URIs for traceability
var evidenceUris = BuildEvidenceUris(request);
// Build the witness
var witness = new PathWitness
{
@@ -98,7 +105,12 @@ public sealed class PathWitnessBuilder : IPathWitnessBuilder
AnalysisConfigDigest = request.AnalysisConfigDigest,
BuildId = request.BuildId
},
ObservedAt = _timeProvider.GetUtcNow()
ObservedAt = _timeProvider.GetUtcNow(),
// PW-SCN-003: Add node hashes and path hash
NodeHashes = nodeHashes,
PathHash = pathHash,
EvidenceUris = evidenceUris,
PredicateType = WitnessPredicateTypes.PathWitnessCanonical
};
// Compute witness ID from canonical content
@@ -480,4 +492,108 @@ public sealed class PathWitnessBuilder : IPathWitnessBuilder
return $"{WitnessSchema.WitnessIdPrefix}{hash}";
}
/// <summary>
/// Computes node hashes and combined path hash for the witness path.
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-003)
/// </summary>
/// <param name="componentPurl">Component PURL for hash computation.</param>
/// <param name="path">Path steps from entrypoint to sink.</param>
/// <returns>Tuple of (top-K node hashes, combined path hash).</returns>
private static (IReadOnlyList<string> nodeHashes, string pathHash) ComputePathHashes(
string componentPurl,
IReadOnlyList<PathStep> path)
{
const int TopK = 10; // Return top-K node hashes
// Compute node hash for each step in the path
var allNodeHashes = new List<string>();
foreach (var step in path)
{
// Use SymbolId as the FQN for hash computation
var nodeHash = ComputeNodeHash(componentPurl, step.SymbolId);
allNodeHashes.Add(nodeHash);
}
// Deduplicate and sort for deterministic ordering
var uniqueHashes = allNodeHashes
.Distinct(StringComparer.Ordinal)
.Order(StringComparer.Ordinal)
.ToList();
// Select top-K hashes
var topKHashes = uniqueHashes.Take(TopK).ToList();
// Compute combined path hash from all node hashes
var pathHash = ComputeCombinedPathHash(allNodeHashes);
return (topKHashes, pathHash);
}
/// <summary>
/// Computes a canonical node hash from PURL and symbol FQN.
/// Uses SHA-256 for compatibility with NodeHashRecipe in StellaOps.Reachability.Core.
/// </summary>
private static string ComputeNodeHash(string purl, string symbolFqn)
{
// Normalize inputs
var normalizedPurl = purl?.Trim().ToLowerInvariant() ?? string.Empty;
var normalizedSymbol = symbolFqn?.Trim() ?? string.Empty;
var input = $"{normalizedPurl}:{normalizedSymbol}";
var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return "sha256:" + Convert.ToHexStringLower(hashBytes);
}
/// <summary>
/// Computes a combined path hash from ordered node hashes.
/// </summary>
private static string ComputeCombinedPathHash(IReadOnlyList<string> nodeHashes)
{
// Extract hex parts and concatenate in order
var hexParts = nodeHashes
.Select(h => h.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase) ? h[7..] : h)
.ToList();
var combined = string.Join(":", hexParts);
var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(combined));
return "path:sha256:" + Convert.ToHexStringLower(hashBytes);
}
/// <summary>
/// Builds evidence URIs for traceability.
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-003)
/// </summary>
private static IReadOnlyList<string> BuildEvidenceUris(PathWitnessRequest request)
{
var uris = new List<string>();
// Add callgraph evidence URI
if (!string.IsNullOrWhiteSpace(request.CallgraphDigest))
{
uris.Add($"evidence:callgraph:{request.CallgraphDigest}");
}
// Add SBOM evidence URI
if (!string.IsNullOrWhiteSpace(request.SbomDigest))
{
uris.Add($"evidence:sbom:{request.SbomDigest}");
}
// Add surface evidence URI
if (!string.IsNullOrWhiteSpace(request.SurfaceDigest))
{
uris.Add($"evidence:surface:{request.SurfaceDigest}");
}
// Add build evidence URI
if (!string.IsNullOrWhiteSpace(request.BuildId))
{
uris.Add($"evidence:build:{request.BuildId}");
}
return uris;
}
}

View File

@@ -460,4 +460,114 @@ public class SarifExportServiceTests
result.Properties.Should().ContainKey("github/alertCategory");
result.Properties!["github/alertCategory"].Should().Be("security");
}
/// <summary>
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-004)
/// </summary>
[Fact]
public async Task ExportAsync_WithNodeHash_IncludesHashMetadata()
{
// Arrange
var findings = new[]
{
new FindingInput
{
Type = FindingType.Vulnerability,
Title = "Test Vulnerability",
VulnerabilityId = "CVE-2026-1234",
Severity = Severity.High,
NodeHash = "sha256:abc123def456",
PathHash = "path:sha256:789xyz",
PathNodeHashes = new[] { "sha256:node1", "sha256:node2", "sha256:node3" },
Reachability = ReachabilityStatus.StaticReachable
}
};
var options = new SarifExportOptions
{
ToolVersion = "1.0.0",
IncludeReachability = true
};
// Act
var log = await _service.ExportAsync(findings, options, TestContext.Current.CancellationToken);
// Assert
var result = log.Runs[0].Results[0];
result.Properties.Should().ContainKey("stellaops/node/hash");
result.Properties!["stellaops/node/hash"].Should().Be("sha256:abc123def456");
result.Properties.Should().ContainKey("stellaops/path/hash");
result.Properties!["stellaops/path/hash"].Should().Be("path:sha256:789xyz");
result.Properties.Should().ContainKey("stellaops/path/nodeHashes");
}
/// <summary>
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-004)
/// </summary>
[Fact]
public async Task ExportAsync_WithFunctionSignature_IncludesFunctionMetadata()
{
// Arrange
var findings = new[]
{
new FindingInput
{
Type = FindingType.Vulnerability,
Title = "Test Vulnerability",
VulnerabilityId = "CVE-2026-5678",
Severity = Severity.Critical,
FunctionSignature = "void ProcessInput(string data)",
FunctionName = "ProcessInput",
FunctionNamespace = "MyApp.Controllers.UserController"
}
};
var options = new SarifExportOptions { ToolVersion = "1.0.0" };
// Act
var log = await _service.ExportAsync(findings, options, TestContext.Current.CancellationToken);
// Assert
var result = log.Runs[0].Results[0];
result.Properties.Should().ContainKey("stellaops/function/signature");
result.Properties!["stellaops/function/signature"].Should().Be("void ProcessInput(string data)");
result.Properties.Should().ContainKey("stellaops/function/name");
result.Properties!["stellaops/function/name"].Should().Be("ProcessInput");
result.Properties.Should().ContainKey("stellaops/function/namespace");
result.Properties!["stellaops/function/namespace"].Should().Be("MyApp.Controllers.UserController");
}
/// <summary>
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-004)
/// </summary>
[Fact]
public async Task ExportAsync_NodeHashWithoutReachabilityFlag_ExcludesHashes()
{
// Arrange
var findings = new[]
{
new FindingInput
{
Type = FindingType.Vulnerability,
Title = "Test Vulnerability",
Severity = Severity.Medium,
NodeHash = "sha256:abc123def456",
PathHash = "path:sha256:789xyz"
}
};
var options = new SarifExportOptions
{
ToolVersion = "1.0.0",
IncludeReachability = false // Hashes should only appear with reachability enabled
};
// Act
var log = await _service.ExportAsync(findings, options, TestContext.Current.CancellationToken);
// Assert
var result = log.Runs[0].Results[0];
result.Properties.Should().NotContainKey("stellaops/node/hash");
result.Properties.Should().NotContainKey("stellaops/path/hash");
}
}

View File

@@ -139,6 +139,42 @@ public sealed record FindingInput
/// Gets custom properties to include.
/// </summary>
public IReadOnlyDictionary<string, object>? Properties { get; init; }
/// <summary>
/// Gets the canonical node hash for the finding location.
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-004)
/// </summary>
public string? NodeHash { get; init; }
/// <summary>
/// Gets the combined path hash if this finding has a reachability path.
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-004)
/// </summary>
public string? PathHash { get; init; }
/// <summary>
/// Gets the top-K node hashes along the reachability path.
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-004)
/// </summary>
public IReadOnlyList<string>? PathNodeHashes { get; init; }
/// <summary>
/// Gets the function signature at the finding location.
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-004)
/// </summary>
public string? FunctionSignature { get; init; }
/// <summary>
/// Gets the fully qualified function name.
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-004)
/// </summary>
public string? FunctionName { get; init; }
/// <summary>
/// Gets the namespace or module of the function.
/// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-004)
/// </summary>
public string? FunctionNamespace { get; init; }
}
/// <summary>

View File

@@ -315,6 +315,43 @@ public sealed class SarifExportService : ISarifExportService
props["stellaops/attestation"] = finding.AttestationDigests;
}
// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-004)
// Node hash and path hash for reachability evidence joining
if (options.IncludeReachability)
{
if (!string.IsNullOrEmpty(finding.NodeHash))
{
props["stellaops/node/hash"] = finding.NodeHash;
}
if (!string.IsNullOrEmpty(finding.PathHash))
{
props["stellaops/path/hash"] = finding.PathHash;
}
if (finding.PathNodeHashes?.Count > 0)
{
props["stellaops/path/nodeHashes"] = finding.PathNodeHashes;
}
}
// Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash (PW-SCN-004)
// Function signature metadata
if (!string.IsNullOrEmpty(finding.FunctionSignature))
{
props["stellaops/function/signature"] = finding.FunctionSignature;
}
if (!string.IsNullOrEmpty(finding.FunctionName))
{
props["stellaops/function/name"] = finding.FunctionName;
}
if (!string.IsNullOrEmpty(finding.FunctionNamespace))
{
props["stellaops/function/namespace"] = finding.FunctionNamespace;
}
// Category
if (!string.IsNullOrEmpty(options.Category))
{