Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.CallGraph.Tests/NodeCallGraphExtractorTests.cs
StellaOps Bot 5146204f1b feat: add security sink detection patterns for JavaScript/TypeScript
- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations).
- Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns.
- Added `package-lock.json` for dependency management.
2025-12-22 23:21:21 +02:00

341 lines
9.0 KiB
C#

// -----------------------------------------------------------------------------
// NodeCallGraphExtractorTests.cs
// Sprint: SPRINT_3610_0003_0001_nodejs_callgraph
// Description: Unit tests for Node.js/JavaScript call graph extraction.
// -----------------------------------------------------------------------------
using StellaOps.Scanner.CallGraph.Node;
using Xunit;
namespace StellaOps.Scanner.CallGraph.Tests;
public class NodeCallGraphExtractorTests
{
[Fact]
public void BabelResultParser_ParsesValidJson()
{
// Arrange
var json = """
{
"module": "my-app",
"version": "1.0.0",
"nodes": [
{
"id": "js:my-app/src/index.handleRequest",
"package": "my-app",
"name": "handleRequest",
"visibility": "public"
}
],
"edges": [
{
"from": "js:my-app/src/index.handleRequest",
"to": "js:external/express.Router",
"kind": "direct"
}
],
"entrypoints": [
{
"id": "js:my-app/src/index.handleRequest",
"type": "http_handler",
"route": "/api/users",
"method": "GET"
}
]
}
""";
// Act
var result = BabelResultParser.Parse(json);
// Assert
Assert.Equal("my-app", result.Module);
Assert.Equal("1.0.0", result.Version);
Assert.Single(result.Nodes);
Assert.Single(result.Edges);
Assert.Single(result.Entrypoints);
}
[Fact]
public void BabelResultParser_ParsesNodeWithPosition()
{
// Arrange
var json = """
{
"module": "test",
"nodes": [
{
"id": "js:test/app.main",
"package": "test",
"name": "main",
"position": {
"file": "app.js",
"line": 10,
"column": 5
}
}
],
"edges": [],
"entrypoints": []
}
""";
// Act
var result = BabelResultParser.Parse(json);
// Assert
Assert.Single(result.Nodes);
var node = result.Nodes[0];
Assert.NotNull(node.Position);
Assert.Equal("app.js", node.Position.File);
Assert.Equal(10, node.Position.Line);
Assert.Equal(5, node.Position.Column);
}
[Fact]
public void BabelResultParser_ParsesEdgeWithSite()
{
// Arrange
var json = """
{
"module": "test",
"nodes": [],
"edges": [
{
"from": "js:test/a.foo",
"to": "js:test/b.bar",
"kind": "callback",
"site": {
"file": "a.js",
"line": 25
}
}
],
"entrypoints": []
}
""";
// Act
var result = BabelResultParser.Parse(json);
// Assert
Assert.Single(result.Edges);
var edge = result.Edges[0];
Assert.Equal("callback", edge.Kind);
Assert.NotNull(edge.Site);
Assert.Equal("a.js", edge.Site.File);
Assert.Equal(25, edge.Site.Line);
}
[Fact]
public void BabelResultParser_ThrowsOnEmptyInput()
{
// Arrange & Act & Assert
Assert.Throws<ArgumentException>(() => BabelResultParser.Parse(""));
Assert.Throws<ArgumentNullException>(() => BabelResultParser.Parse(null!));
}
[Fact]
public void BabelResultParser_ParsesNdjson()
{
// Arrange
var ndjson = """
{"type": "progress", "percent": 50}
{"type": "progress", "percent": 100}
{"module": "app", "nodes": [], "edges": [], "entrypoints": []}
""";
// Act
var result = BabelResultParser.ParseNdjson(ndjson);
// Assert
Assert.Equal("app", result.Module);
}
[Fact]
public void JsEntrypointInfo_HasCorrectProperties()
{
// Arrange
var json = """
{
"module": "api",
"nodes": [],
"edges": [],
"entrypoints": [
{
"id": "js:api/routes.getUsers",
"type": "http_handler",
"route": "/users/:id",
"method": "GET"
}
]
}
""";
// Act
var result = BabelResultParser.Parse(json);
// Assert
Assert.Single(result.Entrypoints);
var ep = result.Entrypoints[0];
Assert.Equal("js:api/routes.getUsers", ep.Id);
Assert.Equal("http_handler", ep.Type);
Assert.Equal("/users/:id", ep.Route);
Assert.Equal("GET", ep.Method);
}
[Fact]
public void BabelResultParser_ParsesSinks()
{
// Arrange
var json = """
{
"module": "test",
"nodes": [
{
"id": "js:test/handler.processRequest",
"package": "test",
"name": "processRequest"
}
],
"edges": [],
"entrypoints": [],
"sinks": [
{
"caller": "js:test/handler.processRequest",
"category": "command_injection",
"method": "child_process.exec",
"site": {
"file": "handler.js",
"line": 42,
"column": 8
}
}
]
}
""";
// Act
var result = BabelResultParser.Parse(json);
// Assert
Assert.Single(result.Sinks);
var sink = result.Sinks[0];
Assert.Equal("js:test/handler.processRequest", sink.Caller);
Assert.Equal("command_injection", sink.Category);
Assert.Equal("child_process.exec", sink.Method);
Assert.NotNull(sink.Site);
Assert.Equal("handler.js", sink.Site.File);
Assert.Equal(42, sink.Site.Line);
}
[Fact]
public void BabelResultParser_ParsesMultipleSinkCategories()
{
// Arrange
var json = """
{
"module": "vulnerable-app",
"nodes": [],
"edges": [],
"entrypoints": [],
"sinks": [
{
"caller": "js:vulnerable-app/db.query",
"category": "sql_injection",
"method": "connection.query"
},
{
"caller": "js:vulnerable-app/api.fetch",
"category": "ssrf",
"method": "fetch"
},
{
"caller": "js:vulnerable-app/file.write",
"category": "file_write",
"method": "fs.writeFileSync"
}
]
}
""";
// Act
var result = BabelResultParser.Parse(json);
// Assert
Assert.Equal(3, result.Sinks.Count);
Assert.Contains(result.Sinks, s => s.Category == "sql_injection");
Assert.Contains(result.Sinks, s => s.Category == "ssrf");
Assert.Contains(result.Sinks, s => s.Category == "file_write");
}
[Fact]
public void BabelResultParser_ParsesEmptySinks()
{
// Arrange
var json = """
{
"module": "safe-app",
"nodes": [],
"edges": [],
"entrypoints": [],
"sinks": []
}
""";
// Act
var result = BabelResultParser.Parse(json);
// Assert
Assert.Empty(result.Sinks);
}
[Fact]
public void BabelResultParser_ParsesMissingSinks()
{
// Arrange - sinks field omitted entirely
var json = """
{
"module": "legacy-app",
"nodes": [],
"edges": [],
"entrypoints": []
}
""";
// Act
var result = BabelResultParser.Parse(json);
// Assert - should default to empty list
Assert.Empty(result.Sinks);
}
[Fact]
public void BabelResultParser_ParsesSinkWithoutSite()
{
// Arrange
var json = """
{
"module": "test",
"nodes": [],
"edges": [],
"entrypoints": [],
"sinks": [
{
"caller": "js:test/func",
"category": "deserialization",
"method": "eval"
}
]
}
""";
// Act
var result = BabelResultParser.Parse(json);
// Assert
Assert.Single(result.Sinks);
Assert.Null(result.Sinks[0].Site);
}
}