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

- 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:
master
2025-11-08 20:53:45 +02:00
parent 515975edc5
commit 536f6249a6
837 changed files with 37279 additions and 14675 deletions

View File

@@ -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);
}

View File

@@ -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.")
{
}
}