149 lines
6.4 KiB
PHP
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.');
|
|
}
|
|
}
|
|
}
|