Source of file ClamAV.php
Size: 10,578 Bytes - Last Modified: 2021-12-23T10:21:01+00:00
/var/www/docs.ssmods.com/process/src/src/ClamAV.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 | <?php namespace Symbiote\SteamedClams; use LogicException; use Psr\Log\LoggerInterface; use SilverStripe\Assets\File; use SilverStripe\Assets\Folder; use SilverStripe\Control\Controller; use SilverStripe\Control\Director; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injector; use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; use Symbiote\SteamedClams\Model\ClamAVScan; use SilverStripe\Assets\Flysystem\ProtectedAssetAdapter; class ClamAV { use Injectable; use Configurable; const MODULE_DIR = 'steamedclams'; /** * If ClamAV daemon can't be connected to or is offline. */ const OFFLINE = false; /** * Configure this to ignore `File` records created before the date * provided. * * eg. You installed this module on a 5 year old website and to avoid a bulky * amount of `ClamAVScan` records from `ClamAVInstallTask`, you're just opting * to not scan those old files for viruses * * @var string */ private static $initial_scan_ignore_before_datetime = '1970-12-25 00:00:00'; /** * If enabled, if ClamAV daemon isn't running or isn't installed * the file will be denied as if it has a virus. */ private static $deny_on_failure = false; /** * Settings that must be identical to your clamd.conf file. * * @config * @var array */ 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', ]; /** * @var \ClamdBase */ protected $clamd_instance = null; /** * @var \ClamdException */ protected $last_exception = null; /** * @var boolean|null */ protected $_cache_isOffline = null; /** * @param File $file * * @return ClamAVScan|null */ 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); 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) { $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.'); } $scanResult = $this->fileScan($filepath); if ($scanResult === self::OFFLINE) { $record = ClamAVScan::create(); $record->Filename = $filepath; $record->IPAddress = $this->getIP(); $record->setRawData($scanResult); 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.'); } $record = ClamAVScan::create(); $record->Filename = $filepath; $record->IPAddress = $this->getIP(); $record->IsScanned = 1; $record->IsInfected = ($stats !== 'OK'); $record->setRawData($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() { 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; } return $this->clamd_instance = $result; } /** * Set exception, if it has a non-falsey value, log it. */ protected function setLastExceptionAndLog(\Exception $e) { if ($e) { Injector::inst()->get(LoggerInterface::class)->warning('Query executed: ' . $e->getMessage()); } $this->last_exception = $e; } /** * Get the current users IP address * * @return string */ protected function getIP() { if (!Controller::has_curr()) { if (Director::is_cli()) { // If running from command line, you can assume it's // the local machine. return '127.0.0.1'; } return ''; } $request = Controller::curr()->getRequest(); if (!$request) { return ''; } 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 * * @return DataList|Arraylist */ public function getInitialFileToScanList() { $excludeFileIDs = ClamAVScan::get()->column('FileID'); $excludeFileIDs = array_unique($excludeFileIDs); $list = $this->getBaseFileList(); if (!$list) { return new Arraylist(); } if (!empty($excludeFileIDs)) { $list = $list->filter([ 'ID:not' => $excludeFileIDs, ]); } $ignoreBeforeDatetime = Config::inst()->get(__CLASS__, 'initial_scan_ignore_before_datetime'); if ($ignoreBeforeDatetime) { $list = $list->filter([ 'Created:GreaterThanOrEqual' => $ignoreBeforeDatetime, ]); //Debug::dump(SS_Datetime::now()); Debug::dump($ignoreBeforeDatetime); Debug::dump($list->count()); exit; } return $list; } /** * @return DataList */ public function getBaseFileList() { $list = File::get(); $list = $list->filter([ 'ClassName:not' => Folder::class, ]); return $list; } /** * Get list of files that couldn't be scanned when uploaded * due to ClamAV daemon being down or not properly configured * ie. after installation of module * * @return ArrayList|DataList */ public function getFailedToScanFileList() { $scanList = ClamAVScan::get(); $scanList = $scanList->filter([ 'IsScanned' => 0, 'Action' => ClamAVScan::ACTION_NONE, 'FileID:not' => 0, ]); $fileIDs = $scanList->column('FileID'); $fileIDs = array_unique($fileIDs); if (!$fileIDs) { return new ArrayList(); } $list = $this->getBaseFileList(); if (!$list) { return new ArrayList(); } $list = $list->filter([ 'ID' => $fileIDs, ]); return $list; } /** * @return boolean */ public function isOffline() { if ($this->_cache_isOffline !== null) { return $this->_cache_isOffline; } $result = $this->version(); $result = ($result === ClamAV::OFFLINE); return $this->_cache_isOffline = $result; } /** * @return string */ public function version() { $this->last_exception = null; try { $clamd = $this->getClamd(); $version = $clamd->version(); } catch (\ClamdSocketException $e) { $this->setLastExceptionAndLog($e); $version = self::OFFLINE; } return $version; } /** * Get the last exception caught by this. * Allows you to report the exact error to an admin/developer user in the CMS. * * @return \ClamdException */ public function getLastException() { return $this->last_exception; } } |