Files
Investidor-V2/tests/Feature/ClamDScannerIntegrationTest.php
T
nala 1106509116
tests / ci (push) Canceled after 0s
vrsao 2
2026-10-08 14:02:01 +02:00

149 lines
6.4 KiB
PHP

<?php
namespace Tests\Feature;
use App\Services\Malware\MalwareScanner;
use Illuminate\Support\Str;
use Tests\TestCase;
class ClamDScannerIntegrationTest extends TestCase
{
private ?string $temporaryFile = null;
protected function tearDown(): void
{
if ($this->temporaryFile && is_file($this->temporaryFile)) {
unlink($this->temporaryFile);
}
parent::tearDown();
}
public function test_real_clamd_scans_a_harmless_document_and_honours_the_file_limit(): void
{
config(['services.malware.scanner' => 'clamd', 'services.malware.host' => '127.0.0.1', 'services.malware.port' => 3310]);
$scanner = app(MalwareScanner::class);
$health = $scanner->health();
if (! $health['available']) {
$this->markTestSkipped('A local ClamD with current signatures is required for this integration test.');
}
$root = storage_path('app/private');
$directory = $root.'/malware-integration-tests';
if (! is_dir($directory)) {
mkdir($directory, 0700, true);
}
$this->temporaryFile = $directory.'/'.Str::uuid().'.txt';
file_put_contents($this->temporaryFile, 'Harmless InvestMoz malware scanner integration test document.');
$result = $scanner->scan($this->temporaryFile, filesize($this->temporaryFile), $root);
$this->assertSame('clean', $result['result']);
$this->assertSame($health['engine_version'], $result['engine_version']);
$this->assertNotNull($result['signatures_at']);
config(['services.malware.max_file_bytes' => 1]);
$limited = $scanner->scan($this->temporaryFile, filesize($this->temporaryFile), $root);
$this->assertSame('inconclusive', $limited['result']);
$this->assertSame('file_scan_limit_exceeded', $limited['reason_code']);
}
public function test_unavailable_clamd_is_indeterminate_and_never_an_approval(): void
{
config(['services.malware.scanner' => 'clamd', 'services.malware.host' => '127.0.0.1', 'services.malware.port' => 1]);
$result = app(MalwareScanner::class)->scan(__FILE__, filesize(__FILE__), dirname(__DIR__, 2));
$this->assertSame('inconclusive', $result['result']);
$this->assertSame('scanner_unavailable', $result['reason_code']);
}
public function test_clamd_response_timeout_is_inconclusive(): void
{
$root = storage_path('app/private');
$directory = $root.'/malware-integration-tests';
if (! is_dir($directory)) {
mkdir($directory, 0700, true);
}
$this->temporaryFile = $directory.'/'.Str::uuid().'.txt';
file_put_contents($this->temporaryFile, 'Harmless timeout fixture.');
$fixture = __DIR__.'/../Fixtures/ClamDStallServer.php';
$process = proc_open([PHP_BINARY, $fixture], [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
$this->assertIsResource($process);
fclose($pipes[0]);
$address = trim((string) fgets($pipes[1]));
$port = (int) substr($address, strrpos($address, ':') + 1);
config(['services.malware.scanner' => 'clamd', 'services.malware.host' => '127.0.0.1',
'services.malware.port' => $port, 'services.malware.timeout_seconds' => 1]);
try {
$result = app(MalwareScanner::class)->scan($this->temporaryFile, filesize($this->temporaryFile), $root);
} finally {
proc_terminate($process);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
}
$this->assertSame('inconclusive', $result['result']);
$this->assertSame('scanner_timeout', $result['reason_code']);
}
public function test_real_clamd_keeps_a_password_protected_archive_inconclusive(): void
{
$this->requireRealClamd();
if (! class_exists(\ZipArchive::class) || ! defined('ZipArchive::EM_AES_256')) {
$this->markTestSkipped('PHP ZipArchive with AES support is required to create the harmless encrypted fixture.');
}
$root = storage_path('app/private');
$directory = $root.'/malware-integration-tests';
if (! is_dir($directory)) {
mkdir($directory, 0700, true);
}
$this->temporaryFile = $directory.'/'.Str::uuid().'.zip';
$archive = new \ZipArchive;
$this->assertSame(true, $archive->open($this->temporaryFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE));
$this->assertTrue($archive->addFromString('readme.txt', 'Harmless encrypted archive fixture.'));
$this->assertTrue($archive->setPassword('local-test-password'));
$this->assertTrue($archive->setEncryptionName('readme.txt', \ZipArchive::EM_AES_256));
$archive->close();
$result = app(MalwareScanner::class)->scan($this->temporaryFile, filesize($this->temporaryFile), $root);
$this->assertSame('inconclusive', $result['result']);
$this->assertSame('encrypted_or_password_protected', $result['reason_code']);
}
public function test_zip_beyond_the_decompression_limit_is_inconclusive(): void
{
$this->requireRealClamd();
if (! class_exists(\ZipArchive::class)) {
$this->markTestSkipped('PHP ZipArchive is required to create the harmless compressed fixture.');
}
$root = storage_path('app/private');
$directory = $root.'/malware-integration-tests';
if (! is_dir($directory)) {
mkdir($directory, 0700, true);
}
$this->temporaryFile = $directory.'/'.Str::uuid().'.zip';
$archive = new \ZipArchive;
$this->assertSame(true, $archive->open($this->temporaryFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE));
$this->assertTrue($archive->addFromString('expanded.txt', str_repeat('A', 110 * 1024 * 1024)));
$archive->close();
$result = app(MalwareScanner::class)->scan($this->temporaryFile, filesize($this->temporaryFile), $root);
$this->assertSame('inconclusive', $result['result']);
$this->assertSame('archive_decompression_limit', $result['reason_code']);
}
private function requireRealClamd(): void
{
config(['services.malware.scanner' => 'clamd', 'services.malware.host' => '127.0.0.1', 'services.malware.port' => 3310]);
if (! app(MalwareScanner::class)->health()['available']) {
$this->markTestSkipped('A local ClamD with current signatures is required for this integration test.');
}
}
}