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.
187 lines
7.6 KiB
C#
187 lines
7.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using StellaOps.Signals.Models;
|
|
|
|
namespace StellaOps.Signals.Parsing;
|
|
|
|
/// <summary>
|
|
/// Simple JSON-based callgraph parser used for initial language coverage.
|
|
/// </summary>
|
|
public sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
|
{
|
|
private readonly JsonSerializerOptions serializerOptions;
|
|
|
|
public SimpleJsonCallgraphParser(string language)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(language);
|
|
Language = language;
|
|
serializerOptions = new JsonSerializerOptions
|
|
{
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
}
|
|
|
|
public string Language { get; }
|
|
|
|
public async Task<CallgraphParseResult> ParseAsync(Stream artifactStream, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(artifactStream);
|
|
|
|
using var document = await JsonDocument.ParseAsync(artifactStream, cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
var root = document.RootElement;
|
|
|
|
if (TryParseLegacy(root, out var legacyResult))
|
|
{
|
|
return legacyResult;
|
|
}
|
|
|
|
if (TryParseSchemaV1(root, out var schemaResult))
|
|
{
|
|
return schemaResult;
|
|
}
|
|
|
|
throw new CallgraphParserValidationException("Callgraph artifact payload is empty or missing required fields.");
|
|
}
|
|
|
|
private static bool TryParseLegacy(JsonElement root, out CallgraphParseResult result)
|
|
{
|
|
result = default!;
|
|
|
|
if (!root.TryGetProperty("graph", out var graphElement))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var nodesElement = graphElement.GetProperty("nodes");
|
|
var edgesElement = graphElement.TryGetProperty("edges", out var edgesValue) ? edgesValue : default;
|
|
|
|
var nodes = new List<CallgraphNode>(nodesElement.GetArrayLength());
|
|
foreach (var nodeElement in nodesElement.EnumerateArray())
|
|
{
|
|
var id = nodeElement.GetProperty("id").GetString();
|
|
if (string.IsNullOrWhiteSpace(id))
|
|
{
|
|
throw new CallgraphParserValidationException("Callgraph node is missing an id.");
|
|
}
|
|
|
|
nodes.Add(new CallgraphNode(
|
|
Id: id.Trim(),
|
|
Name: nodeElement.TryGetProperty("name", out var nameEl) ? nameEl.GetString() ?? id.Trim() : id.Trim(),
|
|
Kind: nodeElement.TryGetProperty("kind", out var kindEl) ? kindEl.GetString() ?? "function" : "function",
|
|
Namespace: nodeElement.TryGetProperty("namespace", out var nsEl) ? nsEl.GetString() : null,
|
|
File: nodeElement.TryGetProperty("file", out var fileEl) ? fileEl.GetString() : null,
|
|
Line: nodeElement.TryGetProperty("line", out var lineEl) && lineEl.ValueKind == JsonValueKind.Number ? lineEl.GetInt32() : null));
|
|
}
|
|
|
|
var edges = new List<CallgraphEdge>();
|
|
if (edgesElement.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var edgeElement in edgesElement.EnumerateArray())
|
|
{
|
|
var source = edgeElement.GetProperty("source").GetString();
|
|
var target = edgeElement.GetProperty("target").GetString();
|
|
if (string.IsNullOrWhiteSpace(source) || string.IsNullOrWhiteSpace(target))
|
|
{
|
|
throw new CallgraphParserValidationException("Callgraph edge requires both source and target.");
|
|
}
|
|
|
|
var type = edgeElement.TryGetProperty("type", out var typeEl) ? typeEl.GetString() ?? "call" : "call";
|
|
edges.Add(new CallgraphEdge(source.Trim(), target.Trim(), type));
|
|
}
|
|
}
|
|
|
|
var formatVersion = root.TryGetProperty("formatVersion", out var versionEl)
|
|
? versionEl.GetString()
|
|
: null;
|
|
|
|
result = new CallgraphParseResult(nodes, edges, string.IsNullOrWhiteSpace(formatVersion) ? "1.0" : formatVersion!.Trim());
|
|
return true;
|
|
}
|
|
|
|
private static bool TryParseSchemaV1(JsonElement root, out CallgraphParseResult result)
|
|
{
|
|
result = default!;
|
|
|
|
if (!root.TryGetProperty("nodes", out var nodesElement) && !root.TryGetProperty("edges", out _))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var nodes = new List<CallgraphNode>();
|
|
if (nodesElement.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var nodeElement in nodesElement.EnumerateArray())
|
|
{
|
|
var id = nodeElement.TryGetProperty("sid", out var sidEl) ? sidEl.GetString() : nodeElement.GetProperty("id").GetString();
|
|
if (string.IsNullOrWhiteSpace(id))
|
|
{
|
|
throw new CallgraphParserValidationException("Callgraph node is missing an id.");
|
|
}
|
|
|
|
nodes.Add(new CallgraphNode(
|
|
Id: id.Trim(),
|
|
Name: nodeElement.TryGetProperty("name", out var nameEl) ? nameEl.GetString() ?? id.Trim() : id.Trim(),
|
|
Kind: nodeElement.TryGetProperty("kind", out var kindEl) ? kindEl.GetString() ?? "function" : "function",
|
|
Namespace: nodeElement.TryGetProperty("namespace", out var nsEl) ? nsEl.GetString() : null,
|
|
File: nodeElement.TryGetProperty("file", out var fileEl) ? fileEl.GetString() : null,
|
|
Line: nodeElement.TryGetProperty("line", out var lineEl) && lineEl.ValueKind == JsonValueKind.Number ? lineEl.GetInt32() : null));
|
|
}
|
|
}
|
|
|
|
if (!root.TryGetProperty("edges", out var edgesElement) || edgesElement.ValueKind != JsonValueKind.Array)
|
|
{
|
|
edgesElement = default;
|
|
}
|
|
|
|
var edges = new List<CallgraphEdge>();
|
|
if (edgesElement.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var edgeElement in edgesElement.EnumerateArray())
|
|
{
|
|
var from = edgeElement.TryGetProperty("from", out var fromEl) ? fromEl.GetString() : edgeElement.GetProperty("source").GetString();
|
|
var to = edgeElement.TryGetProperty("to", out var toEl) ? toEl.GetString() : edgeElement.GetProperty("target").GetString();
|
|
if (string.IsNullOrWhiteSpace(from) || string.IsNullOrWhiteSpace(to))
|
|
{
|
|
throw new CallgraphParserValidationException("Callgraph edge requires both source and target.");
|
|
}
|
|
|
|
var kind = edgeElement.TryGetProperty("kind", out var kindEl)
|
|
? kindEl.GetString() ?? "call"
|
|
: edgeElement.TryGetProperty("type", out var typeEl)
|
|
? typeEl.GetString() ?? "call"
|
|
: "call";
|
|
|
|
edges.Add(new CallgraphEdge(from.Trim(), to.Trim(), kind));
|
|
}
|
|
}
|
|
|
|
if (nodes.Count == 0)
|
|
{
|
|
// When nodes are omitted (framework overlay), derive them from the referenced edges.
|
|
var uniqueNodeIds = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (var edge in edges)
|
|
{
|
|
uniqueNodeIds.Add(edge.SourceId);
|
|
uniqueNodeIds.Add(edge.TargetId);
|
|
}
|
|
|
|
foreach (var nodeId in uniqueNodeIds)
|
|
{
|
|
nodes.Add(new CallgraphNode(nodeId, nodeId, "function", null, null, null));
|
|
}
|
|
}
|
|
|
|
var schemaVersion = root.TryGetProperty("schema_version", out var schemaEl)
|
|
? schemaEl.GetString()
|
|
: "1.0";
|
|
|
|
result = new CallgraphParseResult(nodes, edges, string.IsNullOrWhiteSpace(schemaVersion) ? "1.0" : schemaVersion!.Trim());
|
|
return true;
|
|
}
|
|
|
|
}
|