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.
This commit is contained in:
StellaOps Bot
2025-12-22 23:21:21 +02:00
parent 3ba7157b00
commit 5146204f1b
529 changed files with 73579 additions and 5985 deletions

View File

@@ -0,0 +1,570 @@
// -----------------------------------------------------------------------------
// CompareCommandTests.cs
// Sprint: SPRINT_4200_0002_0004_cli_compare
// Tasks: #7 (CLI Compare Tests)
// Description: Unit tests for CLI compare commands
// -----------------------------------------------------------------------------
using System.CommandLine;
using System.CommandLine.Parsing;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using StellaOps.Cli.Commands.Compare;
namespace StellaOps.Cli.Tests.Commands;
/// <summary>
/// Unit tests for compare CLI commands.
/// </summary>
public class CompareCommandTests
{
private readonly IServiceProvider _services;
private readonly Option<bool> _verboseOption;
private readonly CancellationToken _cancellationToken;
public CompareCommandTests()
{
_services = new ServiceCollection()
.AddSingleton<ICompareClient, LocalCompareClient>()
.BuildServiceProvider();
_verboseOption = new Option<bool>("--verbose", "Enable verbose output");
_verboseOption.AddAlias("-v");
_cancellationToken = CancellationToken.None;
}
#region Command Structure Tests
[Fact]
public void BuildCompareCommand_CreatesCompareCommandTree()
{
// Act
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
// Assert
Assert.Equal("compare", command.Name);
Assert.Equal("Compare scan snapshots (SBOM/vulnerability diff).", command.Description);
}
[Fact]
public void BuildCompareCommand_HasDiffSubcommand()
{
// Act
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var diffCommand = command.Subcommands.FirstOrDefault(c => c.Name == "diff");
// Assert
Assert.NotNull(diffCommand);
Assert.Equal("Compare two scan snapshots and show detailed diff.", diffCommand.Description);
}
[Fact]
public void BuildCompareCommand_HasSummarySubcommand()
{
// Act
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var summaryCommand = command.Subcommands.FirstOrDefault(c => c.Name == "summary");
// Assert
Assert.NotNull(summaryCommand);
Assert.Equal("Show quick summary of changes between snapshots.", summaryCommand.Description);
}
[Fact]
public void BuildCompareCommand_HasCanShipSubcommand()
{
// Act
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var canShipCommand = command.Subcommands.FirstOrDefault(c => c.Name == "can-ship");
// Assert
Assert.NotNull(canShipCommand);
Assert.Equal("Check if target snapshot can ship relative to base.", canShipCommand.Description);
}
[Fact]
public void BuildCompareCommand_HasVulnsSubcommand()
{
// Act
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var vulnsCommand = command.Subcommands.FirstOrDefault(c => c.Name == "vulns");
// Assert
Assert.NotNull(vulnsCommand);
Assert.Equal("List vulnerability changes between snapshots.", vulnsCommand.Description);
}
#endregion
#region Option Tests
[Fact]
public void DiffCommand_HasBaseOption()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var diffCommand = command.Subcommands.First(c => c.Name == "diff");
// Act
var baseOption = diffCommand.Options.FirstOrDefault(o =>
o.Name == "--base" || o.Aliases.Contains("--base") || o.Aliases.Contains("-b"));
// Assert
Assert.NotNull(baseOption);
}
[Fact]
public void DiffCommand_HasTargetOption()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var diffCommand = command.Subcommands.First(c => c.Name == "diff");
// Act
var targetOption = diffCommand.Options.FirstOrDefault(o =>
o.Name == "--target" || o.Aliases.Contains("--target") || o.Aliases.Contains("-t"));
// Assert
Assert.NotNull(targetOption);
}
[Fact]
public void DiffCommand_HasOutputOption()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var diffCommand = command.Subcommands.First(c => c.Name == "diff");
// Act
var outputOption = diffCommand.Options.FirstOrDefault(o =>
o.Name == "--output" || o.Aliases.Contains("--output") || o.Aliases.Contains("-o"));
// Assert
Assert.NotNull(outputOption);
}
[Fact]
public void DiffCommand_HasOutputFileOption()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var diffCommand = command.Subcommands.First(c => c.Name == "diff");
// Act
var outputFileOption = diffCommand.Options.FirstOrDefault(o =>
o.Name == "--output-file" || o.Aliases.Contains("--output-file") || o.Aliases.Contains("-f"));
// Assert
Assert.NotNull(outputFileOption);
}
[Fact]
public void DiffCommand_HasSeverityFilterOption()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var diffCommand = command.Subcommands.First(c => c.Name == "diff");
// Act
var severityOption = diffCommand.Options.FirstOrDefault(o =>
o.Name == "--severity" || o.Aliases.Contains("--severity") || o.Aliases.Contains("-s"));
// Assert
Assert.NotNull(severityOption);
}
[Fact]
public void DiffCommand_HasIncludeUnchangedOption()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var diffCommand = command.Subcommands.First(c => c.Name == "diff");
// Act
var includeUnchangedOption = diffCommand.Options.FirstOrDefault(o =>
o.Name == "--include-unchanged" || o.Aliases.Contains("--include-unchanged"));
// Assert
Assert.NotNull(includeUnchangedOption);
}
[Fact]
public void DiffCommand_HasBackendUrlOption()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var diffCommand = command.Subcommands.First(c => c.Name == "diff");
// Act
var backendUrlOption = diffCommand.Options.FirstOrDefault(o =>
o.Name == "--backend-url" || o.Aliases.Contains("--backend-url"));
// Assert
Assert.NotNull(backendUrlOption);
}
#endregion
#region Parse Tests
[Fact]
public void CompareDiff_ParsesWithBaseAndTarget()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var root = new RootCommand { command };
var parser = new Parser(root);
// Act
var result = parser.Parse("compare diff --base sha256:abc123 --target sha256:def456");
// Assert
Assert.Empty(result.Errors);
}
[Fact]
public void CompareDiff_ParsesWithShortOptions()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var root = new RootCommand { command };
var parser = new Parser(root);
// Act
var result = parser.Parse("compare diff -b sha256:abc123 -t sha256:def456");
// Assert
Assert.Empty(result.Errors);
}
[Fact]
public void CompareDiff_ParsesWithJsonOutput()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var root = new RootCommand { command };
var parser = new Parser(root);
// Act
var result = parser.Parse("compare diff -b sha256:abc123 -t sha256:def456 -o json");
// Assert
Assert.Empty(result.Errors);
}
[Fact]
public void CompareDiff_ParsesWithSarifOutput()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var root = new RootCommand { command };
var parser = new Parser(root);
// Act
var result = parser.Parse("compare diff -b sha256:abc123 -t sha256:def456 -o sarif");
// Assert
Assert.Empty(result.Errors);
}
[Fact]
public void CompareDiff_ParsesWithOutputFile()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var root = new RootCommand { command };
var parser = new Parser(root);
// Act
var result = parser.Parse("compare diff -b sha256:abc123 -t sha256:def456 -o json -f output.json");
// Assert
Assert.Empty(result.Errors);
}
[Fact]
public void CompareDiff_ParsesWithSeverityFilter()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var root = new RootCommand { command };
var parser = new Parser(root);
// Act
var result = parser.Parse("compare diff -b sha256:abc123 -t sha256:def456 -s critical");
// Assert
Assert.Empty(result.Errors);
}
[Fact]
public void CompareDiff_ParsesWithIncludeUnchanged()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var root = new RootCommand { command };
var parser = new Parser(root);
// Act
var result = parser.Parse("compare diff -b sha256:abc123 -t sha256:def456 --include-unchanged");
// Assert
Assert.Empty(result.Errors);
}
[Fact]
public void CompareDiff_FailsWithoutBase()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var root = new RootCommand { command };
var parser = new Parser(root);
// Act
var result = parser.Parse("compare diff -t sha256:def456");
// Assert
Assert.NotEmpty(result.Errors);
}
[Fact]
public void CompareDiff_FailsWithoutTarget()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var root = new RootCommand { command };
var parser = new Parser(root);
// Act
var result = parser.Parse("compare diff -b sha256:abc123");
// Assert
Assert.NotEmpty(result.Errors);
}
[Fact]
public void CompareSummary_ParsesWithBaseAndTarget()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var root = new RootCommand { command };
var parser = new Parser(root);
// Act
var result = parser.Parse("compare summary -b sha256:abc123 -t sha256:def456");
// Assert
Assert.Empty(result.Errors);
}
[Fact]
public void CompareCanShip_ParsesWithBaseAndTarget()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var root = new RootCommand { command };
var parser = new Parser(root);
// Act
var result = parser.Parse("compare can-ship -b sha256:abc123 -t sha256:def456");
// Assert
Assert.Empty(result.Errors);
}
[Fact]
public void CompareVulns_ParsesWithBaseAndTarget()
{
// Arrange
var command = CompareCommandBuilder.BuildCompareCommand(_services, _verboseOption, _cancellationToken);
var root = new RootCommand { command };
var parser = new Parser(root);
// Act
var result = parser.Parse("compare vulns -b sha256:abc123 -t sha256:def456");
// Assert
Assert.Empty(result.Errors);
}
#endregion
#region LocalCompareClient Tests
[Fact]
public async Task LocalCompareClient_CompareAsync_ReturnsResult()
{
// Arrange
var client = new LocalCompareClient();
var request = new CompareRequest
{
BaseDigest = "sha256:abc123",
TargetDigest = "sha256:def456"
};
// Act
var result = await client.CompareAsync(request);
// Assert
Assert.NotNull(result);
Assert.Equal(request.BaseDigest, result.BaseDigest);
Assert.Equal(request.TargetDigest, result.TargetDigest);
Assert.NotNull(result.Summary);
}
[Fact]
public async Task LocalCompareClient_GetSummaryAsync_ReturnsSummary()
{
// Arrange
var client = new LocalCompareClient();
// Act
var summary = await client.GetSummaryAsync("sha256:abc123", "sha256:def456", null);
// Assert
Assert.NotNull(summary);
Assert.True(summary.CanShip);
Assert.NotNull(summary.RiskDirection);
}
[Fact]
public async Task LocalCompareClient_CompareAsync_ReturnsEmptyVulnerabilities()
{
// Arrange
var client = new LocalCompareClient();
var request = new CompareRequest
{
BaseDigest = "sha256:abc123",
TargetDigest = "sha256:def456"
};
// Act
var result = await client.CompareAsync(request);
// Assert
Assert.NotNull(result.Vulnerabilities);
Assert.Empty(result.Vulnerabilities);
}
[Fact]
public async Task LocalCompareClient_CompareAsync_ReturnsUnchangedDirection()
{
// Arrange
var client = new LocalCompareClient();
var request = new CompareRequest
{
BaseDigest = "sha256:abc123",
TargetDigest = "sha256:def456"
};
// Act
var result = await client.CompareAsync(request);
// Assert
Assert.Equal("unchanged", result.RiskDirection);
}
[Fact]
public async Task LocalCompareClient_GetSummaryAsync_ReturnsZeroNetChange()
{
// Arrange
var client = new LocalCompareClient();
// Act
var summary = await client.GetSummaryAsync("sha256:abc123", "sha256:def456", null);
// Assert
Assert.Equal(0, summary.NetBlockingChange);
}
#endregion
#region Record Model Tests
[Fact]
public void CompareRequest_CanBeCreated()
{
// Arrange & Act
var request = new CompareRequest
{
BaseDigest = "sha256:abc",
TargetDigest = "sha256:def"
};
// Assert
Assert.Equal("sha256:abc", request.BaseDigest);
Assert.Equal("sha256:def", request.TargetDigest);
Assert.False(request.IncludeUnchanged);
Assert.Null(request.SeverityFilter);
Assert.Null(request.BackendUrl);
}
[Fact]
public void CompareResult_CanBeCreated()
{
// Arrange & Act
var result = new CompareResult
{
BaseDigest = "sha256:abc",
TargetDigest = "sha256:def",
RiskDirection = "improved",
Summary = new CompareSummary
{
CanShip = true,
RiskDirection = "improved",
Summary = "Test summary"
},
Vulnerabilities = []
};
// Assert
Assert.Equal("sha256:abc", result.BaseDigest);
Assert.Equal("sha256:def", result.TargetDigest);
Assert.Equal("improved", result.RiskDirection);
Assert.True(result.Summary.CanShip);
}
[Fact]
public void CompareSummary_CanBeCreated()
{
// Arrange & Act
var summary = new CompareSummary
{
CanShip = false,
RiskDirection = "degraded",
NetBlockingChange = 5,
Added = 3,
Removed = 1,
CriticalAdded = 2,
Summary = "Risk increased"
};
// Assert
Assert.False(summary.CanShip);
Assert.Equal("degraded", summary.RiskDirection);
Assert.Equal(5, summary.NetBlockingChange);
Assert.Equal(3, summary.Added);
Assert.Equal(1, summary.Removed);
Assert.Equal(2, summary.CriticalAdded);
}
[Fact]
public void VulnChange_CanBeCreated()
{
// Arrange & Act
var vuln = new VulnChange
{
VulnId = "CVE-2024-12345",
Purl = "pkg:npm/lodash@4.17.20",
ChangeType = "Added",
Severity = "High"
};
// Assert
Assert.Equal("CVE-2024-12345", vuln.VulnId);
Assert.Equal("pkg:npm/lodash@4.17.20", vuln.Purl);
Assert.Equal("Added", vuln.ChangeType);
Assert.Equal("High", vuln.Severity);
}
#endregion
}