feat: add stella-callgraph-node for JavaScript/TypeScript call graph extraction

- Implemented a new tool `stella-callgraph-node` that extracts call graphs from JavaScript/TypeScript projects using Babel AST.
- Added command-line interface with options for JSON output and help.
- Included functionality to analyze project structure, detect functions, and build call graphs.
- Created a package.json file for dependency management.

feat: introduce stella-callgraph-python for Python call graph extraction

- Developed `stella-callgraph-python` to extract call graphs from Python projects using AST analysis.
- Implemented command-line interface with options for JSON output and verbose logging.
- Added framework detection to identify popular web frameworks and their entry points.
- Created an AST analyzer to traverse Python code and extract function definitions and calls.
- Included requirements.txt for project dependencies.

chore: add framework detection for Python projects

- Implemented framework detection logic to identify frameworks like Flask, FastAPI, Django, and others based on project files and import patterns.
- Enhanced the AST analyzer to recognize entry points based on decorators and function definitions.
This commit is contained in:
master
2025-12-19 18:11:59 +02:00
parent 951a38d561
commit 8779e9226f
130 changed files with 19011 additions and 422 deletions

View File

@@ -0,0 +1,410 @@
// -----------------------------------------------------------------------------
// WitnessCommandGroupTests.cs
// Sprint: SPRINT_3700_0005_0001_witness_ui_cli
// Tasks: TEST-002
// Description: Unit tests for witness CLI commands
// -----------------------------------------------------------------------------
using System.CommandLine;
using System.CommandLine.Parsing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
using StellaOps.Cli.Commands;
namespace StellaOps.Cli.Tests.Commands;
/// <summary>
/// Unit tests for witness CLI commands.
/// </summary>
public class WitnessCommandGroupTests
{
private readonly IServiceProvider _services;
private readonly Option<bool> _verboseOption;
private readonly CancellationToken _cancellationToken;
public WitnessCommandGroupTests()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging();
_services = serviceCollection.BuildServiceProvider();
_verboseOption = new Option<bool>("--verbose", "-v");
_cancellationToken = CancellationToken.None;
}
#region Command Structure Tests
[Fact]
public void BuildWitnessCommand_CreatesWitnessCommandTree()
{
// Act
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
// Assert
Assert.Equal("witness", command.Name);
Assert.Equal("Reachability witness operations.", command.Description);
}
[Fact]
public void BuildWitnessCommand_HasShowSubcommand()
{
// Act
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var showCommand = command.Subcommands.FirstOrDefault(c => c.Name == "show");
// Assert
Assert.NotNull(showCommand);
Assert.Equal("Display a witness with call path visualization.", showCommand.Description);
}
[Fact]
public void BuildWitnessCommand_HasVerifySubcommand()
{
// Act
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var verifyCommand = command.Subcommands.FirstOrDefault(c => c.Name == "verify");
// Assert
Assert.NotNull(verifyCommand);
}
[Fact]
public void BuildWitnessCommand_HasListSubcommand()
{
// Act
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var listCommand = command.Subcommands.FirstOrDefault(c => c.Name == "list");
// Assert
Assert.NotNull(listCommand);
}
[Fact]
public void BuildWitnessCommand_HasExportSubcommand()
{
// Act
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var exportCommand = command.Subcommands.FirstOrDefault(c => c.Name == "export");
// Assert
Assert.NotNull(exportCommand);
}
#endregion
#region Show Command Tests
[Fact]
public void ShowCommand_HasWitnessIdArgument()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var showCommand = command.Subcommands.First(c => c.Name == "show");
// Act
var witnessIdArg = showCommand.Arguments.FirstOrDefault(a => a.Name == "witness-id");
// Assert
Assert.NotNull(witnessIdArg);
}
[Fact]
public void ShowCommand_HasFormatOption()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var showCommand = command.Subcommands.First(c => c.Name == "show");
// Act
var formatOption = showCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("-f") || o.Aliases.Contains("--format"));
// Assert
Assert.NotNull(formatOption);
}
[Fact]
public void ShowCommand_HasNoColorOption()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var showCommand = command.Subcommands.First(c => c.Name == "show");
// Act
var noColorOption = showCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--no-color"));
// Assert
Assert.NotNull(noColorOption);
}
[Fact]
public void ShowCommand_HasPathOnlyOption()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var showCommand = command.Subcommands.First(c => c.Name == "show");
// Act
var pathOnlyOption = showCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--path-only"));
// Assert
Assert.NotNull(pathOnlyOption);
}
[Theory]
[InlineData("text")]
[InlineData("json")]
[InlineData("yaml")]
public void ShowCommand_FormatOption_AcceptsValidFormats(string format)
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var showCommand = command.Subcommands.First(c => c.Name == "show");
// Act
var parseResult = showCommand.Parse($"wit:abc123 --format {format}");
// Assert
Assert.Empty(parseResult.Errors);
}
[Fact]
public void ShowCommand_FormatOption_RejectsInvalidFormat()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var showCommand = command.Subcommands.First(c => c.Name == "show");
// Act
var parseResult = showCommand.Parse("wit:abc123 --format invalid");
// Assert
Assert.NotEmpty(parseResult.Errors);
}
#endregion
#region Verify Command Tests
[Fact]
public void VerifyCommand_HasWitnessIdArgument()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var verifyCommand = command.Subcommands.First(c => c.Name == "verify");
// Act
var witnessIdArg = verifyCommand.Arguments.FirstOrDefault(a => a.Name == "witness-id");
// Assert
Assert.NotNull(witnessIdArg);
}
[Fact]
public void VerifyCommand_HasPublicKeyOption()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var verifyCommand = command.Subcommands.First(c => c.Name == "verify");
// Act
var publicKeyOption = verifyCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("-k") || o.Aliases.Contains("--public-key"));
// Assert
Assert.NotNull(publicKeyOption);
}
[Fact]
public void VerifyCommand_HasOfflineOption()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var verifyCommand = command.Subcommands.First(c => c.Name == "verify");
// Act
var offlineOption = verifyCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--offline"));
// Assert
Assert.NotNull(offlineOption);
}
#endregion
#region List Command Tests
[Fact]
public void ListCommand_HasScanOption()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var listCommand = command.Subcommands.First(c => c.Name == "list");
// Act
var scanOption = listCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--scan") || o.Aliases.Contains("-s"));
// Assert
Assert.NotNull(scanOption);
}
[Fact]
public void ListCommand_HasVulnOption()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var listCommand = command.Subcommands.First(c => c.Name == "list");
// Act
var vulnOption = listCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--vuln") || o.Aliases.Contains("-v"));
// Assert
Assert.NotNull(vulnOption);
}
[Fact]
public void ListCommand_HasReachableOnlyOption()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var listCommand = command.Subcommands.First(c => c.Name == "list");
// Act
var reachableOption = listCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--reachable-only"));
// Assert
Assert.NotNull(reachableOption);
}
#endregion
#region Export Command Tests
[Fact]
public void ExportCommand_HasWitnessIdArgument()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var exportCommand = command.Subcommands.First(c => c.Name == "export");
// Act
var witnessIdArg = exportCommand.Arguments.FirstOrDefault(a => a.Name == "witness-id");
// Assert
Assert.NotNull(witnessIdArg);
}
[Fact]
public void ExportCommand_HasFormatOption()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var exportCommand = command.Subcommands.First(c => c.Name == "export");
// Act
var formatOption = exportCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("-f") || o.Aliases.Contains("--format"));
// Assert
Assert.NotNull(formatOption);
}
[Fact]
public void ExportCommand_HasOutputOption()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var exportCommand = command.Subcommands.First(c => c.Name == "export");
// Act
var outputOption = exportCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("-o") || o.Aliases.Contains("--output"));
// Assert
Assert.NotNull(outputOption);
}
[Theory]
[InlineData("json")]
[InlineData("sarif")]
public void ExportCommand_FormatOption_AcceptsValidFormats(string format)
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
var exportCommand = command.Subcommands.First(c => c.Name == "export");
// Act
var parseResult = exportCommand.Parse($"wit:abc123 --format {format}");
// Assert
Assert.Empty(parseResult.Errors);
}
#endregion
#region Integration Tests
[Fact]
public void WitnessCommand_CanParseShowCommand()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
// Act
var parseResult = command.Parse("show wit:sha256:abc123 --format json");
// Assert
Assert.Equal("show", parseResult.CommandResult.Command.Name);
Assert.Empty(parseResult.Errors);
}
[Fact]
public void WitnessCommand_CanParseVerifyCommand()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
// Act
var parseResult = command.Parse("verify wit:sha256:abc123 --offline");
// Assert
Assert.Equal("verify", parseResult.CommandResult.Command.Name);
Assert.Empty(parseResult.Errors);
}
[Fact]
public void WitnessCommand_CanParseListCommand()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
// Act
var parseResult = command.Parse("list --scan scan-12345 --reachable-only");
// Assert
Assert.Equal("list", parseResult.CommandResult.Command.Name);
Assert.Empty(parseResult.Errors);
}
[Fact]
public void WitnessCommand_CanParseExportCommand()
{
// Arrange
var command = WitnessCommandGroup.BuildWitnessCommand(_services, _verboseOption, _cancellationToken);
// Act
var parseResult = command.Parse("export wit:sha256:abc123 --format sarif --output report.sarif");
// Assert
Assert.Equal("export", parseResult.CommandResult.Command.Name);
Assert.Empty(parseResult.Errors);
}
#endregion
}