Add unit tests for PhpFrameworkSurface and PhpPharScanner
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
- Implement comprehensive tests for PhpFrameworkSurface, covering scenarios such as empty surfaces, presence of routes, controllers, middlewares, CLI commands, cron jobs, and event listeners. - Validate metadata creation for route counts, HTTP methods, protected and public routes, and route patterns. - Introduce tests for PhpPharScanner, including handling of non-existent files, null or empty paths, invalid PHAR files, and minimal PHAR structures. - Ensure correct computation of SHA256 for valid PHAR files and validate the properties of PhpPharArchive, PhpPharEntry, and PhpPharScanResult.
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Php.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Php.Tests.Internal;
|
||||
|
||||
public sealed class ComposerLockReaderTests : IDisposable
|
||||
{
|
||||
private readonly string _testDir;
|
||||
|
||||
public ComposerLockReaderTests()
|
||||
{
|
||||
_testDir = Path.Combine(Path.GetTempPath(), $"php-lock-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_testDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_testDir))
|
||||
{
|
||||
Directory.Delete(_testDir, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_NoLockFile_ReturnsEmpty()
|
||||
{
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.True(result.IsEmpty);
|
||||
Assert.Empty(result.Packages);
|
||||
Assert.Empty(result.DevPackages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ValidLockFile_ParsesPackages()
|
||||
{
|
||||
var lockContent = @"{
|
||||
""content-hash"": ""abc123def456"",
|
||||
""plugin-api-version"": ""2.6.0"",
|
||||
""packages"": [
|
||||
{
|
||||
""name"": ""vendor/package"",
|
||||
""version"": ""1.2.3"",
|
||||
""type"": ""library""
|
||||
}
|
||||
],
|
||||
""packages-dev"": []
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.lock"), lockContent);
|
||||
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsEmpty);
|
||||
Assert.Single(result.Packages);
|
||||
Assert.Equal("vendor/package", result.Packages[0].Name);
|
||||
Assert.Equal("1.2.3", result.Packages[0].Version);
|
||||
Assert.Equal("library", result.Packages[0].Type);
|
||||
Assert.False(result.Packages[0].IsDev);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesDevPackages()
|
||||
{
|
||||
var lockContent = @"{
|
||||
""packages"": [],
|
||||
""packages-dev"": [
|
||||
{
|
||||
""name"": ""phpunit/phpunit"",
|
||||
""version"": ""10.0.0"",
|
||||
""type"": ""library""
|
||||
}
|
||||
]
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.lock"), lockContent);
|
||||
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Single(result.DevPackages);
|
||||
Assert.Equal("phpunit/phpunit", result.DevPackages[0].Name);
|
||||
Assert.True(result.DevPackages[0].IsDev);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesContentHashAndPluginApi()
|
||||
{
|
||||
var lockContent = @"{
|
||||
""content-hash"": ""a1b2c3d4e5f6"",
|
||||
""plugin-api-version"": ""2.3.0"",
|
||||
""packages"": []
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.lock"), lockContent);
|
||||
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal("a1b2c3d4e5f6", result.ContentHash);
|
||||
Assert.Equal("2.3.0", result.PluginApiVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesSourceInfo()
|
||||
{
|
||||
var lockContent = @"{
|
||||
""packages"": [
|
||||
{
|
||||
""name"": ""vendor/package"",
|
||||
""version"": ""1.0.0"",
|
||||
""source"": {
|
||||
""type"": ""git"",
|
||||
""url"": ""https://github.com/vendor/package.git"",
|
||||
""reference"": ""abc123def456789""
|
||||
}
|
||||
}
|
||||
]
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.lock"), lockContent);
|
||||
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.Equal("git", result.Packages[0].SourceType);
|
||||
Assert.Equal("abc123def456789", result.Packages[0].SourceReference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesDistInfo()
|
||||
{
|
||||
var lockContent = @"{
|
||||
""packages"": [
|
||||
{
|
||||
""name"": ""vendor/package"",
|
||||
""version"": ""1.0.0"",
|
||||
""dist"": {
|
||||
""type"": ""zip"",
|
||||
""url"": ""https://packagist.org/vendor/package/1.0.0"",
|
||||
""shasum"": ""sha256hashhere""
|
||||
}
|
||||
}
|
||||
]
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.lock"), lockContent);
|
||||
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.Equal("sha256hashhere", result.Packages[0].DistSha);
|
||||
Assert.Equal("https://packagist.org/vendor/package/1.0.0", result.Packages[0].DistUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesAutoloadPsr4()
|
||||
{
|
||||
var lockContent = @"{
|
||||
""packages"": [
|
||||
{
|
||||
""name"": ""vendor/package"",
|
||||
""version"": ""1.0.0"",
|
||||
""autoload"": {
|
||||
""psr-4"": {
|
||||
""Vendor\\Package\\"": ""src/""
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.lock"), lockContent);
|
||||
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.NotEmpty(result.Packages[0].Autoload.Psr4);
|
||||
Assert.Contains("Vendor\\Package\\->src/", result.Packages[0].Autoload.Psr4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesAutoloadClassmap()
|
||||
{
|
||||
var lockContent = @"{
|
||||
""packages"": [
|
||||
{
|
||||
""name"": ""vendor/package"",
|
||||
""version"": ""1.0.0"",
|
||||
""autoload"": {
|
||||
""classmap"": [
|
||||
""src/"",
|
||||
""lib/""
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.lock"), lockContent);
|
||||
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.Equal(2, result.Packages[0].Autoload.Classmap.Count);
|
||||
Assert.Contains("src/", result.Packages[0].Autoload.Classmap);
|
||||
Assert.Contains("lib/", result.Packages[0].Autoload.Classmap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesAutoloadFiles()
|
||||
{
|
||||
var lockContent = @"{
|
||||
""packages"": [
|
||||
{
|
||||
""name"": ""vendor/package"",
|
||||
""version"": ""1.0.0"",
|
||||
""autoload"": {
|
||||
""files"": [
|
||||
""src/helpers.php"",
|
||||
""src/functions.php""
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.lock"), lockContent);
|
||||
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.Equal(2, result.Packages[0].Autoload.Files.Count);
|
||||
Assert.Contains("src/helpers.php", result.Packages[0].Autoload.Files);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_MultiplePackages_ParsesAll()
|
||||
{
|
||||
var lockContent = @"{
|
||||
""packages"": [
|
||||
{ ""name"": ""vendor/first"", ""version"": ""1.0.0"" },
|
||||
{ ""name"": ""vendor/second"", ""version"": ""2.0.0"" },
|
||||
{ ""name"": ""vendor/third"", ""version"": ""3.0.0"" }
|
||||
],
|
||||
""packages-dev"": [
|
||||
{ ""name"": ""dev/tool"", ""version"": ""0.1.0"" }
|
||||
]
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.lock"), lockContent);
|
||||
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(3, result.Packages.Count);
|
||||
Assert.Single(result.DevPackages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ComputesSha256()
|
||||
{
|
||||
var lockContent = @"{ ""packages"": [] }";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.lock"), lockContent);
|
||||
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result.LockSha256);
|
||||
Assert.Equal(64, result.LockSha256.Length); // SHA256 hex string length
|
||||
Assert.True(result.LockSha256.All(c => char.IsAsciiHexDigitLower(c)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_SetsLockPath()
|
||||
{
|
||||
var lockContent = @"{ ""packages"": [] }";
|
||||
var lockPath = Path.Combine(_testDir, "composer.lock");
|
||||
await File.WriteAllTextAsync(lockPath, lockContent);
|
||||
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(lockPath, result.LockPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_MissingRequiredFields_SkipsPackage()
|
||||
{
|
||||
var lockContent = @"{
|
||||
""packages"": [
|
||||
{ ""name"": ""valid/package"", ""version"": ""1.0.0"" },
|
||||
{ ""name"": ""missing-version"" },
|
||||
{ ""version"": ""1.0.0"" }
|
||||
]
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.lock"), lockContent);
|
||||
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.Equal("valid/package", result.Packages[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_ReturnsEmptyInstance()
|
||||
{
|
||||
var empty = ComposerLockData.Empty;
|
||||
|
||||
Assert.True(empty.IsEmpty);
|
||||
Assert.Empty(empty.Packages);
|
||||
Assert.Empty(empty.DevPackages);
|
||||
Assert.Equal(string.Empty, empty.LockPath);
|
||||
Assert.Null(empty.ContentHash);
|
||||
Assert.Null(empty.PluginApiVersion);
|
||||
Assert.Null(empty.LockSha256);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_Psr4ArrayPaths_ParsesMultiplePaths()
|
||||
{
|
||||
var lockContent = @"{
|
||||
""packages"": [
|
||||
{
|
||||
""name"": ""vendor/package"",
|
||||
""version"": ""1.0.0"",
|
||||
""autoload"": {
|
||||
""psr-4"": {
|
||||
""Vendor\\Package\\"": [""src/"", ""lib/""]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.lock"), lockContent);
|
||||
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.Equal(2, result.Packages[0].Autoload.Psr4.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_NormalizesBackslashesInPaths()
|
||||
{
|
||||
var lockContent = @"{
|
||||
""packages"": [
|
||||
{
|
||||
""name"": ""vendor/package"",
|
||||
""version"": ""1.0.0"",
|
||||
""autoload"": {
|
||||
""files"": [""src\\helpers.php""]
|
||||
}
|
||||
}
|
||||
]
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.lock"), lockContent);
|
||||
|
||||
var context = CreateContext(_testDir);
|
||||
var result = await ComposerLockData.LoadAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.Contains("src/helpers.php", result.Packages[0].Autoload.Files);
|
||||
}
|
||||
|
||||
private static LanguageAnalyzerContext CreateContext(string rootPath)
|
||||
{
|
||||
return new LanguageAnalyzerContext(rootPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,672 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Php.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Php.Tests.Internal;
|
||||
|
||||
public sealed class PhpCapabilityScannerTests
|
||||
{
|
||||
#region Exec Capabilities
|
||||
|
||||
[Theory]
|
||||
[InlineData("exec('ls -la');", "exec")]
|
||||
[InlineData("shell_exec('whoami');", "shell_exec")]
|
||||
[InlineData("system('cat /etc/passwd');", "system")]
|
||||
[InlineData("passthru('top');", "passthru")]
|
||||
[InlineData("popen('/bin/sh', 'r');", "popen")]
|
||||
[InlineData("proc_open('ls', $descriptors, $pipes);", "proc_open")]
|
||||
[InlineData("pcntl_exec('/bin/bash');", "pcntl_exec")]
|
||||
public void ScanContent_ExecFunction_DetectsCriticalRisk(string line, string expectedFunction)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.Exec && e.FunctionOrPattern == expectedFunction);
|
||||
Assert.All(result.Where(e => e.Kind == PhpCapabilityKind.Exec), e => Assert.Equal(PhpCapabilityRisk.Critical, e.Risk));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_BacktickOperator_DetectsCriticalRisk()
|
||||
{
|
||||
var content = "<?php\n$output = `ls -la`;";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.Exec && e.FunctionOrPattern == "backtick_operator");
|
||||
Assert.Contains(result, e => e.Risk == PhpCapabilityRisk.Critical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_ExecInComment_DoesNotDetect()
|
||||
{
|
||||
var content = @"<?php
|
||||
// exec('ls -la');
|
||||
/* shell_exec('whoami'); */
|
||||
# system('cat');
|
||||
";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Filesystem Capabilities
|
||||
|
||||
[Theory]
|
||||
[InlineData("fopen('file.txt', 'r');", "fopen", PhpCapabilityRisk.Medium)]
|
||||
[InlineData("fwrite($fp, $data);", "fwrite", PhpCapabilityRisk.Medium)]
|
||||
[InlineData("fread($fp, 1024);", "fread", PhpCapabilityRisk.Low)]
|
||||
[InlineData("file_get_contents('data.txt');", "file_get_contents", PhpCapabilityRisk.Medium)]
|
||||
[InlineData("file_put_contents('out.txt', $data);", "file_put_contents", PhpCapabilityRisk.Medium)]
|
||||
public void ScanContent_FileReadWrite_DetectsAppropriateRisk(string line, string expectedFunction, PhpCapabilityRisk expectedRisk)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.Filesystem && e.FunctionOrPattern == expectedFunction);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(expectedRisk, evidence.Risk);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("unlink('file.txt');", "unlink", PhpCapabilityRisk.High)]
|
||||
[InlineData("rmdir('/tmp/dir');", "rmdir", PhpCapabilityRisk.High)]
|
||||
[InlineData("chmod('script.sh', 0755);", "chmod", PhpCapabilityRisk.High)]
|
||||
[InlineData("chown('file.txt', 'root');", "chown", PhpCapabilityRisk.High)]
|
||||
[InlineData("symlink('/etc/passwd', 'link');", "symlink", PhpCapabilityRisk.High)]
|
||||
public void ScanContent_DangerousFileOps_DetectsHighRisk(string line, string expectedFunction, PhpCapabilityRisk expectedRisk)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.Filesystem && e.FunctionOrPattern == expectedFunction);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(expectedRisk, evidence.Risk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_DirectoryFunctions_DetectsLowRisk()
|
||||
{
|
||||
var content = @"<?php
|
||||
$files = scandir('/var/www');
|
||||
$matches = glob('*.php');
|
||||
$dir = opendir('/home');
|
||||
";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.All(result.Where(e => e.Kind == PhpCapabilityKind.Filesystem), e => Assert.Equal(PhpCapabilityRisk.Low, e.Risk));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Network Capabilities
|
||||
|
||||
[Theory]
|
||||
[InlineData("curl_init('http://example.com');", "curl_init")]
|
||||
[InlineData("curl_exec($ch);", "curl_exec")]
|
||||
[InlineData("curl_multi_exec($mh, $active);", "curl_multi_exec")]
|
||||
public void ScanContent_CurlFunctions_DetectsMediumRisk(string line, string expectedFunction)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.Network && e.FunctionOrPattern == expectedFunction);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(PhpCapabilityRisk.Medium, evidence.Risk);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("fsockopen('localhost', 80);", "fsockopen")]
|
||||
[InlineData("socket_create(AF_INET, SOCK_STREAM, SOL_TCP);", "socket_create")]
|
||||
[InlineData("socket_connect($socket, '127.0.0.1', 8080);", "socket_connect")]
|
||||
[InlineData("stream_socket_client('tcp://localhost:80');", "stream_socket_client")]
|
||||
[InlineData("stream_socket_server('tcp://0.0.0.0:8000');", "stream_socket_server")]
|
||||
public void ScanContent_SocketFunctions_DetectsHighRisk(string line, string expectedFunction)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.Network && e.FunctionOrPattern == expectedFunction);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(PhpCapabilityRisk.High, evidence.Risk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_FileGetContentsWithUrl_DetectsNetworkCapability()
|
||||
{
|
||||
var content = "<?php\n$data = file_get_contents('http://example.com/api');";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.Network && e.FunctionOrPattern == "file_get_contents_url");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Environment Capabilities
|
||||
|
||||
[Theory]
|
||||
[InlineData("getenv('HOME');", "getenv", PhpCapabilityRisk.Medium)]
|
||||
[InlineData("putenv('PATH=/usr/bin');", "putenv", PhpCapabilityRisk.High)]
|
||||
[InlineData("apache_getenv('DOCUMENT_ROOT');", "apache_getenv", PhpCapabilityRisk.Medium)]
|
||||
[InlineData("apache_setenv('MY_VAR', 'value');", "apache_setenv", PhpCapabilityRisk.High)]
|
||||
public void ScanContent_EnvFunctions_DetectsAppropriateRisk(string line, string expectedFunction, PhpCapabilityRisk expectedRisk)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.Environment && e.FunctionOrPattern == expectedFunction);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(expectedRisk, evidence.Risk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_EnvSuperglobal_DetectsMediumRisk()
|
||||
{
|
||||
var content = "<?php\n$path = $_ENV['PATH'];";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.Environment && e.FunctionOrPattern == "$_ENV");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_ServerSuperglobal_DetectsLowRisk()
|
||||
{
|
||||
var content = "<?php\n$host = $_SERVER['HTTP_HOST'];";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.Environment && e.FunctionOrPattern == "$_SERVER");
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(PhpCapabilityRisk.Low, evidence.Risk);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialization Capabilities
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_Unserialize_DetectsCriticalRisk()
|
||||
{
|
||||
var content = "<?php\n$obj = unserialize($data);";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.Serialization && e.FunctionOrPattern == "unserialize");
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(PhpCapabilityRisk.Critical, evidence.Risk);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("serialize($object);", "serialize", PhpCapabilityRisk.Low)]
|
||||
[InlineData("json_encode($data);", "json_encode", PhpCapabilityRisk.Low)]
|
||||
[InlineData("json_decode($json);", "json_decode", PhpCapabilityRisk.Low)]
|
||||
[InlineData("igbinary_unserialize($data);", "igbinary_unserialize", PhpCapabilityRisk.High)]
|
||||
public void ScanContent_SerializationFunctions_DetectsAppropriateRisk(string line, string expectedFunction, PhpCapabilityRisk expectedRisk)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.Serialization && e.FunctionOrPattern == expectedFunction);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(expectedRisk, evidence.Risk);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("public function __wakeup()")]
|
||||
[InlineData("private function __sleep()")]
|
||||
[InlineData("public function __serialize()")]
|
||||
[InlineData("public function __unserialize($data)")]
|
||||
public void ScanContent_SerializationMagicMethods_Detects(string line)
|
||||
{
|
||||
var content = $"<?php\nclass Test {{\n {line} {{ }}\n}}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.Serialization);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Crypto Capabilities
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_OpenSslFunctions_DetectsMediumRisk()
|
||||
{
|
||||
var content = @"<?php
|
||||
openssl_encrypt($data, 'AES-256-CBC', $key);
|
||||
openssl_decrypt($encrypted, 'AES-256-CBC', $key);
|
||||
openssl_sign($data, $signature, $privateKey);
|
||||
";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.True(result.Count(e => e.Kind == PhpCapabilityKind.Crypto) >= 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_SodiumFunctions_DetectsLowRisk()
|
||||
{
|
||||
var content = @"<?php
|
||||
sodium_crypto_secretbox($message, $nonce, $key);
|
||||
sodium_crypto_box($message, $nonce, $keyPair);
|
||||
";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.All(result.Where(e => e.Kind == PhpCapabilityKind.Crypto && e.Pattern.StartsWith("sodium")),
|
||||
e => Assert.Equal(PhpCapabilityRisk.Low, e.Risk));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("md5($password);", "md5", PhpCapabilityRisk.Medium)]
|
||||
[InlineData("sha1($data);", "sha1", PhpCapabilityRisk.Low)]
|
||||
[InlineData("hash('sha256', $data);", "hash", PhpCapabilityRisk.Low)]
|
||||
[InlineData("password_hash($password, PASSWORD_DEFAULT);", "password_hash", PhpCapabilityRisk.Low)]
|
||||
[InlineData("mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC);", "mcrypt_encrypt", PhpCapabilityRisk.High)]
|
||||
public void ScanContent_HashFunctions_DetectsAppropriateRisk(string line, string expectedFunction, PhpCapabilityRisk expectedRisk)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.Crypto && e.FunctionOrPattern == expectedFunction);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(expectedRisk, evidence.Risk);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Database Capabilities
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_MysqliFunctions_DetectsDatabase()
|
||||
{
|
||||
var content = @"<?php
|
||||
$conn = mysqli_connect('localhost', 'user', 'pass', 'db');
|
||||
mysqli_query($conn, 'SELECT * FROM users');
|
||||
mysqli_close($conn);
|
||||
";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.True(result.Count(e => e.Kind == PhpCapabilityKind.Database) >= 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_PdoUsage_DetectsDatabase()
|
||||
{
|
||||
var content = @"<?php
|
||||
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
|
||||
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.Database && e.FunctionOrPattern == "PDO");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_PostgresFunctions_DetectsDatabase()
|
||||
{
|
||||
var content = @"<?php
|
||||
$conn = pg_connect('host=localhost dbname=test');
|
||||
pg_query($conn, 'SELECT * FROM users');
|
||||
";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.True(result.Count(e => e.Kind == PhpCapabilityKind.Database) >= 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_RawSqlQuery_DetectsHighRisk()
|
||||
{
|
||||
var content = "<?php\n$query = \"SELECT * FROM users WHERE id = $id\";";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.Database && e.FunctionOrPattern == "raw_sql_query");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Upload Capabilities
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_FilesSuperglobal_DetectsHighRisk()
|
||||
{
|
||||
var content = "<?php\n$file = $_FILES['upload'];";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.Upload && e.FunctionOrPattern == "$_FILES");
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(PhpCapabilityRisk.High, evidence.Risk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_MoveUploadedFile_DetectsHighRisk()
|
||||
{
|
||||
var content = "<?php\nmove_uploaded_file($_FILES['file']['tmp_name'], '/uploads/file.txt');";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.Upload && e.FunctionOrPattern == "move_uploaded_file");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stream Wrapper Capabilities
|
||||
|
||||
[Theory]
|
||||
[InlineData("php://input", PhpCapabilityRisk.High)]
|
||||
[InlineData("php://filter", PhpCapabilityRisk.Critical)]
|
||||
[InlineData("php://memory", PhpCapabilityRisk.Low)]
|
||||
[InlineData("data://", PhpCapabilityRisk.High)]
|
||||
[InlineData("phar://", PhpCapabilityRisk.Critical)]
|
||||
[InlineData("zip://", PhpCapabilityRisk.High)]
|
||||
[InlineData("expect://", PhpCapabilityRisk.Critical)]
|
||||
public void ScanContent_StreamWrappers_DetectsAppropriateRisk(string wrapper, PhpCapabilityRisk expectedRisk)
|
||||
{
|
||||
var content = $"<?php\n$data = file_get_contents('{wrapper}data');";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.StreamWrapper && e.FunctionOrPattern == wrapper);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(expectedRisk, evidence.Risk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_StreamWrapperRegister_DetectsHighRisk()
|
||||
{
|
||||
var content = "<?php\nstream_wrapper_register('myproto', 'MyProtocolHandler');";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.StreamWrapper && e.FunctionOrPattern == "stream_wrapper_register");
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(PhpCapabilityRisk.High, evidence.Risk);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dynamic Code Capabilities
|
||||
|
||||
[Theory]
|
||||
[InlineData("eval($code);", "eval")]
|
||||
[InlineData("create_function('$a', 'return $a * 2;');", "create_function")]
|
||||
[InlineData("assert($condition);", "assert")]
|
||||
public void ScanContent_DynamicCodeExecution_DetectsCriticalRisk(string line, string expectedFunction)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.DynamicCode && e.FunctionOrPattern == expectedFunction);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(PhpCapabilityRisk.Critical, evidence.Risk);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("call_user_func($callback, $arg);", "call_user_func")]
|
||||
[InlineData("call_user_func_array($callback, $args);", "call_user_func_array")]
|
||||
[InlineData("preg_replace('/pattern/e', 'code', $subject);", "preg_replace")]
|
||||
public void ScanContent_DynamicCodeHigh_DetectsHighRisk(string line, string expectedFunction)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.DynamicCode && e.FunctionOrPattern == expectedFunction);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(PhpCapabilityRisk.High, evidence.Risk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_VariableFunction_DetectsHighRisk()
|
||||
{
|
||||
var content = "<?php\n$func = 'system';\n$func('ls');";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.DynamicCode && e.FunctionOrPattern == "variable_function");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reflection Capabilities
|
||||
|
||||
[Theory]
|
||||
[InlineData("new ReflectionClass('MyClass');", "ReflectionClass")]
|
||||
[InlineData("new ReflectionMethod($obj, 'method');", "ReflectionMethod")]
|
||||
[InlineData("new ReflectionFunction('func');", "ReflectionFunction")]
|
||||
[InlineData("new ReflectionProperty($obj, 'prop');", "ReflectionProperty")]
|
||||
public void ScanContent_ReflectionClasses_DetectsMediumRisk(string line, string expectedClass)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.Reflection && e.FunctionOrPattern == expectedClass);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(PhpCapabilityRisk.Medium, evidence.Risk);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("get_defined_functions();", "get_defined_functions")]
|
||||
[InlineData("get_defined_vars();", "get_defined_vars")]
|
||||
[InlineData("get_loaded_extensions();", "get_loaded_extensions")]
|
||||
public void ScanContent_IntrospectionFunctions_Detects(string line, string expectedFunction)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.Reflection && e.FunctionOrPattern == expectedFunction);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Output Control Capabilities
|
||||
|
||||
[Theory]
|
||||
[InlineData("header('Location: /redirect');", "header", PhpCapabilityRisk.Medium)]
|
||||
[InlineData("setcookie('session', $value);", "setcookie", PhpCapabilityRisk.Medium)]
|
||||
[InlineData("ob_start();", "ob_start", PhpCapabilityRisk.Low)]
|
||||
public void ScanContent_OutputFunctions_DetectsAppropriateRisk(string line, string expectedFunction, PhpCapabilityRisk expectedRisk)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.OutputControl && e.FunctionOrPattern == expectedFunction);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(expectedRisk, evidence.Risk);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Session Capabilities
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_SessionSuperglobal_DetectsMediumRisk()
|
||||
{
|
||||
var content = "<?php\n$_SESSION['user'] = $userId;";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.Session && e.FunctionOrPattern == "$_SESSION");
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(PhpCapabilityRisk.Medium, evidence.Risk);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("session_start();", "session_start", PhpCapabilityRisk.Medium)]
|
||||
[InlineData("session_destroy();", "session_destroy", PhpCapabilityRisk.Low)]
|
||||
[InlineData("session_set_save_handler($handler);", "session_set_save_handler", PhpCapabilityRisk.High)]
|
||||
public void ScanContent_SessionFunctions_DetectsAppropriateRisk(string line, string expectedFunction, PhpCapabilityRisk expectedRisk)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.Session && e.FunctionOrPattern == expectedFunction);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(expectedRisk, evidence.Risk);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error Handling Capabilities
|
||||
|
||||
[Theory]
|
||||
[InlineData("ini_set('display_errors', 1);", "ini_set", PhpCapabilityRisk.High)]
|
||||
[InlineData("ini_get('memory_limit');", "ini_get", PhpCapabilityRisk.Low)]
|
||||
[InlineData("phpinfo();", "phpinfo", PhpCapabilityRisk.High)]
|
||||
[InlineData("error_reporting(E_ALL);", "error_reporting", PhpCapabilityRisk.Medium)]
|
||||
[InlineData("set_error_handler($handler);", "set_error_handler", PhpCapabilityRisk.Medium)]
|
||||
public void ScanContent_ErrorFunctions_DetectsAppropriateRisk(string line, string expectedFunction, PhpCapabilityRisk expectedRisk)
|
||||
{
|
||||
var content = $"<?php\n{line}";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.FirstOrDefault(e => e.Kind == PhpCapabilityKind.ErrorHandling && e.FunctionOrPattern == expectedFunction);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Equal(expectedRisk, evidence.Risk);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases and Integration
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_EmptyContent_ReturnsEmpty()
|
||||
{
|
||||
var result = PhpCapabilityScanner.ScanContent("", "test.php");
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_NullContent_ReturnsEmpty()
|
||||
{
|
||||
var result = PhpCapabilityScanner.ScanContent(null!, "test.php");
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_WhitespaceContent_ReturnsEmpty()
|
||||
{
|
||||
var result = PhpCapabilityScanner.ScanContent(" \n\t ", "test.php");
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_MultipleCapabilities_DetectsAll()
|
||||
{
|
||||
var content = @"<?php
|
||||
exec('ls');
|
||||
$data = file_get_contents('data.txt');
|
||||
$conn = mysqli_connect('localhost', 'user', 'pass');
|
||||
$_SESSION['user'] = $user;
|
||||
unserialize($input);
|
||||
";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.True(result.Count >= 5);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.Exec);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.Filesystem);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.Database);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.Session);
|
||||
Assert.Contains(result, e => e.Kind == PhpCapabilityKind.Serialization);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_MultiLineComment_SkipsCommentedCode()
|
||||
{
|
||||
var content = @"<?php
|
||||
/*
|
||||
exec('ls');
|
||||
unserialize($data);
|
||||
*/
|
||||
echo 'Hello';
|
||||
";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_CaseInsensitive_DetectsFunctions()
|
||||
{
|
||||
var content = @"<?php
|
||||
EXEC('ls');
|
||||
Shell_Exec('whoami');
|
||||
UNSERIALIZE($data);
|
||||
";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.Contains(result, e => e.FunctionOrPattern == "exec");
|
||||
Assert.Contains(result, e => e.FunctionOrPattern == "shell_exec");
|
||||
Assert.Contains(result, e => e.FunctionOrPattern == "unserialize");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_CorrectLineNumbers_ReportsAccurately()
|
||||
{
|
||||
var content = @"<?php
|
||||
// Line 2
|
||||
// Line 3
|
||||
exec('ls'); // Line 4
|
||||
// Line 5
|
||||
shell_exec('pwd'); // Line 6
|
||||
";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var execEvidence = result.FirstOrDefault(e => e.FunctionOrPattern == "exec");
|
||||
var shellExecEvidence = result.FirstOrDefault(e => e.FunctionOrPattern == "shell_exec");
|
||||
|
||||
Assert.NotNull(execEvidence);
|
||||
Assert.NotNull(shellExecEvidence);
|
||||
Assert.Equal(4, execEvidence.SourceLine);
|
||||
Assert.Equal(6, shellExecEvidence.SourceLine);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_SnippetTruncation_TruncatesLongLines()
|
||||
{
|
||||
var longLine = new string('x', 200);
|
||||
var content = $"<?php\nexec('{longLine}');";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "test.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
var evidence = result.First();
|
||||
Assert.NotNull(evidence.Snippet);
|
||||
Assert.True(evidence.Snippet.Length <= 153); // 150 + "..."
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanContent_SourceFilePreserved_InEvidence()
|
||||
{
|
||||
var content = "<?php\nexec('ls');";
|
||||
var result = PhpCapabilityScanner.ScanContent(content, "src/controllers/AdminController.php");
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.All(result, e => Assert.Equal("src/controllers/AdminController.php", e.SourceFile));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Php.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Php.Tests.Internal;
|
||||
|
||||
public sealed class PhpComposerManifestReaderTests : IDisposable
|
||||
{
|
||||
private readonly string _testDir;
|
||||
|
||||
public PhpComposerManifestReaderTests()
|
||||
{
|
||||
_testDir = Path.Combine(Path.GetTempPath(), $"manifest-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_testDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_testDir))
|
||||
{
|
||||
Directory.Delete(_testDir, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
#region PhpComposerManifestReader Tests
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_NullPath_ReturnsNull()
|
||||
{
|
||||
var result = await PhpComposerManifestReader.LoadAsync(null!, CancellationToken.None);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_EmptyPath_ReturnsNull()
|
||||
{
|
||||
var result = await PhpComposerManifestReader.LoadAsync("", CancellationToken.None);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_NonExistentDirectory_ReturnsNull()
|
||||
{
|
||||
var result = await PhpComposerManifestReader.LoadAsync("/nonexistent/path", CancellationToken.None);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_NoComposerJson_ReturnsNull()
|
||||
{
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_InvalidJson_ReturnsNull()
|
||||
{
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.json"), "{ invalid json }");
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ValidManifest_ParsesBasicFields()
|
||||
{
|
||||
var manifest = @"{
|
||||
""name"": ""vendor/package"",
|
||||
""description"": ""A test package"",
|
||||
""type"": ""library"",
|
||||
""version"": ""1.2.3"",
|
||||
""license"": ""MIT""
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.json"), manifest);
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("vendor/package", result.Name);
|
||||
Assert.Equal("A test package", result.Description);
|
||||
Assert.Equal("library", result.Type);
|
||||
Assert.Equal("1.2.3", result.Version);
|
||||
Assert.Equal("MIT", result.License);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesLicenseArray()
|
||||
{
|
||||
var manifest = @"{
|
||||
""name"": ""vendor/package"",
|
||||
""license"": [""MIT"", ""Apache-2.0""]
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.json"), manifest);
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("MIT OR Apache-2.0", result.License);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesAuthors()
|
||||
{
|
||||
var manifest = @"{
|
||||
""name"": ""vendor/package"",
|
||||
""authors"": [
|
||||
{ ""name"": ""John Doe"", ""email"": ""john@example.com"" },
|
||||
{ ""name"": ""Jane Smith"" }
|
||||
]
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.json"), manifest);
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.Authors.Count);
|
||||
Assert.Contains("John Doe <john@example.com>", result.Authors);
|
||||
Assert.Contains("Jane Smith", result.Authors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesRequireDependencies()
|
||||
{
|
||||
var manifest = @"{
|
||||
""name"": ""vendor/package"",
|
||||
""require"": {
|
||||
""php"": "">=8.1"",
|
||||
""ext-json"": ""*"",
|
||||
""monolog/monolog"": ""^3.0""
|
||||
}
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.json"), manifest);
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(3, result.Require.Count);
|
||||
Assert.Equal(">=8.1", result.Require["php"]);
|
||||
Assert.Equal("*", result.Require["ext-json"]);
|
||||
Assert.Equal("^3.0", result.Require["monolog/monolog"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesRequireDevDependencies()
|
||||
{
|
||||
var manifest = @"{
|
||||
""name"": ""vendor/package"",
|
||||
""require-dev"": {
|
||||
""phpunit/phpunit"": ""^10.0"",
|
||||
""phpstan/phpstan"": ""^1.0""
|
||||
}
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.json"), manifest);
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.RequireDev.Count);
|
||||
Assert.Equal("^10.0", result.RequireDev["phpunit/phpunit"]);
|
||||
Assert.Equal("^1.0", result.RequireDev["phpstan/phpstan"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesAutoloadPsr4()
|
||||
{
|
||||
var manifest = @"{
|
||||
""name"": ""vendor/package"",
|
||||
""autoload"": {
|
||||
""psr-4"": {
|
||||
""Vendor\\Package\\"": ""src/""
|
||||
}
|
||||
}
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.json"), manifest);
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotEmpty(result.Autoload.Psr4);
|
||||
Assert.Contains("Vendor\\Package\\->src/", result.Autoload.Psr4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesAutoloadClassmap()
|
||||
{
|
||||
var manifest = @"{
|
||||
""name"": ""vendor/package"",
|
||||
""autoload"": {
|
||||
""classmap"": [""lib/"", ""src/Legacy/""]
|
||||
}
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.json"), manifest);
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.Autoload.Classmap.Count);
|
||||
Assert.Contains("lib/", result.Autoload.Classmap);
|
||||
Assert.Contains("src/Legacy/", result.Autoload.Classmap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesAutoloadFiles()
|
||||
{
|
||||
var manifest = @"{
|
||||
""name"": ""vendor/package"",
|
||||
""autoload"": {
|
||||
""files"": [""src/helpers.php"", ""src/functions.php""]
|
||||
}
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.json"), manifest);
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.Autoload.Files.Count);
|
||||
Assert.Contains("src/helpers.php", result.Autoload.Files);
|
||||
Assert.Contains("src/functions.php", result.Autoload.Files);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesScripts()
|
||||
{
|
||||
var manifest = @"{
|
||||
""name"": ""vendor/package"",
|
||||
""scripts"": {
|
||||
""test"": ""phpunit"",
|
||||
""lint"": ""phpstan analyse""
|
||||
}
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.json"), manifest);
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.Scripts.Count);
|
||||
Assert.Equal("phpunit", result.Scripts["test"]);
|
||||
Assert.Equal("phpstan analyse", result.Scripts["lint"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesScriptsArray()
|
||||
{
|
||||
var manifest = @"{
|
||||
""name"": ""vendor/package"",
|
||||
""scripts"": {
|
||||
""check"": [""phpstan analyse"", ""phpunit""]
|
||||
}
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.json"), manifest);
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result.Scripts);
|
||||
Assert.Contains("phpstan analyse", result.Scripts["check"]);
|
||||
Assert.Contains("phpunit", result.Scripts["check"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesBin()
|
||||
{
|
||||
var manifest = @"{
|
||||
""name"": ""vendor/package"",
|
||||
""bin"": [""bin/console"", ""bin/migrate""]
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.json"), manifest);
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.Bin.Count);
|
||||
Assert.Equal("bin/console", result.Bin["console"]);
|
||||
Assert.Equal("bin/migrate", result.Bin["migrate"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesMinimumStability()
|
||||
{
|
||||
var manifest = @"{
|
||||
""name"": ""vendor/package"",
|
||||
""minimum-stability"": ""dev""
|
||||
}";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.json"), manifest);
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("dev", result.MinimumStability);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ComputesSha256()
|
||||
{
|
||||
var manifest = @"{ ""name"": ""vendor/package"" }";
|
||||
await File.WriteAllTextAsync(Path.Combine(_testDir, "composer.json"), manifest);
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Sha256);
|
||||
Assert.Equal(64, result.Sha256.Length);
|
||||
Assert.True(result.Sha256.All(c => char.IsAsciiHexDigitLower(c)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_SetsManifestPath()
|
||||
{
|
||||
var manifest = @"{ ""name"": ""vendor/package"" }";
|
||||
var manifestPath = Path.Combine(_testDir, "composer.json");
|
||||
await File.WriteAllTextAsync(manifestPath, manifest);
|
||||
|
||||
var result = await PhpComposerManifestReader.LoadAsync(_testDir, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(manifestPath, result.ManifestPath);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpComposerManifest Tests
|
||||
|
||||
[Fact]
|
||||
public void RequiredPhpVersion_ReturnsPhpConstraint()
|
||||
{
|
||||
var manifest = new PhpComposerManifest(
|
||||
"/test/composer.json",
|
||||
"vendor/package",
|
||||
null, null, null, null,
|
||||
Array.Empty<string>(),
|
||||
new Dictionary<string, string> { { "php", ">=8.1" } },
|
||||
new Dictionary<string, string>(),
|
||||
ComposerAutoloadData.Empty,
|
||||
ComposerAutoloadData.Empty,
|
||||
new Dictionary<string, string>(),
|
||||
new Dictionary<string, string>(),
|
||||
null, null);
|
||||
|
||||
Assert.Equal(">=8.1", manifest.RequiredPhpVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequiredPhpVersion_ReturnsNullWhenNotSpecified()
|
||||
{
|
||||
var manifest = new PhpComposerManifest(
|
||||
"/test/composer.json",
|
||||
"vendor/package",
|
||||
null, null, null, null,
|
||||
Array.Empty<string>(),
|
||||
new Dictionary<string, string>(),
|
||||
new Dictionary<string, string>(),
|
||||
ComposerAutoloadData.Empty,
|
||||
ComposerAutoloadData.Empty,
|
||||
new Dictionary<string, string>(),
|
||||
new Dictionary<string, string>(),
|
||||
null, null);
|
||||
|
||||
Assert.Null(manifest.RequiredPhpVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequiredExtensions_ReturnsExtensionsList()
|
||||
{
|
||||
var manifest = new PhpComposerManifest(
|
||||
"/test/composer.json",
|
||||
"vendor/package",
|
||||
null, null, null, null,
|
||||
Array.Empty<string>(),
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "ext-json", "*" },
|
||||
{ "ext-mbstring", "*" },
|
||||
{ "ext-curl", "*" },
|
||||
{ "monolog/monolog", "^3.0" }
|
||||
},
|
||||
new Dictionary<string, string>(),
|
||||
ComposerAutoloadData.Empty,
|
||||
ComposerAutoloadData.Empty,
|
||||
new Dictionary<string, string>(),
|
||||
new Dictionary<string, string>(),
|
||||
null, null);
|
||||
|
||||
var extensions = manifest.RequiredExtensions.ToList();
|
||||
|
||||
Assert.Equal(3, extensions.Count);
|
||||
Assert.Contains("json", extensions);
|
||||
Assert.Contains("mbstring", extensions);
|
||||
Assert.Contains("curl", extensions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateMetadata_IncludesAllFields()
|
||||
{
|
||||
var manifest = new PhpComposerManifest(
|
||||
"/test/composer.json",
|
||||
"vendor/package",
|
||||
"Test package",
|
||||
"library",
|
||||
"1.0.0",
|
||||
"MIT",
|
||||
new[] { "Author" },
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "php", ">=8.1" },
|
||||
{ "ext-json", "*" },
|
||||
{ "monolog/monolog", "^3.0" }
|
||||
},
|
||||
new Dictionary<string, string> { { "phpunit/phpunit", "^10.0" } },
|
||||
ComposerAutoloadData.Empty,
|
||||
ComposerAutoloadData.Empty,
|
||||
new Dictionary<string, string>(),
|
||||
new Dictionary<string, string>(),
|
||||
null,
|
||||
"abc123def456");
|
||||
|
||||
var metadata = manifest.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("vendor/package", metadata["composer.manifest.name"]);
|
||||
Assert.Equal("library", metadata["composer.manifest.type"]);
|
||||
Assert.Equal("MIT", metadata["composer.manifest.license"]);
|
||||
Assert.Equal(">=8.1", metadata["composer.manifest.php_version"]);
|
||||
Assert.Equal("json", metadata["composer.manifest.extensions"]);
|
||||
Assert.Equal("3", metadata["composer.manifest.require_count"]);
|
||||
Assert.Equal("1", metadata["composer.manifest.require_dev_count"]);
|
||||
Assert.Equal("abc123def456", metadata["composer.manifest.sha256"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_HasNullValues()
|
||||
{
|
||||
var empty = PhpComposerManifest.Empty;
|
||||
|
||||
Assert.Equal(string.Empty, empty.ManifestPath);
|
||||
Assert.Null(empty.Name);
|
||||
Assert.Null(empty.Description);
|
||||
Assert.Null(empty.Type);
|
||||
Assert.Null(empty.Version);
|
||||
Assert.Null(empty.License);
|
||||
Assert.Empty(empty.Authors);
|
||||
Assert.Empty(empty.Require);
|
||||
Assert.Empty(empty.RequireDev);
|
||||
Assert.Null(empty.MinimumStability);
|
||||
Assert.Null(empty.Sha256);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ComposerAutoloadData Tests
|
||||
|
||||
[Fact]
|
||||
public void ComposerAutoloadData_Empty_HasEmptyCollections()
|
||||
{
|
||||
var empty = ComposerAutoloadData.Empty;
|
||||
|
||||
Assert.Empty(empty.Psr4);
|
||||
Assert.Empty(empty.Classmap);
|
||||
Assert.Empty(empty.Files);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Php.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Php.Tests.Internal;
|
||||
|
||||
public sealed class PhpExtensionScannerTests
|
||||
{
|
||||
#region PhpExtension Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpExtension_RecordProperties_SetCorrectly()
|
||||
{
|
||||
var extension = new PhpExtension(
|
||||
"pdo_mysql",
|
||||
"8.2.0",
|
||||
"/usr/lib/php/extensions/pdo_mysql.so",
|
||||
PhpExtensionSource.PhpIni,
|
||||
false,
|
||||
PhpExtensionCategory.Database);
|
||||
|
||||
Assert.Equal("pdo_mysql", extension.Name);
|
||||
Assert.Equal("8.2.0", extension.Version);
|
||||
Assert.Equal("/usr/lib/php/extensions/pdo_mysql.so", extension.LibraryPath);
|
||||
Assert.Equal(PhpExtensionSource.PhpIni, extension.Source);
|
||||
Assert.False(extension.IsBundled);
|
||||
Assert.Equal(PhpExtensionCategory.Database, extension.Category);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpExtension_CreateMetadata_IncludesAllFields()
|
||||
{
|
||||
var extension = new PhpExtension(
|
||||
"openssl",
|
||||
"3.0.0",
|
||||
"/usr/lib/php/openssl.so",
|
||||
PhpExtensionSource.ConfD,
|
||||
false,
|
||||
PhpExtensionCategory.Crypto);
|
||||
|
||||
var metadata = extension.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("openssl", metadata["extension.name"]);
|
||||
Assert.Equal("3.0.0", metadata["extension.version"]);
|
||||
Assert.Equal("/usr/lib/php/openssl.so", metadata["extension.library"]);
|
||||
Assert.Equal("confd", metadata["extension.source"]);
|
||||
Assert.Equal("false", metadata["extension.bundled"]);
|
||||
Assert.Equal("crypto", metadata["extension.category"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpExtension_BundledExtension_MarkedCorrectly()
|
||||
{
|
||||
var extension = new PhpExtension(
|
||||
"json",
|
||||
null,
|
||||
null,
|
||||
PhpExtensionSource.Bundled,
|
||||
true,
|
||||
PhpExtensionCategory.Core);
|
||||
|
||||
Assert.True(extension.IsBundled);
|
||||
Assert.Equal(PhpExtensionSource.Bundled, extension.Source);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpExtensionSource Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpExtensionSource_HasExpectedValues()
|
||||
{
|
||||
Assert.Equal(0, (int)PhpExtensionSource.PhpIni);
|
||||
Assert.Equal(1, (int)PhpExtensionSource.ConfD);
|
||||
Assert.Equal(2, (int)PhpExtensionSource.Bundled);
|
||||
Assert.Equal(3, (int)PhpExtensionSource.Container);
|
||||
Assert.Equal(4, (int)PhpExtensionSource.UsageDetected);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpExtensionCategory Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpExtensionCategory_HasExpectedValues()
|
||||
{
|
||||
Assert.Equal(0, (int)PhpExtensionCategory.Core);
|
||||
Assert.Equal(1, (int)PhpExtensionCategory.Database);
|
||||
Assert.Equal(2, (int)PhpExtensionCategory.Crypto);
|
||||
Assert.Equal(3, (int)PhpExtensionCategory.Image);
|
||||
Assert.Equal(4, (int)PhpExtensionCategory.Compression);
|
||||
Assert.Equal(5, (int)PhpExtensionCategory.Xml);
|
||||
Assert.Equal(6, (int)PhpExtensionCategory.Cache);
|
||||
Assert.Equal(7, (int)PhpExtensionCategory.Debug);
|
||||
Assert.Equal(8, (int)PhpExtensionCategory.Network);
|
||||
Assert.Equal(9, (int)PhpExtensionCategory.Text);
|
||||
Assert.Equal(10, (int)PhpExtensionCategory.Other);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpEnvironmentSettings Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpEnvironmentSettings_Empty_HasDefaults()
|
||||
{
|
||||
var settings = PhpEnvironmentSettings.Empty;
|
||||
|
||||
Assert.Empty(settings.Extensions);
|
||||
Assert.NotNull(settings.Security);
|
||||
Assert.NotNull(settings.Upload);
|
||||
Assert.NotNull(settings.Session);
|
||||
Assert.NotNull(settings.Error);
|
||||
Assert.NotNull(settings.Limits);
|
||||
Assert.Empty(settings.WebServerSettings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpEnvironmentSettings_HasSettings_TrueWithExtensions()
|
||||
{
|
||||
var extensions = new[] { new PhpExtension("pdo", null, null, PhpExtensionSource.PhpIni, false, PhpExtensionCategory.Database) };
|
||||
var settings = new PhpEnvironmentSettings(
|
||||
extensions,
|
||||
PhpSecuritySettings.Default,
|
||||
PhpUploadSettings.Default,
|
||||
PhpSessionSettings.Default,
|
||||
PhpErrorSettings.Default,
|
||||
PhpResourceLimits.Default,
|
||||
new Dictionary<string, string>());
|
||||
|
||||
Assert.True(settings.HasSettings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpEnvironmentSettings_CreateMetadata_IncludesExtensionCount()
|
||||
{
|
||||
var extensions = new[]
|
||||
{
|
||||
new PhpExtension("pdo", null, null, PhpExtensionSource.PhpIni, false, PhpExtensionCategory.Database),
|
||||
new PhpExtension("openssl", null, null, PhpExtensionSource.PhpIni, false, PhpExtensionCategory.Crypto),
|
||||
new PhpExtension("gd", null, null, PhpExtensionSource.PhpIni, false, PhpExtensionCategory.Image)
|
||||
};
|
||||
|
||||
var settings = new PhpEnvironmentSettings(
|
||||
extensions,
|
||||
PhpSecuritySettings.Default,
|
||||
PhpUploadSettings.Default,
|
||||
PhpSessionSettings.Default,
|
||||
PhpErrorSettings.Default,
|
||||
PhpResourceLimits.Default,
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var metadata = settings.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("3", metadata["env.extension_count"]);
|
||||
Assert.Equal("1", metadata["env.extensions_database"]);
|
||||
Assert.Equal("1", metadata["env.extensions_crypto"]);
|
||||
Assert.Equal("1", metadata["env.extensions_image"]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpSecuritySettings Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpSecuritySettings_Default_HasExpectedValues()
|
||||
{
|
||||
var security = PhpSecuritySettings.Default;
|
||||
|
||||
Assert.Empty(security.DisabledFunctions);
|
||||
Assert.Empty(security.DisabledClasses);
|
||||
Assert.False(security.OpenBasedir);
|
||||
Assert.Null(security.OpenBasedirValue);
|
||||
Assert.True(security.AllowUrlFopen);
|
||||
Assert.False(security.AllowUrlInclude);
|
||||
Assert.True(security.ExposePhp);
|
||||
Assert.False(security.RegisterGlobals);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpSecuritySettings_CreateMetadata_IncludesDisabledFunctions()
|
||||
{
|
||||
var security = new PhpSecuritySettings(
|
||||
new[] { "exec", "shell_exec", "system", "passthru" },
|
||||
new[] { "Directory" },
|
||||
true,
|
||||
"/var/www",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false);
|
||||
|
||||
var metadata = security.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("4", metadata["security.disabled_functions_count"]);
|
||||
Assert.Contains("exec", metadata["security.disabled_functions"]);
|
||||
Assert.Contains("shell_exec", metadata["security.disabled_functions"]);
|
||||
Assert.Equal("1", metadata["security.disabled_classes_count"]);
|
||||
Assert.Equal("true", metadata["security.open_basedir"]);
|
||||
Assert.Equal("false", metadata["security.allow_url_fopen"]);
|
||||
Assert.Equal("false", metadata["security.allow_url_include"]);
|
||||
Assert.Equal("false", metadata["security.expose_php"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpSecuritySettings_DangerousConfiguration_Detectable()
|
||||
{
|
||||
var security = new PhpSecuritySettings(
|
||||
Array.Empty<string>(),
|
||||
Array.Empty<string>(),
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
true, // allow_url_include is dangerous!
|
||||
true,
|
||||
false);
|
||||
|
||||
Assert.True(security.AllowUrlInclude);
|
||||
Assert.True(security.AllowUrlFopen);
|
||||
Assert.False(security.OpenBasedir);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpUploadSettings Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpUploadSettings_Default_HasExpectedValues()
|
||||
{
|
||||
var upload = PhpUploadSettings.Default;
|
||||
|
||||
Assert.True(upload.FileUploads);
|
||||
Assert.Equal("2M", upload.MaxFileSize);
|
||||
Assert.Equal("8M", upload.MaxPostSize);
|
||||
Assert.Equal(20, upload.MaxFileUploads);
|
||||
Assert.Null(upload.UploadTmpDir);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpUploadSettings_CreateMetadata_IncludesAllFields()
|
||||
{
|
||||
var upload = new PhpUploadSettings(
|
||||
true,
|
||||
"64M",
|
||||
"128M",
|
||||
50,
|
||||
"/tmp/uploads");
|
||||
|
||||
var metadata = upload.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("true", metadata["upload.enabled"]);
|
||||
Assert.Equal("64M", metadata["upload.max_file_size"]);
|
||||
Assert.Equal("128M", metadata["upload.max_post_size"]);
|
||||
Assert.Equal("50", metadata["upload.max_files"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpUploadSettings_DisabledUploads()
|
||||
{
|
||||
var upload = new PhpUploadSettings(false, null, null, 0, null);
|
||||
|
||||
Assert.False(upload.FileUploads);
|
||||
Assert.Equal(0, upload.MaxFileUploads);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpSessionSettings Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpSessionSettings_Default_HasExpectedValues()
|
||||
{
|
||||
var session = PhpSessionSettings.Default;
|
||||
|
||||
Assert.Equal("files", session.SaveHandler);
|
||||
Assert.Null(session.SavePath);
|
||||
Assert.False(session.CookieHttponly);
|
||||
Assert.False(session.CookieSecure);
|
||||
Assert.Null(session.CookieSamesite);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpSessionSettings_CreateMetadata_IncludesAllFields()
|
||||
{
|
||||
var session = new PhpSessionSettings(
|
||||
"redis",
|
||||
"tcp://localhost:6379",
|
||||
true,
|
||||
true,
|
||||
"Strict");
|
||||
|
||||
var metadata = session.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("redis", metadata["session.save_handler"]);
|
||||
Assert.Equal("true", metadata["session.cookie_httponly"]);
|
||||
Assert.Equal("true", metadata["session.cookie_secure"]);
|
||||
Assert.Equal("Strict", metadata["session.cookie_samesite"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpSessionSettings_SecureConfiguration()
|
||||
{
|
||||
var session = new PhpSessionSettings(
|
||||
"files",
|
||||
"/var/lib/php/sessions",
|
||||
true,
|
||||
true,
|
||||
"Lax");
|
||||
|
||||
Assert.True(session.CookieHttponly);
|
||||
Assert.True(session.CookieSecure);
|
||||
Assert.Equal("Lax", session.CookieSamesite);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpErrorSettings Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpErrorSettings_Default_HasExpectedValues()
|
||||
{
|
||||
var error = PhpErrorSettings.Default;
|
||||
|
||||
Assert.False(error.DisplayErrors);
|
||||
Assert.False(error.DisplayStartupErrors);
|
||||
Assert.True(error.LogErrors);
|
||||
Assert.Equal("E_ALL", error.ErrorReporting);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpErrorSettings_CreateMetadata_IncludesAllFields()
|
||||
{
|
||||
var error = new PhpErrorSettings(
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
"E_ALL & ~E_NOTICE");
|
||||
|
||||
var metadata = error.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("true", metadata["error.display_errors"]);
|
||||
Assert.Equal("true", metadata["error.display_startup_errors"]);
|
||||
Assert.Equal("false", metadata["error.log_errors"]);
|
||||
Assert.Equal("E_ALL & ~E_NOTICE", metadata["error.error_reporting"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpErrorSettings_ProductionConfiguration()
|
||||
{
|
||||
var error = new PhpErrorSettings(false, false, true, "E_ALL");
|
||||
|
||||
Assert.False(error.DisplayErrors);
|
||||
Assert.False(error.DisplayStartupErrors);
|
||||
Assert.True(error.LogErrors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpErrorSettings_DevelopmentConfiguration()
|
||||
{
|
||||
var error = new PhpErrorSettings(true, true, true, "E_ALL");
|
||||
|
||||
Assert.True(error.DisplayErrors);
|
||||
Assert.True(error.DisplayStartupErrors);
|
||||
Assert.True(error.LogErrors);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpResourceLimits Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpResourceLimits_Default_HasExpectedValues()
|
||||
{
|
||||
var limits = PhpResourceLimits.Default;
|
||||
|
||||
Assert.Equal("128M", limits.MemoryLimit);
|
||||
Assert.Equal(30, limits.MaxExecutionTime);
|
||||
Assert.Equal(60, limits.MaxInputTime);
|
||||
Assert.Equal("1000", limits.MaxInputVars);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpResourceLimits_CreateMetadata_IncludesAllFields()
|
||||
{
|
||||
var limits = new PhpResourceLimits(
|
||||
"512M",
|
||||
120,
|
||||
180,
|
||||
"5000");
|
||||
|
||||
var metadata = limits.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("512M", metadata["limits.memory_limit"]);
|
||||
Assert.Equal("120", metadata["limits.max_execution_time"]);
|
||||
Assert.Equal("180", metadata["limits.max_input_time"]);
|
||||
Assert.Equal("5000", metadata["limits.max_input_vars"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpResourceLimits_HighPerformanceConfiguration()
|
||||
{
|
||||
var limits = new PhpResourceLimits("2G", 300, 300, "10000");
|
||||
|
||||
Assert.Equal("2G", limits.MemoryLimit);
|
||||
Assert.Equal(300, limits.MaxExecutionTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpResourceLimits_RestrictedConfiguration()
|
||||
{
|
||||
var limits = new PhpResourceLimits("64M", 10, 10, "500");
|
||||
|
||||
Assert.Equal("64M", limits.MemoryLimit);
|
||||
Assert.Equal(10, limits.MaxExecutionTime);
|
||||
Assert.Equal(10, limits.MaxInputTime);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Php.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Php.Tests.Internal;
|
||||
|
||||
public sealed class PhpFrameworkSurfaceScannerTests
|
||||
{
|
||||
#region PhpFrameworkSurface Tests
|
||||
|
||||
[Fact]
|
||||
public void Empty_ReturnsEmptySurface()
|
||||
{
|
||||
var surface = PhpFrameworkSurface.Empty;
|
||||
|
||||
Assert.Empty(surface.Routes);
|
||||
Assert.Empty(surface.Controllers);
|
||||
Assert.Empty(surface.Middlewares);
|
||||
Assert.Empty(surface.CliCommands);
|
||||
Assert.Empty(surface.CronJobs);
|
||||
Assert.Empty(surface.EventListeners);
|
||||
Assert.False(surface.HasSurface);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasSurface_TrueWhenRoutesPresent()
|
||||
{
|
||||
var routes = new[] { CreateRoute("/api/users", "GET") };
|
||||
var surface = new PhpFrameworkSurface(
|
||||
routes,
|
||||
Array.Empty<PhpController>(),
|
||||
Array.Empty<PhpMiddleware>(),
|
||||
Array.Empty<PhpCliCommand>(),
|
||||
Array.Empty<PhpCronJob>(),
|
||||
Array.Empty<PhpEventListener>());
|
||||
|
||||
Assert.True(surface.HasSurface);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasSurface_TrueWhenControllersPresent()
|
||||
{
|
||||
var controllers = new[] { new PhpController("UserController", "App\\Http\\Controllers", "app/Http/Controllers/UserController.php", new[] { "index", "show" }, true) };
|
||||
var surface = new PhpFrameworkSurface(
|
||||
Array.Empty<PhpRoute>(),
|
||||
controllers,
|
||||
Array.Empty<PhpMiddleware>(),
|
||||
Array.Empty<PhpCliCommand>(),
|
||||
Array.Empty<PhpCronJob>(),
|
||||
Array.Empty<PhpEventListener>());
|
||||
|
||||
Assert.True(surface.HasSurface);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasSurface_TrueWhenMiddlewaresPresent()
|
||||
{
|
||||
var middlewares = new[] { new PhpMiddleware("AuthMiddleware", "App\\Http\\Middleware", "app/Http/Middleware/AuthMiddleware.php", PhpMiddlewareKind.Auth) };
|
||||
var surface = new PhpFrameworkSurface(
|
||||
Array.Empty<PhpRoute>(),
|
||||
Array.Empty<PhpController>(),
|
||||
middlewares,
|
||||
Array.Empty<PhpCliCommand>(),
|
||||
Array.Empty<PhpCronJob>(),
|
||||
Array.Empty<PhpEventListener>());
|
||||
|
||||
Assert.True(surface.HasSurface);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasSurface_TrueWhenCliCommandsPresent()
|
||||
{
|
||||
var commands = new[] { new PhpCliCommand("app:sync", "Sync data", "SyncCommand", "app/Console/Commands/SyncCommand.php") };
|
||||
var surface = new PhpFrameworkSurface(
|
||||
Array.Empty<PhpRoute>(),
|
||||
Array.Empty<PhpController>(),
|
||||
Array.Empty<PhpMiddleware>(),
|
||||
commands,
|
||||
Array.Empty<PhpCronJob>(),
|
||||
Array.Empty<PhpEventListener>());
|
||||
|
||||
Assert.True(surface.HasSurface);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasSurface_TrueWhenCronJobsPresent()
|
||||
{
|
||||
var cronJobs = new[] { new PhpCronJob("hourly", "ReportCommand", "Generate hourly report", "app/Console/Kernel.php") };
|
||||
var surface = new PhpFrameworkSurface(
|
||||
Array.Empty<PhpRoute>(),
|
||||
Array.Empty<PhpController>(),
|
||||
Array.Empty<PhpMiddleware>(),
|
||||
Array.Empty<PhpCliCommand>(),
|
||||
cronJobs,
|
||||
Array.Empty<PhpEventListener>());
|
||||
|
||||
Assert.True(surface.HasSurface);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasSurface_TrueWhenEventListenersPresent()
|
||||
{
|
||||
var listeners = new[] { new PhpEventListener("UserRegistered", "SendWelcomeEmail", 0, "app/Providers/EventServiceProvider.php") };
|
||||
var surface = new PhpFrameworkSurface(
|
||||
Array.Empty<PhpRoute>(),
|
||||
Array.Empty<PhpController>(),
|
||||
Array.Empty<PhpMiddleware>(),
|
||||
Array.Empty<PhpCliCommand>(),
|
||||
Array.Empty<PhpCronJob>(),
|
||||
listeners);
|
||||
|
||||
Assert.True(surface.HasSurface);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateMetadata_IncludesAllCounts()
|
||||
{
|
||||
var routes = new[] { CreateRoute("/api/users", "GET"), CreateRoute("/api/users/{id}", "GET", true) };
|
||||
var controllers = new[] { new PhpController("UserController", null, "UserController.php", Array.Empty<string>(), false) };
|
||||
var middlewares = new[] { new PhpMiddleware("AuthMiddleware", null, "AuthMiddleware.php", PhpMiddlewareKind.Auth) };
|
||||
var commands = new[] { new PhpCliCommand("app:sync", null, "SyncCommand", "SyncCommand.php") };
|
||||
var cronJobs = new[] { new PhpCronJob("hourly", "Report", null, "Kernel.php") };
|
||||
var listeners = new[] { new PhpEventListener("Event", "Handler", 0, "Provider.php") };
|
||||
|
||||
var surface = new PhpFrameworkSurface(routes, controllers, middlewares, commands, cronJobs, listeners);
|
||||
var metadata = surface.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("2", metadata["surface.route_count"]);
|
||||
Assert.Equal("1", metadata["surface.controller_count"]);
|
||||
Assert.Equal("1", metadata["surface.middleware_count"]);
|
||||
Assert.Equal("1", metadata["surface.cli_command_count"]);
|
||||
Assert.Equal("1", metadata["surface.cron_job_count"]);
|
||||
Assert.Equal("1", metadata["surface.event_listener_count"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateMetadata_IncludesHttpMethods()
|
||||
{
|
||||
var routes = new[]
|
||||
{
|
||||
CreateRoute("/users", "GET"),
|
||||
CreateRoute("/users", "POST"),
|
||||
CreateRoute("/users/{id}", "PUT"),
|
||||
CreateRoute("/users/{id}", "DELETE")
|
||||
};
|
||||
|
||||
var surface = new PhpFrameworkSurface(
|
||||
routes,
|
||||
Array.Empty<PhpController>(),
|
||||
Array.Empty<PhpMiddleware>(),
|
||||
Array.Empty<PhpCliCommand>(),
|
||||
Array.Empty<PhpCronJob>(),
|
||||
Array.Empty<PhpEventListener>());
|
||||
|
||||
var metadata = surface.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Contains("GET", metadata["surface.http_methods"]);
|
||||
Assert.Contains("POST", metadata["surface.http_methods"]);
|
||||
Assert.Contains("PUT", metadata["surface.http_methods"]);
|
||||
Assert.Contains("DELETE", metadata["surface.http_methods"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateMetadata_CountsProtectedAndPublicRoutes()
|
||||
{
|
||||
var routes = new[]
|
||||
{
|
||||
CreateRoute("/public", "GET", requiresAuth: false),
|
||||
CreateRoute("/api/users", "GET", requiresAuth: true),
|
||||
CreateRoute("/api/admin", "GET", requiresAuth: true)
|
||||
};
|
||||
|
||||
var surface = new PhpFrameworkSurface(
|
||||
routes,
|
||||
Array.Empty<PhpController>(),
|
||||
Array.Empty<PhpMiddleware>(),
|
||||
Array.Empty<PhpCliCommand>(),
|
||||
Array.Empty<PhpCronJob>(),
|
||||
Array.Empty<PhpEventListener>());
|
||||
|
||||
var metadata = surface.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("2", metadata["surface.protected_routes"]);
|
||||
Assert.Equal("1", metadata["surface.public_routes"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateMetadata_IncludesRoutePatterns()
|
||||
{
|
||||
var routes = new[]
|
||||
{
|
||||
CreateRoute("/api/v1/users", "GET"),
|
||||
CreateRoute("/api/v1/posts", "GET"),
|
||||
CreateRoute("/api/v1/comments", "GET")
|
||||
};
|
||||
|
||||
var surface = new PhpFrameworkSurface(
|
||||
routes,
|
||||
Array.Empty<PhpController>(),
|
||||
Array.Empty<PhpMiddleware>(),
|
||||
Array.Empty<PhpCliCommand>(),
|
||||
Array.Empty<PhpCronJob>(),
|
||||
Array.Empty<PhpEventListener>());
|
||||
|
||||
var metadata = surface.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.True(metadata.ContainsKey("surface.route_patterns"));
|
||||
Assert.Contains("/api/v1/users", metadata["surface.route_patterns"]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpRoute Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpRoute_RecordProperties_SetCorrectly()
|
||||
{
|
||||
var route = new PhpRoute(
|
||||
"/api/users/{id}",
|
||||
new[] { "GET", "HEAD" },
|
||||
"UserController",
|
||||
"show",
|
||||
"users.show",
|
||||
true,
|
||||
new[] { "auth", "throttle" },
|
||||
"routes/api.php",
|
||||
42);
|
||||
|
||||
Assert.Equal("/api/users/{id}", route.Pattern);
|
||||
Assert.Equal(2, route.Methods.Count);
|
||||
Assert.Contains("GET", route.Methods);
|
||||
Assert.Contains("HEAD", route.Methods);
|
||||
Assert.Equal("UserController", route.Controller);
|
||||
Assert.Equal("show", route.Action);
|
||||
Assert.Equal("users.show", route.Name);
|
||||
Assert.True(route.RequiresAuth);
|
||||
Assert.Equal(2, route.Middlewares.Count);
|
||||
Assert.Equal("routes/api.php", route.SourceFile);
|
||||
Assert.Equal(42, route.SourceLine);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpController Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpController_RecordProperties_SetCorrectly()
|
||||
{
|
||||
var controller = new PhpController(
|
||||
"UserController",
|
||||
"App\\Http\\Controllers",
|
||||
"app/Http/Controllers/UserController.php",
|
||||
new[] { "index", "show", "store", "update", "destroy" },
|
||||
true);
|
||||
|
||||
Assert.Equal("UserController", controller.ClassName);
|
||||
Assert.Equal("App\\Http\\Controllers", controller.Namespace);
|
||||
Assert.Equal("app/Http/Controllers/UserController.php", controller.SourceFile);
|
||||
Assert.Equal(5, controller.Actions.Count);
|
||||
Assert.True(controller.IsApiController);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpController_IsApiController_FalseForWebController()
|
||||
{
|
||||
var controller = new PhpController(
|
||||
"HomeController",
|
||||
"App\\Http\\Controllers",
|
||||
"app/Http/Controllers/HomeController.php",
|
||||
new[] { "index", "about" },
|
||||
false);
|
||||
|
||||
Assert.False(controller.IsApiController);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpMiddleware Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpMiddleware_RecordProperties_SetCorrectly()
|
||||
{
|
||||
var middleware = new PhpMiddleware(
|
||||
"AuthenticateMiddleware",
|
||||
"App\\Http\\Middleware",
|
||||
"app/Http/Middleware/AuthenticateMiddleware.php",
|
||||
PhpMiddlewareKind.Auth);
|
||||
|
||||
Assert.Equal("AuthenticateMiddleware", middleware.ClassName);
|
||||
Assert.Equal("App\\Http\\Middleware", middleware.Namespace);
|
||||
Assert.Equal("app/Http/Middleware/AuthenticateMiddleware.php", middleware.SourceFile);
|
||||
Assert.Equal(PhpMiddlewareKind.Auth, middleware.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpMiddlewareKind_HasExpectedValues()
|
||||
{
|
||||
Assert.Equal(0, (int)PhpMiddlewareKind.General);
|
||||
Assert.Equal(1, (int)PhpMiddlewareKind.Auth);
|
||||
Assert.Equal(2, (int)PhpMiddlewareKind.Cors);
|
||||
Assert.Equal(3, (int)PhpMiddlewareKind.RateLimit);
|
||||
Assert.Equal(4, (int)PhpMiddlewareKind.Logging);
|
||||
Assert.Equal(5, (int)PhpMiddlewareKind.Security);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpCliCommand Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpCliCommand_RecordProperties_SetCorrectly()
|
||||
{
|
||||
var command = new PhpCliCommand(
|
||||
"app:import-data",
|
||||
"Import data from external source",
|
||||
"ImportDataCommand",
|
||||
"app/Console/Commands/ImportDataCommand.php");
|
||||
|
||||
Assert.Equal("app:import-data", command.Name);
|
||||
Assert.Equal("Import data from external source", command.Description);
|
||||
Assert.Equal("ImportDataCommand", command.ClassName);
|
||||
Assert.Equal("app/Console/Commands/ImportDataCommand.php", command.SourceFile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpCliCommand_NullDescription_Allowed()
|
||||
{
|
||||
var command = new PhpCliCommand(
|
||||
"app:sync",
|
||||
null,
|
||||
"SyncCommand",
|
||||
"SyncCommand.php");
|
||||
|
||||
Assert.Null(command.Description);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpCronJob Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpCronJob_RecordProperties_SetCorrectly()
|
||||
{
|
||||
var cronJob = new PhpCronJob(
|
||||
"daily",
|
||||
"CleanupOldData",
|
||||
"Remove data older than 30 days",
|
||||
"app/Console/Kernel.php");
|
||||
|
||||
Assert.Equal("daily", cronJob.Schedule);
|
||||
Assert.Equal("CleanupOldData", cronJob.Handler);
|
||||
Assert.Equal("Remove data older than 30 days", cronJob.Description);
|
||||
Assert.Equal("app/Console/Kernel.php", cronJob.SourceFile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpCronJob_VariousSchedules()
|
||||
{
|
||||
var jobs = new[]
|
||||
{
|
||||
new PhpCronJob("hourly", "HourlyJob", null, "Kernel.php"),
|
||||
new PhpCronJob("daily", "DailyJob", null, "Kernel.php"),
|
||||
new PhpCronJob("weekly", "WeeklyJob", null, "Kernel.php"),
|
||||
new PhpCronJob("monthly", "MonthlyJob", null, "Kernel.php"),
|
||||
new PhpCronJob("everyMinute", "MinuteJob", null, "Kernel.php"),
|
||||
new PhpCronJob("everyFiveMinutes", "FiveMinJob", null, "Kernel.php")
|
||||
};
|
||||
|
||||
Assert.Equal(6, jobs.Length);
|
||||
Assert.All(jobs, j => Assert.NotNull(j.Schedule));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpEventListener Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpEventListener_RecordProperties_SetCorrectly()
|
||||
{
|
||||
var listener = new PhpEventListener(
|
||||
"App\\Events\\UserRegistered",
|
||||
"App\\Listeners\\SendWelcomeEmail",
|
||||
10,
|
||||
"app/Providers/EventServiceProvider.php");
|
||||
|
||||
Assert.Equal("App\\Events\\UserRegistered", listener.EventName);
|
||||
Assert.Equal("App\\Listeners\\SendWelcomeEmail", listener.Handler);
|
||||
Assert.Equal(10, listener.Priority);
|
||||
Assert.Equal("app/Providers/EventServiceProvider.php", listener.SourceFile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpEventListener_DefaultPriority()
|
||||
{
|
||||
var listener = new PhpEventListener(
|
||||
"EventName",
|
||||
"Handler",
|
||||
0,
|
||||
"Provider.php");
|
||||
|
||||
Assert.Equal(0, listener.Priority);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static PhpRoute CreateRoute(string pattern, string method, bool requiresAuth = false)
|
||||
{
|
||||
return new PhpRoute(
|
||||
pattern,
|
||||
new[] { method },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
requiresAuth,
|
||||
Array.Empty<string>(),
|
||||
"routes/web.php",
|
||||
1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
using System.Text;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Php.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Php.Tests.Internal;
|
||||
|
||||
public sealed class PhpPharScannerTests : IDisposable
|
||||
{
|
||||
private readonly string _testDir;
|
||||
|
||||
public PhpPharScannerTests()
|
||||
{
|
||||
_testDir = Path.Combine(Path.GetTempPath(), $"phar-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_testDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_testDir))
|
||||
{
|
||||
Directory.Delete(_testDir, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
#region PhpPharScanner Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ScanFileAsync_NonExistentFile_ReturnsNull()
|
||||
{
|
||||
var result = await PhpPharScanner.ScanFileAsync(
|
||||
Path.Combine(_testDir, "nonexistent.phar"),
|
||||
"nonexistent.phar",
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ScanFileAsync_NullPath_ReturnsNull()
|
||||
{
|
||||
var result = await PhpPharScanner.ScanFileAsync(null!, "test.phar", CancellationToken.None);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ScanFileAsync_EmptyPath_ReturnsNull()
|
||||
{
|
||||
var result = await PhpPharScanner.ScanFileAsync("", "test.phar", CancellationToken.None);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ScanFileAsync_InvalidPharFile_ReturnsNull()
|
||||
{
|
||||
var filePath = Path.Combine(_testDir, "invalid.phar");
|
||||
await File.WriteAllTextAsync(filePath, "This is not a valid PHAR file");
|
||||
|
||||
var result = await PhpPharScanner.ScanFileAsync(filePath, "invalid.phar", CancellationToken.None);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ScanFileAsync_MinimalPhar_ParsesStub()
|
||||
{
|
||||
// Create a minimal PHAR structure with __HALT_COMPILER();
|
||||
var stub = "<?php\necho 'Hello';\n__HALT_COMPILER();";
|
||||
var pharContent = CreateMinimalPharBytes(stub);
|
||||
var filePath = Path.Combine(_testDir, "minimal.phar");
|
||||
await File.WriteAllBytesAsync(filePath, pharContent);
|
||||
|
||||
var result = await PhpPharScanner.ScanFileAsync(filePath, "minimal.phar", CancellationToken.None);
|
||||
|
||||
// May return null if manifest parsing fails, but should not throw
|
||||
// The minimal PHAR may not have a valid manifest
|
||||
if (result is not null)
|
||||
{
|
||||
Assert.Contains("__HALT_COMPILER();", result.Stub);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ScanFileAsync_ComputesSha256()
|
||||
{
|
||||
var stub = "<?php\n__HALT_COMPILER();";
|
||||
var pharContent = CreateMinimalPharBytes(stub);
|
||||
var filePath = Path.Combine(_testDir, "hash.phar");
|
||||
await File.WriteAllBytesAsync(filePath, pharContent);
|
||||
|
||||
var result = await PhpPharScanner.ScanFileAsync(filePath, "hash.phar", CancellationToken.None);
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
Assert.NotNull(result.Sha256);
|
||||
Assert.Equal(64, result.Sha256.Length);
|
||||
Assert.True(result.Sha256.All(c => char.IsAsciiHexDigitLower(c)));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpPharArchive Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpPharArchive_Constructor_NormalizesBackslashes()
|
||||
{
|
||||
var archive = new PhpPharArchive(
|
||||
@"C:\path\to\file.phar",
|
||||
@"vendor\file.phar",
|
||||
null,
|
||||
null,
|
||||
Array.Empty<PhpPharEntry>(),
|
||||
null);
|
||||
|
||||
Assert.Equal("C:/path/to/file.phar", archive.FilePath);
|
||||
Assert.Equal("vendor/file.phar", archive.RelativePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharArchive_Constructor_RequiresFilePath()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new PhpPharArchive(
|
||||
"",
|
||||
"test.phar",
|
||||
null,
|
||||
null,
|
||||
Array.Empty<PhpPharEntry>(),
|
||||
null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharArchive_HasEmbeddedVendor_TrueForVendorPath()
|
||||
{
|
||||
var entries = new[]
|
||||
{
|
||||
new PhpPharEntry("vendor/autoload.php", 100, 80, 0, 0, PhpPharCompression.None, null),
|
||||
new PhpPharEntry("src/Main.php", 200, 150, 0, 0, PhpPharCompression.None, null)
|
||||
};
|
||||
|
||||
var archive = new PhpPharArchive("/test.phar", "test.phar", null, null, entries, null);
|
||||
|
||||
Assert.True(archive.HasEmbeddedVendor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharArchive_HasEmbeddedVendor_FalseWithoutVendor()
|
||||
{
|
||||
var entries = new[]
|
||||
{
|
||||
new PhpPharEntry("src/Main.php", 200, 150, 0, 0, PhpPharCompression.None, null),
|
||||
new PhpPharEntry("lib/Helper.php", 100, 80, 0, 0, PhpPharCompression.None, null)
|
||||
};
|
||||
|
||||
var archive = new PhpPharArchive("/test.phar", "test.phar", null, null, entries, null);
|
||||
|
||||
Assert.False(archive.HasEmbeddedVendor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharArchive_HasComposerFiles_TrueForComposerJson()
|
||||
{
|
||||
var entries = new[]
|
||||
{
|
||||
new PhpPharEntry("composer.json", 500, 400, 0, 0, PhpPharCompression.None, null),
|
||||
new PhpPharEntry("src/Main.php", 200, 150, 0, 0, PhpPharCompression.None, null)
|
||||
};
|
||||
|
||||
var archive = new PhpPharArchive("/test.phar", "test.phar", null, null, entries, null);
|
||||
|
||||
Assert.True(archive.HasComposerFiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharArchive_HasComposerFiles_TrueForComposerLock()
|
||||
{
|
||||
var entries = new[]
|
||||
{
|
||||
new PhpPharEntry("composer.lock", 5000, 4000, 0, 0, PhpPharCompression.None, null),
|
||||
new PhpPharEntry("src/Main.php", 200, 150, 0, 0, PhpPharCompression.None, null)
|
||||
};
|
||||
|
||||
var archive = new PhpPharArchive("/test.phar", "test.phar", null, null, entries, null);
|
||||
|
||||
Assert.True(archive.HasComposerFiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharArchive_FileCount_ReturnsCorrectCount()
|
||||
{
|
||||
var entries = new[]
|
||||
{
|
||||
new PhpPharEntry("file1.php", 100, 80, 0, 0, PhpPharCompression.None, null),
|
||||
new PhpPharEntry("file2.php", 200, 150, 0, 0, PhpPharCompression.None, null),
|
||||
new PhpPharEntry("file3.php", 300, 250, 0, 0, PhpPharCompression.None, null)
|
||||
};
|
||||
|
||||
var archive = new PhpPharArchive("/test.phar", "test.phar", null, null, entries, null);
|
||||
|
||||
Assert.Equal(3, archive.FileCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharArchive_TotalUncompressedSize_SumsCorrectly()
|
||||
{
|
||||
var entries = new[]
|
||||
{
|
||||
new PhpPharEntry("file1.php", 100, 80, 0, 0, PhpPharCompression.None, null),
|
||||
new PhpPharEntry("file2.php", 200, 150, 0, 0, PhpPharCompression.None, null),
|
||||
new PhpPharEntry("file3.php", 300, 250, 0, 0, PhpPharCompression.None, null)
|
||||
};
|
||||
|
||||
var archive = new PhpPharArchive("/test.phar", "test.phar", null, null, entries, null);
|
||||
|
||||
Assert.Equal(600, archive.TotalUncompressedSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharArchive_CreateMetadata_IncludesBasicInfo()
|
||||
{
|
||||
var entries = new[]
|
||||
{
|
||||
new PhpPharEntry("vendor/autoload.php", 100, 80, 0, 0, PhpPharCompression.None, null),
|
||||
new PhpPharEntry("composer.json", 200, 150, 0, 0, PhpPharCompression.None, null)
|
||||
};
|
||||
|
||||
var archive = new PhpPharArchive("/test.phar", "test.phar", null, null, entries, "abc123");
|
||||
var metadata = archive.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("test.phar", metadata["phar.path"]);
|
||||
Assert.Equal("2", metadata["phar.file_count"]);
|
||||
Assert.Equal("300", metadata["phar.total_size"]);
|
||||
Assert.Equal("true", metadata["phar.has_vendor"]);
|
||||
Assert.Equal("true", metadata["phar.has_composer"]);
|
||||
Assert.Equal("abc123", metadata["phar.sha256"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharArchive_CreateMetadata_IncludesManifestInfo()
|
||||
{
|
||||
var manifest = new PhpPharManifest(
|
||||
"myapp",
|
||||
"1.2.3",
|
||||
0x1100,
|
||||
PhpPharCompression.GZip,
|
||||
PhpPharSignatureType.Sha256,
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var archive = new PhpPharArchive("/test.phar", "test.phar", manifest, null, Array.Empty<PhpPharEntry>(), null);
|
||||
var metadata = archive.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("myapp", metadata["phar.alias"]);
|
||||
Assert.Equal("1.2.3", metadata["phar.version"]);
|
||||
Assert.Equal("gzip", metadata["phar.compression"]);
|
||||
Assert.Equal("sha256", metadata["phar.signature_type"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharArchive_CreateMetadata_DetectsAutoloadInStub()
|
||||
{
|
||||
var stubWithAutoload = "<?php\nspl_autoload_register(function($class) {});\n__HALT_COMPILER();";
|
||||
var archive = new PhpPharArchive("/test.phar", "test.phar", null, stubWithAutoload, Array.Empty<PhpPharEntry>(), null);
|
||||
var metadata = archive.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("true", metadata["phar.stub_has_autoload"]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpPharEntry Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpPharEntry_Extension_ReturnsCorrectExtension()
|
||||
{
|
||||
var entry = new PhpPharEntry("src/Main.php", 100, 80, 0, 0, PhpPharCompression.None, null);
|
||||
|
||||
Assert.Equal("php", entry.Extension);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharEntry_IsPhpFile_TrueForPhpExtension()
|
||||
{
|
||||
var entry = new PhpPharEntry("src/Main.php", 100, 80, 0, 0, PhpPharCompression.None, null);
|
||||
|
||||
Assert.True(entry.IsPhpFile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharEntry_IsPhpFile_FalseForOtherExtensions()
|
||||
{
|
||||
var entry = new PhpPharEntry("config/app.json", 100, 80, 0, 0, PhpPharCompression.None, null);
|
||||
|
||||
Assert.False(entry.IsPhpFile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharEntry_IsVendorFile_TrueForVendorPath()
|
||||
{
|
||||
var entry = new PhpPharEntry("vendor/monolog/monolog/src/Logger.php", 100, 80, 0, 0, PhpPharCompression.None, null);
|
||||
|
||||
Assert.True(entry.IsVendorFile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharEntry_IsVendorFile_FalseForSrcPath()
|
||||
{
|
||||
var entry = new PhpPharEntry("src/Main.php", 100, 80, 0, 0, PhpPharCompression.None, null);
|
||||
|
||||
Assert.False(entry.IsVendorFile);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpPharScanResult Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpPharScanResult_Empty_HasNoContent()
|
||||
{
|
||||
var result = PhpPharScanResult.Empty;
|
||||
|
||||
Assert.Empty(result.Archives);
|
||||
Assert.Empty(result.Usages);
|
||||
Assert.False(result.HasPharContent);
|
||||
Assert.Equal(0, result.TotalArchivedFiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharScanResult_HasPharContent_TrueWithArchives()
|
||||
{
|
||||
var archive = new PhpPharArchive("/test.phar", "test.phar", null, null, Array.Empty<PhpPharEntry>(), null);
|
||||
var result = new PhpPharScanResult(new[] { archive }, Array.Empty<PhpPharUsage>());
|
||||
|
||||
Assert.True(result.HasPharContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharScanResult_HasPharContent_TrueWithUsages()
|
||||
{
|
||||
var usage = new PhpPharUsage("src/Main.php", 10, "phar://myapp.phar/src/Helper.php", "myapp.phar/src/Helper.php");
|
||||
var result = new PhpPharScanResult(Array.Empty<PhpPharArchive>(), new[] { usage });
|
||||
|
||||
Assert.True(result.HasPharContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharScanResult_TotalArchivedFiles_SumsAcrossArchives()
|
||||
{
|
||||
var entries1 = new[]
|
||||
{
|
||||
new PhpPharEntry("file1.php", 100, 80, 0, 0, PhpPharCompression.None, null),
|
||||
new PhpPharEntry("file2.php", 200, 150, 0, 0, PhpPharCompression.None, null)
|
||||
};
|
||||
var entries2 = new[]
|
||||
{
|
||||
new PhpPharEntry("file3.php", 300, 250, 0, 0, PhpPharCompression.None, null)
|
||||
};
|
||||
|
||||
var archive1 = new PhpPharArchive("/test1.phar", "test1.phar", null, null, entries1, null);
|
||||
var archive2 = new PhpPharArchive("/test2.phar", "test2.phar", null, null, entries2, null);
|
||||
var result = new PhpPharScanResult(new[] { archive1, archive2 }, Array.Empty<PhpPharUsage>());
|
||||
|
||||
Assert.Equal(3, result.TotalArchivedFiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharScanResult_ArchivesWithVendor_FiltersCorrectly()
|
||||
{
|
||||
var entriesWithVendor = new[]
|
||||
{
|
||||
new PhpPharEntry("vendor/autoload.php", 100, 80, 0, 0, PhpPharCompression.None, null)
|
||||
};
|
||||
var entriesWithoutVendor = new[]
|
||||
{
|
||||
new PhpPharEntry("src/Main.php", 200, 150, 0, 0, PhpPharCompression.None, null)
|
||||
};
|
||||
|
||||
var archive1 = new PhpPharArchive("/with-vendor.phar", "with-vendor.phar", null, null, entriesWithVendor, null);
|
||||
var archive2 = new PhpPharArchive("/without-vendor.phar", "without-vendor.phar", null, null, entriesWithoutVendor, null);
|
||||
var result = new PhpPharScanResult(new[] { archive1, archive2 }, Array.Empty<PhpPharUsage>());
|
||||
|
||||
var archivesWithVendor = result.ArchivesWithVendor.ToList();
|
||||
Assert.Single(archivesWithVendor);
|
||||
Assert.Equal("with-vendor.phar", archivesWithVendor[0].RelativePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharScanResult_CreateMetadata_IncludesAllCounts()
|
||||
{
|
||||
var entries = new[]
|
||||
{
|
||||
new PhpPharEntry("vendor/autoload.php", 100, 80, 0, 0, PhpPharCompression.None, null)
|
||||
};
|
||||
var archive = new PhpPharArchive("/test.phar", "test.phar", null, null, entries, null);
|
||||
var usage = new PhpPharUsage("src/Main.php", 10, "phar://test.phar/file.php", "test.phar/file.php");
|
||||
var result = new PhpPharScanResult(new[] { archive }, new[] { usage });
|
||||
|
||||
var metadata = result.CreateMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("1", metadata["phar.archive_count"]);
|
||||
Assert.Equal("1", metadata["phar.usage_count"]);
|
||||
Assert.Equal("1", metadata["phar.total_archived_files"]);
|
||||
Assert.Equal("1", metadata["phar.archives_with_vendor"]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PhpPharUsage Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpPharUsage_RecordProperties_SetCorrectly()
|
||||
{
|
||||
var usage = new PhpPharUsage("src/Main.php", 42, "include 'phar://app.phar/Helper.php';", "app.phar/Helper.php");
|
||||
|
||||
Assert.Equal("src/Main.php", usage.SourceFile);
|
||||
Assert.Equal(42, usage.SourceLine);
|
||||
Assert.Equal("include 'phar://app.phar/Helper.php';", usage.Snippet);
|
||||
Assert.Equal("app.phar/Helper.php", usage.PharPath);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Compression and Signature Enums Tests
|
||||
|
||||
[Fact]
|
||||
public void PhpPharCompression_HasExpectedValues()
|
||||
{
|
||||
Assert.Equal(0, (int)PhpPharCompression.None);
|
||||
Assert.Equal(1, (int)PhpPharCompression.GZip);
|
||||
Assert.Equal(2, (int)PhpPharCompression.BZip2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhpPharSignatureType_HasExpectedValues()
|
||||
{
|
||||
Assert.Equal(0, (int)PhpPharSignatureType.None);
|
||||
Assert.Equal(1, (int)PhpPharSignatureType.Md5);
|
||||
Assert.Equal(2, (int)PhpPharSignatureType.Sha1);
|
||||
Assert.Equal(3, (int)PhpPharSignatureType.Sha256);
|
||||
Assert.Equal(4, (int)PhpPharSignatureType.Sha512);
|
||||
Assert.Equal(5, (int)PhpPharSignatureType.OpenSsl);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static byte[] CreateMinimalPharBytes(string stub)
|
||||
{
|
||||
// Create a minimal PHAR file structure
|
||||
// This is a simplified version - real PHARs have more complex structure
|
||||
var stubBytes = Encoding.UTF8.GetBytes(stub);
|
||||
|
||||
// Add some padding and a minimal manifest structure after __HALT_COMPILER();
|
||||
var padding = new byte[] { 0x0D, 0x0A }; // CRLF
|
||||
|
||||
// Minimal manifest: 4 bytes length + 4 bytes file count + 2 bytes API + 4 bytes flags + 4 bytes alias len + 4 bytes metadata len
|
||||
var manifestLength = 18u;
|
||||
var fileCount = 0u;
|
||||
var apiVersion = (ushort)0x1100;
|
||||
var flags = 0u;
|
||||
var aliasLength = 0u;
|
||||
var metadataLength = 0u;
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
ms.Write(stubBytes, 0, stubBytes.Length);
|
||||
ms.Write(padding, 0, padding.Length);
|
||||
ms.Write(BitConverter.GetBytes(manifestLength), 0, 4);
|
||||
ms.Write(BitConverter.GetBytes(fileCount), 0, 4);
|
||||
ms.Write(BitConverter.GetBytes(apiVersion), 0, 2);
|
||||
ms.Write(BitConverter.GetBytes(flags), 0, 4);
|
||||
ms.Write(BitConverter.GetBytes(aliasLength), 0, 4);
|
||||
ms.Write(BitConverter.GetBytes(metadataLength), 0, 4);
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -30,7 +30,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Scanner.Analyzers.Lang.Tests/StellaOps.Scanner.Analyzers.Lang.Tests.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang.Php/StellaOps.Scanner.Analyzers.Lang.Php.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Core/StellaOps.Scanner.Core.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user