diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..183e3ca --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,12 @@ +name: Module CI + +on: + push: + pull_request: + +jobs: + ci: + name: CI + uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1 + with: + endtoend: false diff --git a/.gitignore b/.gitignore index 723ef36..aee89db 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -.idea \ No newline at end of file +.idea +.phpunit.result.cache +composer.lock +public/ +vendor/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 334f0be..0000000 --- a/.travis.yml +++ /dev/null @@ -1,31 +0,0 @@ -language: php -php: - - 5.3 - -env: - matrix: - - DB=MYSQL CORE_RELEASE=3.2 - -matrix: - include: - - php: 5.3 - env: DB=MYSQL CORE_RELEASE=3.2 - - php: 5.4 - env: DB=MYSQL CORE_RELEASE=3.2 - - php: 5.5 - env: DB=MYSQL CORE_RELEASE=3.3 - - php: 5.6 - env: DB=MYSQL CORE_RELEASE=3 - - php: 5.6 - env: DB=MYSQL CORE_RELEASE=3.2 - - php: 5.6 - env: DB=PGSQL CORE_RELEASE=3.3 - -before_script: - - phpenv rehash - - git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support - - php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss - - cd ~/builds/ss - -script: - - vendor/bin/phpunit steamedclams/tests/ diff --git a/README.md b/README.md index b3c9b86..b06f8a2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ nightly cron that scans the files or if you have queuedjobs installed, it will a # Composer Install ``` -composer require symbiote/silverstripe-steamedclams:~2.0 +composer require symbiote/silverstripe-steamedclams ``` # Screenshots @@ -74,6 +74,8 @@ Symbiote\SteamedClams\ClamAV: deny_on_failure: false # For configuring on existing site builds and ignoring the scanning of pre-module install `File` records. initial_scan_ignore_before_datetime: '1970-12-25 00:00:00' + # If true will send files to clamd as streams (by default files are referenced using their path). Useful when files are stored remotely and/or encrypted at rest. + use_streams: false ``` If you have the QueuedJobs module installed, you can configure when files missed by ClamAV daemon are scanned. @@ -137,9 +139,10 @@ ClamAVEmulator::config()->mode = ClamAVEmulator::MODE_OFFLINE; ``` # Supports -- Silverstripe 4.0 and up +- Silverstripe 5.0 and up - [Versioned Files](https://github.com/symbiote/silverstripe-versionedfiles) - [CDN Content](https://github.com/symbiote/silverstripe-cdncontent) +- For Silverstripe 4.x use 3.0 - For Silverstripe 3.2 and up (3.1 *should* work, create an issue if you determine otherwise) use 1.0 # Credits diff --git a/_config/extensions.yml b/_config/extensions.yml index d16ba11..c30f356 100644 --- a/_config/extensions.yml +++ b/_config/extensions.yml @@ -7,3 +7,6 @@ SilverStripe\Assets\File: SilverStripe\SiteConfig\SiteConfig: extensions: - Symbiote\SteamedClams\Extension\ClamAVSiteConfigExtension +SilverStripe\Admin\Forms\UsedOnTable: + extensions: + - Symbiote\SteamedClams\Extension\ClamAVUsedOnTableExtension diff --git a/composer.json b/composer.json index 42cca76..0f5ed77 100644 --- a/composer.json +++ b/composer.json @@ -13,14 +13,21 @@ } ], "require": { - "silverstripe/framework": "^4" + "silverstripe/framework": "^5", + "silverstripe/admin": "^2", + "silverstripe/reports": "^5", + "silverstripe/siteconfig": "^5", + "xenolope/quahog": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" }, "suggest": { "silverstripe/queuedjobs": "For allowing ClamAV 'missed files' scan to be run from a queued job. Otherwise you can run the tasks manually or via cronjob." }, "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "4.0.x-dev" }, "expose": [ "client/css", diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..a8f91f6 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,16 @@ + + + + + src/ + + + tests/ + + + + + tests + + + diff --git a/src/Admin/ClamAVAdmin.php b/src/Admin/ClamAVAdmin.php index a33a032..d456d39 100644 --- a/src/Admin/ClamAVAdmin.php +++ b/src/Admin/ClamAVAdmin.php @@ -44,7 +44,7 @@ class ClamAVAdmin extends ModelAdmin /** * @var string */ - private static $menu_icon = 'vendor/symbiote/silverstripe-steamedclams/client/images/clamav_icon.png'; + private static $menu_icon = 'symbiote/silverstripe-steamedclams:client/images/clamav_icon.png'; /** * @var array @@ -117,7 +117,7 @@ public function getEditForm($id = null, $fields = null) $versionField = LiteralField::create('ClamAV_Version', $version); $versionField->setRightTitle($reason); $versionField->dontEscape = true; - $fields->insertBefore($versionField, $insertBeforeFieldName); + $fields->insertBefore($insertBeforeFieldName, $versionField); // Files to scan with install task $listCount = 0; @@ -129,12 +129,12 @@ public function getEditForm($id = null, $fields = null) if ($listCount > 0) { $fields->insertBefore( + $insertBeforeFieldName, ReadonlyField::create( 'ClamAV_InitialScan', 'Files to scan with install task', $listCount . ' ' - ), - $insertBeforeFieldName + ) ); } @@ -145,9 +145,9 @@ public function getEditForm($id = null, $fields = null) $listCount = $list->count(); } $fields->insertBefore( + $insertBeforeFieldName, ReadonlyField::create('ClamAV_NeedScan', 'Files that failed to scan', $listCount . ' ') - ->setRightTitle('Due to ClamAV daemon being inaccessible/offline.'), - $insertBeforeFieldName + ->setRightTitle('Due to ClamAV daemon being inaccessible/offline.') ); }); diff --git a/src/ClamAV.php b/src/ClamAV.php index 569f1d2..8b846fc 100644 --- a/src/ClamAV.php +++ b/src/ClamAV.php @@ -15,6 +15,7 @@ use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; +use Socket\Raw\Exception; use Symbiote\SteamedClams\Model\ClamAVScan; use SilverStripe\Assets\Flysystem\ProtectedAssetAdapter; @@ -56,7 +57,6 @@ class ClamAV */ private static $clamd = [ // Path to a local socket file the daemon will listen on. - // Default: disabled (must be specified by a user) 'LocalSocket' => '/var/run/clamav/clamd.ctl', ]; @@ -82,68 +82,32 @@ class ClamAV */ public function scanFileRecordForVirus(File $file) { - $isFileMaybeExternal = false; - $fileMetaData = $file->File->getMetadata(); - $filepath = $file->getFullPath(); - - if (!file_exists($filepath)) { - // If file can't be found, attempt to download - // from external CDN or similar. - $isFileMaybeExternal = true; - $this->beforeHandleMissingFile($file); - - return null; - } - - $record = $this->scanFileForVirus($filepath); + $record = $this->scanFileForVirus($file); if ($record && $record instanceof DataObject) { $record->FileID = $file->ID; } - if ($isFileMaybeExternal) { - // If file was downloaded from external CDN - // or similar, delete the file / cleanup. - $this->afterHandleMissingFile($file); - } return $record; } /** - * If file doesn't exist on local machine, try to download from a CDN module or similar. - * * @param File $file - * - * @return boolean|null - */ - public function beforeHandleMissingFile(File $file) - { - $result = null; - - // Support CDN module 'CDNFile' extension - if ($file->hasMethod('downloadFromContentService')) { - $result = $result || $file->downloadFromContentService(); - } - - // note(Jake): Perhaps add extension here to support other modules - - return $result; - } - - /** * @return ClamAVScan|null */ - public function scanFileForVirus($filepath) + public function scanFileForVirus($file): ?ClamAVScan { - $clamdConf = Config::inst()->get(__CLASS__, 'clamd'); - $localSocket = isset($clamdConf['LocalSocket']) ? $clamdConf['LocalSocket'] : ''; + $filepath = $file->getFullPath(true); - if (!$localSocket) { - throw new LogicException('Empty value for "clamd.LocalSocket" config not allowed.'); - } + try { + $clamd = $this->getClamd(); - $scanResult = $this->fileScan($filepath); + $scanResult = $clamd->scanStream($file->getString()); + } catch (\Socket\Raw\Exception $e) { + $this->setLastExceptionAndLog($e); + $scanResult = null; + } - if ($scanResult === self::OFFLINE) { + if (!$scanResult) { $record = ClamAVScan::create(); $record->Filename = $filepath; $record->IPAddress = $this->getIP(); @@ -152,65 +116,54 @@ public function scanFileForVirus($filepath) return $record; } - $stats = ($scanResult && isset($scanResult['stats'])) ? $scanResult['stats'] : null; - - $filename = ($scanResult && isset($scanResult['file'])) ? $scanResult['file'] : null; - if ($stats === null || $filename === null) { - throw new LogicException('Expected an array with "stats" and "file" as key.'); - } + $filename = $scanResult->getFilename(); + if ($filename === 'stream') $filename = $filepath; $record = ClamAVScan::create(); $record->Filename = $filepath; $record->IPAddress = $this->getIP(); $record->IsScanned = 1; - $record->IsInfected = ($stats !== 'OK'); - $record->setRawData($scanResult); + $record->IsInfected = $scanResult->hasFailed(); + $record->setRawData((array)$scanResult); return $record; } - /** - * Scan for virus, return array() if ClamAV daemon is running and - * returns false if it is not (or an error occurred connecting to the socket) - * - * @param string $filepath - * - * @return array|false - */ - protected function fileScan($filepath) - { - $this->last_exception = null; - - try { - $clamd = $this->getClamd(); - $scanResult = $clamd->fileScan($filepath); - } catch (\ClamdSocketException $e) { - $this->setLastExceptionAndLog($e); - $scanResult = self::OFFLINE; - } - - return $scanResult; - } - /** * Return underlying Clamd implementation. * * @return \ClamdBase */ - protected function getClamd() + protected function getClamd(bool $startSession = true) { if ($this->clamd_instance) { return $this->clamd_instance; } - $result = null; - if (class_exists(Injector::class)) { - $result = Injector::inst()->create(\ClamdPipe::class); - } else { - $result = new \ClamdPipe; + $clamdConf = Config::inst()->get(__CLASS__, 'clamd'); + $localSocket = isset($clamdConf['LocalSocket']) ? $clamdConf['LocalSocket'] : ''; + + if (!$localSocket) { + throw new LogicException('Empty value for "clamd.LocalSocket" config not allowed.'); } - return $this->clamd_instance = $result; + try { + $socket = (new \Socket\Raw\Factory())->createClient('unix://' . $localSocket); + $clamdClient = new \Xenolope\Quahog\Client($socket, 30, PHP_NORMAL_READ); + + if ($startSession) { + $clamdClient->startSession(); + } + } catch (\Socket\Raw\Exception $e) { + throw new Exception('ClamAV socket error: ' . $e->getMessage()); + } + + return $this->clamd_instance = $clamdClient; + } + + public function endClamdSession() + { + $this->getClamd()->endSession(); } /** @@ -248,28 +201,6 @@ protected function getIP() return $request->getIP(); } - /** - * If file didn't exist on local machine and downloaded from CDN, we want to re-remove it. - * - * @param File $file - * - * @return boolean|null - */ - public function afterHandleMissingFile(File $file) - { - $result = null; - - // Support CDN module 'CDNFile' extension - if ($file->hasMethod('deleteLocalIfExistsOnContentService')) { - //$this->log('Removing '.$file->ClassName.' #'.$file->ID.' from local machine -IF- it exists on CDN...'); - $result = $result || $file->deleteLocalIfExistsOnContentService(); - } - - // note(Jake): Perhaps add extension here to support other modules - - return $result; - } - /** * Get list of files that haven't been checked at all. * ie. before installation of module @@ -370,7 +301,7 @@ public function version() $clamd = $this->getClamd(); $version = $clamd->version(); - } catch (\ClamdSocketException $e) { + } catch (\Socket\Raw\Exception $e) { $this->setLastExceptionAndLog($e); $version = self::OFFLINE; } diff --git a/src/ClamAVEmulator.php b/src/ClamAVEmulator.php index 5e5985e..c5c0e3c 100644 --- a/src/ClamAVEmulator.php +++ b/src/ClamAVEmulator.php @@ -3,7 +3,9 @@ namespace Symbiote\SteamedClams; use LogicException; +use SilverStripe\Assets\File; use SilverStripe\Core\Config\Config; +use SilverStripe\ORM\DataObject; /** * For emulating/faking ClamAV results @@ -117,4 +119,14 @@ protected function modeInvalid() . '". Use constants provided in ' . __CLASS__ . ' class.' ); } + + public function scanFileRecordForVirus(File $file) + { + $record = $this->scanFileForVirus('fake/file.txt'); + if ($record && $record instanceof DataObject) { + $record->FileID = $file->ID; + } + return $record; + } + } diff --git a/src/Extension/ClamAVExtension.php b/src/Extension/ClamAVExtension.php index 66baa71..01462af 100644 --- a/src/Extension/ClamAVExtension.php +++ b/src/Extension/ClamAVExtension.php @@ -5,6 +5,7 @@ use SilverStripe\Assets\File; use SilverStripe\Assets\Flysystem\ProtectedAssetAdapter; use SilverStripe\Assets\Folder; +use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataList; @@ -12,7 +13,6 @@ use SilverStripe\ORM\ValidationResult; use Symbiote\SteamedClams\ClamAV; use Symbiote\SteamedClams\Model\ClamAVScan; -use SilverStripe\Core\Config\Config; use Silverstripe\SiteConfig\SiteConfig; /** @@ -55,13 +55,13 @@ public function validate(ValidationResult $validationResult) // If its a new file, scan it. $doVirusScan = ($this->owner->ID == 0); - // Support VersionedFiles module - // ie. If file has been replaced, scan it. + // Scan a file if it changed $changedFields = $this->owner->getChangedFields(true, DataObject::CHANGE_VALUE); - $currentVersionIDChanged = (isset($changedFields['CurrentVersionID'])) ? $changedFields['CurrentVersionID'] : []; - - if ($currentVersionIDChanged && $currentVersionIDChanged['before'] != $currentVersionIDChanged['after']) { - $doVirusScan = true; + foreach (['File', 'FileHash', 'Version', 'CurrentVersionID'] as $changeField) { + if (isset($changedFields[$changeField]) && $changedFields[$changeField]['before'] !== $changedFields[$changeField]['after']) { + $doVirusScan = true; + break; + } } // NOTE(Jake): Perhaps add $this->extend('updateDoVirusScan'); so other modules can support this. @@ -105,11 +105,8 @@ public function validate(ValidationResult $validationResult) // Delete infected file // (If file hasn't been written to DB yet) if ($this->owner->ID == 0) { - $filepath = $this->owner->getFullPath(); + $this->owner->deleteFile(); - if (file_exists($filepath)) { - @unlink($filepath); - } $record->Action = ClamAVScan::ACTION_DELETED; } @@ -152,26 +149,27 @@ public function isVirusScannable() } /** - * Returns an absolute filesystem path to the file. - * Use {@link getRelativePath()} to get the same path relative to the webroot. + * Returns a source URL/path to the file based on the used assets store + * Optionally removes any query params (e.g. when used with S3). * - * @return String + * @param bool $stripQueryParams + * @return string|null */ - public function getFullPath() + public function getFullPath(bool $stripQueryParams = false): ?string { - if (!isset($this->owner->File)) { + /** @var File $owner */ + $owner = $this->getOwner(); + + if (!isset($owner->File)) { return null; } - $fileMetaData = $this->owner->File->getMetadata(); - - if ($this->owner->isPublished()) { - return PUBLIC_PATH . $this->owner->File->getURL(); - } else { - return ASSETS_PATH . '/' . - Config::inst()->get(ProtectedAssetAdapter::class, 'secure_folder') - . '/' . $fileMetaData['path']; + $sourceUrl = $this->owner->File->getSourceURL() ?? ''; + if ($stripQueryParams) { + return strtok($sourceUrl, '?'); } + + return $sourceUrl; } /** diff --git a/src/Extension/ClamAVUsedOnTableExtension.php b/src/Extension/ClamAVUsedOnTableExtension.php new file mode 100644 index 0000000..20c9827 --- /dev/null +++ b/src/Extension/ClamAVUsedOnTableExtension.php @@ -0,0 +1,20 @@ +FileID > 0) { + /** @var File $file */ $file = $this->File(); if ($file->exists()) { - $file->delete(); + $file->deleteFile(); } } - $action = (int)$this->Action; + $action = (int) $this->Action; if ($action !== ClamAVScan::ACTION_DELETED) { $this->Action = ClamAVScan::ACTION_DELETED; $member = Security::getCurrentUser(); @@ -539,7 +540,7 @@ public function getUserIdentifier() public function getRawDataSummary() { $rawData = $this->RawData; - $value = ($rawData && isset($rawData['stats'])) ? $rawData['stats'] : ''; + $value = ($rawData && isset($rawData['status'])) ? $rawData['status'] : ''; return $value; } @@ -551,7 +552,7 @@ public function getRawData() { $value = $this->getField('RawData'); if (is_string($value)) { - $value = Convert::json2array($value); + $value = json_decode($value, true); } return $value; @@ -561,11 +562,12 @@ public function getRawData() * @param array $value * * @return null + * @throws \JsonException */ public function setRawData($value) { if (is_array($value)) { - $value = Convert::array2json($value); + $value = json_encode($value, JSON_THROW_ON_ERROR); } $this->setField('RawData', $value); } diff --git a/src/Tasks/ClamAVBaseTask.php b/src/Tasks/ClamAVBaseTask.php index 6552126..a847ce3 100644 --- a/src/Tasks/ClamAVBaseTask.php +++ b/src/Tasks/ClamAVBaseTask.php @@ -173,7 +173,7 @@ protected function log($messageOrDataObject, $type = '', Exception $exception = * @param $errline * @param $errcontext */ - public function log_error_handler($errno, $errstr, $errfile, $errline, $errcontext) + public function log_error_handler(int $errno, string $errstr, string $errfile = null, int $errline = null, array $errcontext = null): void { DB::alteration_message($errstr, 'error'); diff --git a/src/thirdparty/clamd.php b/src/thirdparty/clamd.php deleted file mode 100644 index d469d6a..0000000 --- a/src/thirdparty/clamd.php +++ /dev/null @@ -1,172 +0,0 @@ - - * Licence : MIT - * - * @source https://github.com/FileZ/php-clamd - * @source_mirror https://github.com/SilbinaryWolf/php-clamd - */ - -// todo(Jake): do PR against "https://github.com/FileZ/php-clamd" -define('CLAMD_PIPE', '/var/run/clamav/clamd.ctl'); -define('CLAMD_HOST', '127.0.0.1'); -define('CLAMD_PORT', 3310); -define('CLAMD_MAXP', 20000); - -/* EICAR is a simple test for AV scanners, see: https://en.wikipedia.org/wiki/EICAR_test_file */ -$EICAR_TEST = 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'; - - -class ClamdSocketException extends Exception { - protected $errorCode; - - public function __construct($message, $socketErrorCode) { - $this->errorCode = $socketErrorCode; - if (!$message) { - $message = socket_strerror($this->errorCode); - } - parent::__construct($message); - } - - /* Get socket error (returned from 'socket_last_error') */ - public function getErrorCode() { - return $this->errorCode; - } -} - -/* An abstract class that `ClamdPipe` and `ClamdNetwork` will inherit. */ -abstract class ClamdBase { - - abstract protected function getSocket(); - - /* Send command to Clamd */ - private function sendCommand($command) { - $return = null; - - $socket = $this->getSocket(); - socket_send($socket, $command, strlen($command), 0); - socket_recv($socket, $return, CLAMD_MAXP, 0); - socket_close($socket); - - return $return; - } - - /* `ping` command is used to see whether Clamd is alive or not */ - public function ping() { - $return = $this->sendCommand('PING'); - return strcmp($return, 'PONG') ? true : false; - } - - /* `version` is used to receive the version of Clamd */ - public function version() { - return trim($this->sendCommand('VERSION')); - } - - /* `reload` Reload Clamd */ - public function reload() { - return $this->sendCommand('RELOAD'); - } - - /* `shutdown` Shutdown Clamd */ - public function shutdown() { - return $this->sendCommand('SHUTDOWN'); - } - - /* `fileScan` is used to scan single file. */ - public function fileScan($file) { - list($file, $stats) = explode(':', $this->sendCommand('SCAN ' . $file)); - - return array( 'file' => $file, 'stats' => trim($stats)); - } - - /* `continueScan` is used to scan multiple files/directories. */ - public function continueScan($file) { - $return = array(); - - foreach( explode("\n", trim($this->sendCommand('CONTSCAN ' . $file))) as $results ) { - list($file, $stats) = explode(':', $results); - array_push($return, array( 'file' => $file, 'stats' => trim($stats) )); - } - return $return; - } - - /* `streamScan` is used to scan a buffer. */ - public function streamScan($buffer) { - $port = null; - $socket = null; - $command = 'STREAM'; - $return = null; - - $socket = $this->getSocket(); - socket_send($socket, $command, strlen($command), 0); - socket_recv($socket, $return, CLAMD_MAXP, 0); - - sscanf($return, 'PORT %d\n', $port); - - $stream = socket_create(AF_INET, SOCK_STREAM, 0); - socket_connect($stream, CLAMD_HOST, $port); - socket_send($stream, $buffer, strlen($buffer), 0); - socket_close($stream); - - socket_recv($socket, $return, CLAMD_MAXP, 0); - - socket_close($socket); - - return array('stats' => trim(str_replace('stream: ', '', $return))); - } -} - -/* This class can be used to connect to local socket, the default */ -class ClamdPipe extends ClamdBase { - private $pip; - - /* You need to pass the path to the socket pipe */ - public function __construct($pip=CLAMD_PIPE) { - $this->pip = $pip; - } - - protected function getSocket() { - $socket = @socket_create(AF_UNIX, SOCK_STREAM, 0); - if ($socket === FALSE) { - throw new ClamdSocketException('', socket_last_error()); - } - $hasError = @socket_connect($socket, $this->pip); - if ($hasError === FALSE) { - $errorCode = socket_last_error(); - $errorMessage = socket_strerror($errorCode); - if ($errorCode === 2) { - // ie. `No such file or directory "/var/run/clamav/clamd.ctl"` - $errorMessage .= ' "'.$this->pip.'", Is clamd running and are your user/group permissions configured properly?'; - } - throw new ClamdSocketException($errorMessage, $errorCode); - } - return $socket; - } -} - - -/* This class can be used to connect to Clamd running over the network */ -class ClamdNetwork extends ClamdBase { - private $host; - private $port; - - /* You need to pass the host address and the port the the server */ - public function __construct($host=CLAMD_HOST, $port=CLAMD_PORT) { - $this->host = $host; - $this->port = $port; - } - - protected function getSocket() { - $socket = @socket_create(AF_INET, SOCK_STREAM, 0); - if ($socket === FALSE) { - throw new ClamdSocketException('', socket_last_error()); - } - $hasError = @socket_connect($socket, $this->host, $this->port); - if ($hasError === FALSE) { - throw new ClamdSocketException('', socket_last_error()); - } - return $socket; - } -} diff --git a/tests/ClamAVCMSTest.php b/tests/ClamAVCMSTest.php index 0e0ef77..440d991 100644 --- a/tests/ClamAVCMSTest.php +++ b/tests/ClamAVCMSTest.php @@ -1,13 +1,13 @@ logInAs('admin'); // Test ModelAdmin listing $controller = singleton(ClamAVAdmin::class); $response = $this->get($controller->Link()); + + $this->assertEquals(200, $response->getStatusCode()); } /** * */ - public function testClamAVReport() + public function testClamAVReport(): void { if (!class_exists(Report::class)) { return; @@ -43,5 +45,7 @@ public function testClamAVReport() // Test Report page $controller = singleton(ClamAVScanReport::class); $response = $this->get($controller->getLink()); + + $this->assertEquals(200, $response->getStatusCode()); } } diff --git a/tests/ClamAVExtensionTest.php b/tests/ClamAVExtensionTest.php index 27f8854..ce0d031 100644 --- a/tests/ClamAVExtensionTest.php +++ b/tests/ClamAVExtensionTest.php @@ -1,9 +1,12 @@ set('ignore_cli', false); + + $clamAV = new ClamAVEmulator(); + Injector::inst()->registerService($clamAV, ClamAV::class); } //protected static $fixture_file = 'ClamAVExtensionTest.yml'; - protected function getMockFile($name = 'test-file.txt') + protected function getMockFile($name = 'test-file.txt'): string { - $absoluteTmpPath = ASSETS_PATH . DIRECTORY_SEPARATOR . $name; + $absoluteTmpPath = TestAssetStore::base_path() . DIRECTORY_SEPARATOR . $name; file_put_contents($absoluteTmpPath, 'testtext'); return $absoluteTmpPath; } + /** * */ - public function testBlockFileWriteIfVirusAndDenyOnFailure() + public function testBlockFileWriteIfVirusAndDenyOnFailure(): void { - ClamAVEmulator::config()->mode = ClamAVEmulator::MODE_HAS_VIRUS; - ClamAV::config()->deny_on_failure = true; + ClamAVEmulator::config()->set('mode', ClamAVEmulator::MODE_HAS_VIRUS); + ClamAV::config()->set('deny_on_failure', true); $scanCount = ClamAVScan::get()->count(); @@ -60,10 +67,10 @@ public function testBlockFileWriteIfVirusAndDenyOnFailure() $this->assertEquals($fileCount, File::get()->count()); } - public function testFileLogIfVirus() + public function testFileLogIfVirus(): void { - ClamAVEmulator::config()->mode = ClamAVEmulator::MODE_HAS_VIRUS; - ClamAV::config()->deny_on_failure = false; + ClamAVEmulator::config()->set('mode', ClamAVEmulator::MODE_HAS_VIRUS); + ClamAV::config()->set('deny_on_failure', false); $name = 'updated-file.txt'; @@ -81,19 +88,17 @@ public function testFileLogIfVirus() // Ensure scan is created $this->assertEquals($scanCount + 1, ClamAVScan::get()->count()); - // Ensure file not get created if it has a virus + // Ensure file created because deny_on_failure is disabled $this->assertEquals($fileCount, File::get()->count()); } - public function testFileLogIfVirusScannerOffline() + public function testFileLogIfVirusScannerOffline(): void { - ClamAVEmulator::config()->mode = ClamAVEmulator::MODE_OFFLINE; - ClamAV::config()->deny_on_failure = false; + ClamAVEmulator::config()->set('mode', ClamAVEmulator::MODE_OFFLINE); + ClamAV::config()->set('deny_on_failure', false); $name = 'updated-file.txt'; - $filePathName = $this->getMockFile($name); - $fileCount = File::get()->count(); $scanCount = ClamAVScan::get()->count(); $record = File::create(); $record->Name = $name; @@ -109,16 +114,16 @@ public function testFileLogIfVirusScannerOffline() $this->assertEquals($scanCount + 1, ClamAVScan::get()->count()); // Ensure file gets created regardless of whether it has a virus - $this->assertEquals($fileCount, File::get()->count()); + $this->assertEquals(1, File::get()->count()); } - public function testPhysicalFileRemovalOnNewFileRecordIfDenied() + public function testPhysicalFileRemovalOnNewFileRecordIfDenied(): void { - ClamAVEmulator::config()->mode = ClamAVEmulator::MODE_HAS_VIRUS; - ClamAV::config()->deny_on_failure = true; + ClamAVEmulator::config()->set('mode', ClamAVEmulator::MODE_HAS_VIRUS); + ClamAV::config()->set('deny_on_failure', true); $filename = 'clamav_' . __FUNCTION__ . '.txt'; - $filepath = ASSETS_PATH . DIRECTORY_SEPARATOR . $filename; + $filepath = TestAssetStore::base_path() . DIRECTORY_SEPARATOR . $filename; $this->assertFalse(file_exists($filepath)); file_put_contents($filepath, 'testtext'); @@ -128,9 +133,8 @@ public function testPhysicalFileRemovalOnNewFileRecordIfDenied() $record = File::create(); $record->File->setFromLocalFile($filepath, $filename); - $fileMetaData = $record->File->getMetadata(); - $newFilepath = ASSETS_PATH . '/' . Config::inst()->get(ProtectedAssetAdapter::class, 'secure_folder') - . '/'. $fileMetaData['path']; + $newFilepath = TestAssetStore::base_path() . '/' . Config::inst()->get(ProtectedAssetAdapter::class, 'secure_folder') + . '/'. $record->File->getFilename(); try { $record->write(); @@ -145,20 +149,20 @@ public function testPhysicalFileRemovalOnNewFileRecordIfDenied() $this->assertFalse($fileExists); } - public function testPhysicalFileRemovalOnNewFileRecordIfNotDenied() + public function testPhysicalFileRemovalOnNewFileRecordIfNotDenied(): void { - ClamAVEmulator::config()->mode = ClamAVEmulator::MODE_HAS_VIRUS; - ClamAV::config()->deny_on_failure = false; + ClamAVEmulator::config()->set('mode', ClamAVEmulator::MODE_HAS_VIRUS); + ClamAV::config()->set('deny_on_failure', false); $filename = 'clamav_' . __FUNCTION__ . '.txt'; - $filepath = ASSETS_PATH . DIRECTORY_SEPARATOR . $filename; + $filepath = TestAssetStore::base_path() . DIRECTORY_SEPARATOR . $filename; // Ensure file didn't already exist on system $this->assertFalse(file_exists($filepath)); file_put_contents($filepath, 'testtext'); $this->assertTrue(file_exists($filepath)); $record = File::create(); - $record->Filename = ASSETS_DIR . '/' . $filename; + $record->Filename = TestAssetStore::base_path() . '/' . $filename; try { $record->write(); } catch (ValidationException $e) { diff --git a/tests/ClamAVUsedOnTableExtensionTest.php b/tests/ClamAVUsedOnTableExtensionTest.php new file mode 100644 index 0000000..b8a5b25 --- /dev/null +++ b/tests/ClamAVUsedOnTableExtensionTest.php @@ -0,0 +1,26 @@ +updateUsageExcludedClasses($excluded); + $this->assertContains(ClamAVScan::class, $excluded, 'ClamAVScan has been added to exclusion list'); + $this->assertContains('Page', $excluded, 'Pre-existing exclusion list entries are retained'); + } +}