consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user