old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user