consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -10,6 +10,8 @@
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Explainability.Assumptions;
using StellaOps.Scanner.Reachability.Witnesses;
using System.Security.Cryptography;
using System.Text;
using System.Collections.Immutable;
namespace StellaOps.Scanner.Reachability.Stack;
@@ -45,7 +47,7 @@ public sealed class ReachabilityResultFactory : IReachabilityResultFactory
ReachabilityVerdict.Unreachable => await CreateNotAffectedResultAsync(stack, context, cancellationToken).ConfigureAwait(false),
ReachabilityVerdict.Exploitable or
ReachabilityVerdict.LikelyExploitable or
ReachabilityVerdict.PossiblyExploitable => CreateAffectedPlaceholderResult(stack),
ReachabilityVerdict.PossiblyExploitable => CreateAffectedResultFromStack(stack, context),
ReachabilityVerdict.Unknown => CreateUnknownResult(stack.Explanation ?? "Reachability could not be determined"),
_ => CreateUnknownResult($"Unexpected verdict: {stack.Verdict}")
};
@@ -188,20 +190,119 @@ public sealed class ReachabilityResultFactory : IReachabilityResultFactory
return await _suppressionBuilder.BuildUnreachableAsync(fallbackRequest, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Creates a placeholder Affected result when PathWitness is not yet available.
/// The caller should use CreateAffectedResult(PathWitness) when they have built the witness.
/// </summary>
private Witnesses.ReachabilityResult CreateAffectedPlaceholderResult(ReachabilityStack stack)
private Witnesses.ReachabilityResult CreateAffectedResultFromStack(
ReachabilityStack stack,
WitnessGenerationContext context)
{
_logger.LogDebug(
"Verdict is {Verdict} for finding {FindingId} - PathWitness should be built separately",
stack.Verdict,
stack.FindingId);
var selectedPath = stack.StaticCallGraph.Paths
.OrderBy(path => path.Sites.Length)
.ThenByDescending(path => path.Confidence)
.FirstOrDefault();
// Return Unknown with metadata indicating affected; caller should build PathWitness
// and call CreateAffectedResult(pathWitness) to get proper result
return Witnesses.ReachabilityResult.Unknown();
var entrypoint = selectedPath?.Entrypoint ?? stack.StaticCallGraph.ReachingEntrypoints.FirstOrDefault();
if (entrypoint is null)
{
_logger.LogWarning(
"Affected verdict for finding {FindingId} has no entrypoint witness data. Returning Unknown.",
stack.FindingId);
return Witnesses.ReachabilityResult.Unknown();
}
var pathSteps = new List<PathStep>();
if (selectedPath is not null)
{
pathSteps.AddRange(selectedPath.Sites.Select(site => new PathStep
{
Symbol = site.MethodName,
SymbolId = BuildSymbolId(site.MethodName, site.ClassName),
File = site.FileName,
Line = site.LineNumber
}));
}
if (pathSteps.Count == 0)
{
pathSteps.Add(new PathStep
{
Symbol = stack.Symbol.Name,
SymbolId = BuildSymbolId(stack.Symbol.Name, stack.Symbol.Library),
File = null,
Line = null
});
}
var gates = stack.RuntimeGating.Conditions
.Where(c => c.IsBlocking)
.Select(c => new DetectedGate
{
Type = MapGateType(c.Type.ToString()),
GuardSymbol = c.ConfigKey ?? c.EnvVar ?? c.Description,
Confidence = MapConditionConfidence(c),
Detail = c.Description
})
.OrderBy(g => g.Type, StringComparer.Ordinal)
.ThenBy(g => g.GuardSymbol, StringComparer.Ordinal)
.ToArray();
var nodeHashes = pathSteps
.Select(step => ComputePathNodeHash(context.ComponentPurl, step.SymbolId))
.Distinct(StringComparer.Ordinal)
.OrderBy(hash => hash, StringComparer.Ordinal)
.ToArray();
var pathHash = ComputePathHash(nodeHashes);
var witness = new PathWitness
{
WitnessId = string.Empty,
Artifact = new WitnessArtifact
{
SbomDigest = context.SbomDigest,
ComponentPurl = context.ComponentPurl
},
Vuln = new WitnessVuln
{
Id = context.VulnId,
Source = context.VulnSource,
AffectedRange = context.AffectedRange
},
Entrypoint = new WitnessEntrypoint
{
Kind = entrypoint.Type.ToString().ToLowerInvariant(),
Name = entrypoint.Name,
SymbolId = BuildSymbolId(entrypoint.Name, entrypoint.Location)
},
Path = pathSteps,
Sink = new WitnessSink
{
Symbol = stack.Symbol.Name,
SymbolId = BuildSymbolId(stack.Symbol.Name, stack.Symbol.Library),
SinkType = stack.Symbol.Type.ToString().ToLowerInvariant()
},
Gates = gates.Length == 0 ? null : gates,
Evidence = new WitnessEvidence
{
CallgraphDigest = context.GraphDigest ?? "unknown",
AnalysisConfigDigest = "reachability-stack-v1",
BuildId = context.ImageDigest
},
ObservedAt = stack.AnalyzedAt,
NodeHashes = nodeHashes,
PathHash = pathHash,
EvidenceUris = new[]
{
$"evidence:sbom:{context.SbomDigest}",
$"evidence:graph:{context.GraphDigest ?? "unknown"}"
},
ObservationType = ObservationType.Static
};
witness = witness with
{
WitnessId = $"wit:sha256:{ComputeWitnessIdHash(witness)}",
ClaimId = ClaimIdGenerator.Generate(witness.Artifact, witness.PathHash ?? string.Empty)
};
return Witnesses.ReachabilityResult.Affected(witness);
}
private static double MapConfidence(ConfidenceLevel level) => level switch
@@ -243,4 +344,39 @@ public sealed class ReachabilityResultFactory : IReachabilityResultFactory
var blockingCount = layer3.Conditions.Count(c => c.IsBlocking);
return (int)(100.0 * blockingCount / layer3.Conditions.Length);
}
private static string BuildSymbolId(string symbol, string? scope)
{
var input = $"{scope ?? "global"}::{symbol}";
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return $"sym:{Convert.ToHexStringLower(bytes)[..16]}";
}
private static string ComputePathNodeHash(string purl, string symbolId)
{
var input = $"{purl.Trim().ToLowerInvariant()}:{symbolId.Trim()}";
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return $"sha256:{Convert.ToHexStringLower(bytes)}";
}
private static string ComputePathHash(IReadOnlyList<string> nodeHashes)
{
var input = string.Join(":", nodeHashes.Select(v => v.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase) ? v[7..] : v));
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return $"path:sha256:{Convert.ToHexStringLower(bytes)}";
}
private static string ComputeWitnessIdHash(PathWitness witness)
{
var input = string.Join(
"|",
witness.Artifact.SbomDigest,
witness.Artifact.ComponentPurl,
witness.Vuln.Id,
witness.Entrypoint.SymbolId,
witness.Sink.SymbolId,
witness.PathHash ?? string.Empty);
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexStringLower(bytes);
}
}