Add SBOM, symbols, traces, and VEX files for CVE-2022-21661 SQLi case
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Created CycloneDX and SPDX SBOM files for both reachable and unreachable images. - Added symbols.json detailing function entry and sink points in the WordPress code. - Included runtime traces for function calls in both reachable and unreachable scenarios. - Developed OpenVEX files indicating vulnerability status and justification for both cases. - Updated README for evaluator harness to guide integration with scanner output.
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Signals.Models;
|
||||
|
||||
namespace StellaOps.Signals.Services;
|
||||
|
||||
public interface IReachabilityScoringService
|
||||
{
|
||||
Task<ReachabilityFactDocument> RecomputeAsync(ReachabilityRecomputeRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence;
|
||||
|
||||
namespace StellaOps.Signals.Services;
|
||||
|
||||
public sealed class ReachabilityScoringService : IReachabilityScoringService
|
||||
{
|
||||
private const double ReachableConfidence = 0.75;
|
||||
private const double UnreachableConfidence = 0.25;
|
||||
private const double RuntimeBonus = 0.15;
|
||||
private const double MaxConfidence = 0.99;
|
||||
private const double MinConfidence = 0.05;
|
||||
|
||||
private readonly ICallgraphRepository callgraphRepository;
|
||||
private readonly IReachabilityFactRepository factRepository;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly ILogger<ReachabilityScoringService> logger;
|
||||
|
||||
public ReachabilityScoringService(
|
||||
ICallgraphRepository callgraphRepository,
|
||||
IReachabilityFactRepository factRepository,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<ReachabilityScoringService> logger)
|
||||
{
|
||||
this.callgraphRepository = callgraphRepository ?? throw new ArgumentNullException(nameof(callgraphRepository));
|
||||
this.factRepository = factRepository ?? throw new ArgumentNullException(nameof(factRepository));
|
||||
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<ReachabilityFactDocument> RecomputeAsync(ReachabilityRecomputeRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
ValidateRequest(request);
|
||||
|
||||
var callgraph = await callgraphRepository.GetByIdAsync(request.CallgraphId, cancellationToken).ConfigureAwait(false);
|
||||
if (callgraph is null)
|
||||
{
|
||||
throw new ReachabilityCallgraphNotFoundException(request.CallgraphId);
|
||||
}
|
||||
|
||||
IEnumerable<ReachabilityBlockedEdge> blockedEdges = request.BlockedEdges is { Count: > 0 } list
|
||||
? list
|
||||
: Array.Empty<ReachabilityBlockedEdge>();
|
||||
var graph = BuildGraph(callgraph, blockedEdges);
|
||||
var entryPoints = NormalizeEntryPoints(request.EntryPoints, graph.Nodes, graph.Inbound);
|
||||
var targets = request.Targets.Where(t => !string.IsNullOrWhiteSpace(t)).Select(t => t.Trim()).Distinct(StringComparer.Ordinal).ToList();
|
||||
if (targets.Count == 0)
|
||||
{
|
||||
throw new ReachabilityScoringValidationException("At least one target symbol is required.");
|
||||
}
|
||||
|
||||
var runtimeHits = request.RuntimeHits?.Where(hit => !string.IsNullOrWhiteSpace(hit))
|
||||
.Select(hit => hit.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList() ?? new List<string>();
|
||||
|
||||
var states = new List<ReachabilityStateDocument>(targets.Count);
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var path = FindPath(entryPoints, target, graph.Adjacency);
|
||||
var reachable = path is not null;
|
||||
var confidence = reachable ? ReachableConfidence : UnreachableConfidence;
|
||||
|
||||
var runtimeEvidence = runtimeHits.Where(hit => path?.Contains(hit, StringComparer.Ordinal) == true)
|
||||
.ToList();
|
||||
if (runtimeEvidence.Count > 0)
|
||||
{
|
||||
confidence = Math.Min(MaxConfidence, confidence + RuntimeBonus);
|
||||
}
|
||||
|
||||
confidence = Math.Clamp(confidence, MinConfidence, MaxConfidence);
|
||||
|
||||
states.Add(new ReachabilityStateDocument
|
||||
{
|
||||
Target = target,
|
||||
Reachable = reachable,
|
||||
Confidence = confidence,
|
||||
Path = path ?? new List<string>(),
|
||||
Evidence = new ReachabilityEvidenceDocument
|
||||
{
|
||||
RuntimeHits = runtimeEvidence,
|
||||
BlockedEdges = request.BlockedEdges?.Select(edge => $"{edge.From} -> {edge.To}").ToList()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var document = new ReachabilityFactDocument
|
||||
{
|
||||
CallgraphId = request.CallgraphId,
|
||||
Subject = request.Subject,
|
||||
EntryPoints = entryPoints,
|
||||
States = states,
|
||||
Metadata = request.Metadata,
|
||||
ComputedAt = timeProvider.GetUtcNow(),
|
||||
SubjectKey = request.Subject.ToSubjectKey()
|
||||
};
|
||||
|
||||
logger.LogInformation("Computed reachability fact for subject {SubjectKey} with {StateCount} targets.", document.SubjectKey, states.Count);
|
||||
return await factRepository.UpsertAsync(document, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static void ValidateRequest(ReachabilityRecomputeRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.CallgraphId))
|
||||
{
|
||||
throw new ReachabilityScoringValidationException("Callgraph id is required.");
|
||||
}
|
||||
|
||||
if (request.Subject is null)
|
||||
{
|
||||
throw new ReachabilityScoringValidationException("Subject is required.");
|
||||
}
|
||||
}
|
||||
|
||||
private static ReachabilityGraph BuildGraph(CallgraphDocument document, IEnumerable<ReachabilityBlockedEdge> blockedEdges)
|
||||
{
|
||||
var adjacency = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
|
||||
var inbound = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
|
||||
var blocked = new HashSet<(string From, string To)>(new ReachabilityBlockedEdgeComparer());
|
||||
foreach (var blockedEdge in blockedEdges)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(blockedEdge.From) && !string.IsNullOrWhiteSpace(blockedEdge.To))
|
||||
{
|
||||
blocked.Add((blockedEdge.From.Trim(), blockedEdge.To.Trim()));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var edge in document.Edges)
|
||||
{
|
||||
if (blocked.Contains((edge.SourceId, edge.TargetId)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!adjacency.TryGetValue(edge.SourceId, out var targets))
|
||||
{
|
||||
targets = new HashSet<string>(StringComparer.Ordinal);
|
||||
adjacency[edge.SourceId] = targets;
|
||||
}
|
||||
|
||||
if (targets.Add(edge.TargetId) && !inbound.TryGetValue(edge.TargetId, out var sources))
|
||||
{
|
||||
sources = new HashSet<string>(StringComparer.Ordinal);
|
||||
inbound[edge.TargetId] = sources;
|
||||
}
|
||||
|
||||
inbound[edge.TargetId].Add(edge.SourceId);
|
||||
}
|
||||
|
||||
var nodes = new HashSet<string>(document.Nodes?.Select(n => n.Id) ?? Array.Empty<string>(), StringComparer.Ordinal);
|
||||
foreach (var pair in adjacency)
|
||||
{
|
||||
nodes.Add(pair.Key);
|
||||
foreach (var neighbor in pair.Value)
|
||||
{
|
||||
nodes.Add(neighbor);
|
||||
}
|
||||
}
|
||||
|
||||
return new ReachabilityGraph(nodes, adjacency, inbound);
|
||||
}
|
||||
|
||||
private static List<string> NormalizeEntryPoints(IEnumerable<string> requestedEntries, HashSet<string> nodes, Dictionary<string, HashSet<string>> inbound)
|
||||
{
|
||||
var entries = requestedEntries?
|
||||
.Where(entry => !string.IsNullOrWhiteSpace(entry))
|
||||
.Select(entry => entry.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Where(nodes.Contains)
|
||||
.ToList() ?? new List<string>();
|
||||
|
||||
if (entries.Count > 0)
|
||||
{
|
||||
return entries;
|
||||
}
|
||||
|
||||
var inferred = nodes.Where(node => !inbound.ContainsKey(node)).ToList();
|
||||
if (inferred.Count == 0)
|
||||
{
|
||||
inferred.AddRange(nodes);
|
||||
}
|
||||
|
||||
return inferred;
|
||||
}
|
||||
|
||||
private static List<string>? FindPath(IEnumerable<string> entryPoints, string target, Dictionary<string, HashSet<string>> adjacency)
|
||||
{
|
||||
var queue = new Queue<string>();
|
||||
var parents = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var entry in entryPoints)
|
||||
{
|
||||
if (visited.Add(entry))
|
||||
{
|
||||
queue.Enqueue(entry);
|
||||
}
|
||||
}
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
if (string.Equals(current, target, StringComparison.Ordinal))
|
||||
{
|
||||
return BuildPath(current, parents);
|
||||
}
|
||||
|
||||
if (!adjacency.TryGetValue(current, out var neighbors))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var neighbor in neighbors)
|
||||
{
|
||||
if (visited.Add(neighbor))
|
||||
{
|
||||
parents[neighbor] = current;
|
||||
queue.Enqueue(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<string> BuildPath(string target, Dictionary<string, string> parents)
|
||||
{
|
||||
var path = new List<string>();
|
||||
var current = target;
|
||||
path.Add(current);
|
||||
|
||||
while (parents.TryGetValue(current, out var prev))
|
||||
{
|
||||
path.Add(prev);
|
||||
current = prev;
|
||||
}
|
||||
|
||||
path.Reverse();
|
||||
return path;
|
||||
}
|
||||
|
||||
private sealed record ReachabilityGraph(
|
||||
HashSet<string> Nodes,
|
||||
Dictionary<string, HashSet<string>> Adjacency,
|
||||
Dictionary<string, HashSet<string>> Inbound);
|
||||
|
||||
private sealed class ReachabilityBlockedEdgeComparer : IEqualityComparer<(string From, string To)>
|
||||
{
|
||||
public bool Equals((string From, string To) x, (string From, string To) y)
|
||||
=> string.Equals(x.From, y.From, StringComparison.Ordinal)
|
||||
&& string.Equals(x.To, y.To, StringComparison.Ordinal);
|
||||
|
||||
public int GetHashCode((string From, string To) obj)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (StringComparer.Ordinal.GetHashCode(obj.From) * 397)
|
||||
^ StringComparer.Ordinal.GetHashCode(obj.To);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ReachabilityScoringValidationException : Exception
|
||||
{
|
||||
public ReachabilityScoringValidationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ReachabilityCallgraphNotFoundException : Exception
|
||||
{
|
||||
public ReachabilityCallgraphNotFoundException(string callgraphId) : base($"Callgraph '{callgraphId}' was not found.")
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user