Files
git.stella-ops.org/src/Signals/StellaOps.Signals/Parsing/SimpleJsonCallgraphParser.cs
master 536f6249a6
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Add SBOM, symbols, traces, and VEX files for CVE-2022-21661 SQLi case
- 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.
2025-11-08 20:53:45 +02:00

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