diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 3d7687f1..e380d77d 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -10,10 +10,10 @@ env: jobs: package: name: Package nightly release - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup krankler run: | wget https://github.com/ChristophWurst/krankerl/releases/download/v0.13.3/krankerl @@ -21,7 +21,7 @@ jobs: - name: Package app run: | ./krankerl package - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: ${{ env.APP_NAME }}.tar.gz path: build/artifacts/${{ env.APP_NAME }}.tar.gz diff --git a/lib/AppConfig.php b/lib/AppConfig.php index c99e5327..1dcc6ddf 100644 --- a/lib/AppConfig.php +++ b/lib/AppConfig.php @@ -65,6 +65,7 @@ class AppConfig { 'av_icap_chunk_size' => '1048576', 'av_icap_connect_timeout' => '5', 'av_scan_first_bytes' => -1, + 'av_block_unscannable' => false, ]; /** @@ -94,6 +95,14 @@ public function setAvIcapTls(bool $enable): void { $this->setAppValue('av_icap_tls', $enable ? '1' : '0'); } + public function getAvBlockUnscannable(): bool { + return (bool)$this->getAppValue('av_block_unscannable'); + } + + public function setAvBlockUnscannable(bool $block): void { + $this->setAppValue('av_block_unscannable', $block ? '1' : '0'); + } + /** * Get full commandline * diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index b38a31f6..79eb74b9 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -100,6 +100,8 @@ function (string $mountPoint, IStorage $storage) { $activityManager = $container->get(IManager::class); $eventDispatcher = $container->get(IEventDispatcher::class); $appManager = $container->get(IAppManager::class); + /** @var AppConfig $appConfig */ + $appConfig = $container->get(AppConfig::class); return new AvirWrapper([ 'storage' => $storage, 'scannerFactory' => $scannerFactory, @@ -110,6 +112,7 @@ function (string $mountPoint, IStorage $storage) { 'eventDispatcher' => $eventDispatcher, 'trashEnabled' => $appManager->isEnabledForUser('files_trashbin'), 'mount_point' => $mountPoint, + 'block_unscannable' => $appConfig->getAvBlockUnscannable(), ]); }, 1 diff --git a/lib/AvirWrapper.php b/lib/AvirWrapper.php index 75316d14..a9d46b8d 100644 --- a/lib/AvirWrapper.php +++ b/lib/AvirWrapper.php @@ -32,6 +32,7 @@ class AvirWrapper extends Wrapper { private bool $shouldScan = true; private bool $trashEnabled; private string $mountPoint; + private bool $blockUnscannable = false; /** * @param array $parameters @@ -45,6 +46,7 @@ public function __construct($parameters) { $this->isHomeStorage = $parameters['isHomeStorage']; $this->trashEnabled = $parameters['trashEnabled']; $this->mountPoint = $parameters['mount_point']; + $this->blockUnscannable = $parameters['block_unscannable']; /** @var IEventDispatcher $eventDispatcher */ $eventDispatcher = $parameters['eventDispatcher']; @@ -107,6 +109,9 @@ function () use ($scanner, $path) { if ($status->getNumericStatus() === Status::SCANRESULT_INFECTED) { $this->handleInfected($path, $status); } + if ($this->blockUnscannable && $status->getNumericStatus() === Status::SCANRESULT_UNSCANNABLE) { + $this->handleInfected($path, $status); + } } ); } catch (\Exception $e) { diff --git a/lib/BackgroundJob/BackgroundScanner.php b/lib/BackgroundJob/BackgroundScanner.php index 54dd62fb..4490bcd3 100644 --- a/lib/BackgroundJob/BackgroundScanner.php +++ b/lib/BackgroundJob/BackgroundScanner.php @@ -212,7 +212,7 @@ public function getUnscannedFiles() { ->andWhere($query->expr()->neq('mimetype', $query->createNamedParameter($dirMimeTypeId))) ->andWhere($query->expr()->orX( $query->expr()->like('path', $query->createNamedParameter('files/%')), - $query->expr()->notLike('s.id', $query->createNamedParameter("home::%")) + $query->expr()->notLike('s.id', $query->createNamedParameter('home::%')) )) ->andWhere($this->getSizeLimitExpression($query)) ->setMaxResults($this->getBatchSize() * 10); diff --git a/lib/Command/BackgroundScan.php b/lib/Command/BackgroundScan.php index aa216c6c..4ad696fb 100644 --- a/lib/Command/BackgroundScan.php +++ b/lib/Command/BackgroundScan.php @@ -35,7 +35,7 @@ protected function configure() { $this ->setName('files_antivirus:background-scan') ->setDescription('Run the background scan') - ->addOption('max', 'm', InputOption::VALUE_REQUIRED, "Maximum number of files to process"); + ->addOption('max', 'm', InputOption::VALUE_REQUIRED, 'Maximum number of files to process'); } protected function execute(InputInterface $input, OutputInterface $output): int { @@ -61,7 +61,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln("scanned $count files"); if ($count === $max) { - $output->writeln(" there might still be unscanned files remaining"); + $output->writeln(' there might still be unscanned files remaining'); } return 0; diff --git a/lib/Command/Mark.php b/lib/Command/Mark.php index 8ad488fe..c262bc0d 100644 --- a/lib/Command/Mark.php +++ b/lib/Command/Mark.php @@ -37,9 +37,9 @@ protected function configure() { $this ->setName('files_antivirus:mark') ->setDescription('Mark a file as scanned or unscanned') - ->addOption('forever', 'f', InputOption::VALUE_NONE, "When marking a file as scanned, set it to never rescan the file in the future") - ->addArgument('file', InputArgument::REQUIRED, "Path of the file to mark") - ->addArgument('mode', InputArgument::REQUIRED, "Either scanned or unscanned"); + ->addOption('forever', 'f', InputOption::VALUE_NONE, 'When marking a file as scanned, set it to never rescan the file in the future') + ->addArgument('file', InputArgument::REQUIRED, 'Path of the file to mark') + ->addArgument('mode', InputArgument::REQUIRED, 'Either scanned or unscanned'); } protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/lib/Command/Scan.php b/lib/Command/Scan.php index b6150bd9..2c2c76a0 100644 --- a/lib/Command/Scan.php +++ b/lib/Command/Scan.php @@ -41,8 +41,8 @@ protected function configure() { $this ->setName('files_antivirus:scan') ->setDescription('Scan a file') - ->addArgument('file', InputArgument::REQUIRED, "Path of the file to scan") - ->addOption('debug', null, InputOption::VALUE_NONE, "Enable debug output for supported backends"); + ->addArgument('file', InputArgument::REQUIRED, 'Path of the file to scan') + ->addOption('debug', null, InputOption::VALUE_NONE, 'Enable debug output for supported backends'); } protected function execute(InputInterface $input, OutputInterface $output): int { @@ -73,18 +73,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int $exit = 2; break; case \OCA\Files_Antivirus\Status::SCANRESULT_CLEAN: - $status = "is clean"; + $status = 'is clean'; $exit = 0; break; case \OCA\Files_Antivirus\Status::SCANRESULT_INFECTED: - $status = "is infected"; + $status = 'is infected'; $exit = 1; break; + case \OCA\Files_Antivirus\Status::SCANRESULT_UNSCANNABLE: + $status = 'is not scannable'; + $exit = 2; + break; } if ($result->getDetails()) { - $details = ": " . $result->getDetails(); + $details = ': ' . $result->getDetails(); } else { - $details = ""; + $details = ''; } $output->writeln("$path $status$details"); diff --git a/lib/Command/Status.php b/lib/Command/Status.php index 4c15f83b..c991c116 100644 --- a/lib/Command/Status.php +++ b/lib/Command/Status.php @@ -41,15 +41,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $unscanned = $this->backgroundScanner->getUnscannedFiles(); - $count = $this->processFiles($unscanned, $output, $verbose, "is unscanned"); + $count = $this->processFiles($unscanned, $output, $verbose, 'is unscanned'); $output->writeln("$count unscanned files"); $rescan = $this->backgroundScanner->getToRescanFiles(); - $count = $this->processFiles($rescan, $output, $verbose, "is scheduled for re-scan"); + $count = $this->processFiles($rescan, $output, $verbose, 'is scheduled for re-scan'); $output->writeln("$count files scheduled for re-scan"); $outdated = $this->backgroundScanner->getOutdatedFiles(); - $count = $this->processFiles($outdated, $output, $verbose, "has been updated"); + $count = $this->processFiles($outdated, $output, $verbose, 'has been updated'); $output->writeln("$count have been updated since the last scan"); return 0; diff --git a/lib/Command/Test.php b/lib/Command/Test.php index 152d63ae..e034ff89 100644 --- a/lib/Command/Test.php +++ b/lib/Command/Test.php @@ -40,33 +40,33 @@ protected function configure() { $this ->setName('files_antivirus:test') ->setDescription('Test the availability of the configured scanner') - ->addOption('debug', null, InputOption::VALUE_NONE, "Enable debug output for supported backends"); + ->addOption('debug', null, InputOption::VALUE_NONE, 'Enable debug output for supported backends'); } protected function execute(InputInterface $input, OutputInterface $output): int { - $output->write("Scanning regular text: "); + $output->write('Scanning regular text: '); $scanner = $this->scannerFactory->getScanner('/foo.txt'); if ($input->getOption('debug')) { - $output->writeln(""); + $output->writeln(''); $scanner->setDebugCallback(function ($content) use ($output) { $output->writeln($content); }); } - $result = $scanner->scanString("dummy scan content"); + $result = $scanner->scanString('dummy scan content'); if ($result->getNumericStatus() === Status::SCANRESULT_INFECTED) { $details = $result->getDetails(); $output->writeln("❌ $details"); return 1; } elseif ($result->getNumericStatus() === Status::SCANRESULT_UNCHECKED) { - $output->writeln("- file not scanned or scan still pending"); + $output->writeln('- file not scanned or scan still pending'); } else { - $output->writeln(""); + $output->writeln(''); } - $output->write("Scanning EICAR test file: "); + $output->write('Scanning EICAR test file: '); $scanner = $this->scannerFactory->getScanner('/test-virus-eicar.txt'); if ($input->getOption('debug')) { - $output->writeln(""); + $output->writeln(''); $scanner->setDebugCallback(function ($content) use ($output) { $output->writeln($content); }); @@ -78,17 +78,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln("❌ file not detected $details"); return 1; } elseif ($result->getNumericStatus() === Status::SCANRESULT_UNCHECKED) { - $output->writeln("- file not scanned or scan still pending"); + $output->writeln('- file not scanned or scan still pending'); + } elseif ($result->getNumericStatus() === Status::SCANRESULT_UNSCANNABLE) { + $output->writeln('- file could not be scanned'); } else { - $output->writeln(""); + $output->writeln(''); } // send a modified version of the EICAR because some scanners don't hold the scan request // by default for files that haven't been seen before. - $output->write("Scanning modified EICAR test file: "); + $output->write('Scanning modified EICAR test file: '); $scanner = $this->scannerFactory->getScanner('/test-virus-eicar-modified.txt'); if ($input->getOption('debug')) { - $output->writeln(""); + $output->writeln(''); $scanner->setDebugCallback(function ($content) use ($output) { $output->writeln($content); }); @@ -99,9 +101,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln("❌ file not detected $details"); return 1; } elseif ($result->getNumericStatus() === Status::SCANRESULT_UNCHECKED) { - $output->writeln("- file not scanned or scan still pending"); + $output->writeln('- file not scanned or scan still pending'); + } elseif ($result->getNumericStatus() === Status::SCANRESULT_UNSCANNABLE) { + $output->writeln('- file could not be scanned'); } else { - $output->writeln(""); + $output->writeln(''); } return 0; diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index abc04a48..2683087f 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -52,6 +52,7 @@ public function __construct($appName, IRequest $request, AppConfig $appconfig, I * @param int $avScanFirstBytes - scan size limit * @param string $avIcapMode * @param bool $avIcapTls + * @param bool $avBlockUnscannable * @return JSONResponse */ public function save( @@ -68,7 +69,8 @@ public function save( $avIcapMode, $avIcapRequestService, $avIcapResponseHeader, - $avIcapTls + $avIcapTls, + $avBlockUnscannable ) { $this->settings->setAvMode($avMode); $this->settings->setAvSocket($avSocket); @@ -84,10 +86,11 @@ public function save( $this->settings->setAvIcapRequestService($avIcapRequestService); $this->settings->setAvIcapResponseHeader($avIcapResponseHeader); $this->settings->setAvIcapTls((bool)$avIcapTls); + $this->settings->setAvBlockUnscannable((bool)$avBlockUnscannable); try { $scanner = $this->scannerFactory->getScanner('/self-test.txt'); - $result = $scanner->scanString("dummy scan content"); + $result = $scanner->scanString('dummy scan content'); $success = $result->getNumericStatus() == Status::SCANRESULT_CLEAN; $message = $success ? $this->l10n->t('Saved') : 'unexpected scan results for test content'; } catch (\Exception $e) { diff --git a/lib/Db/Rule.php b/lib/Db/Rule.php index 9fcad298..e1c1d630 100644 --- a/lib/Db/Rule.php +++ b/lib/Db/Rule.php @@ -43,35 +43,35 @@ class Rule extends Entity implements JsonSerializable { /** * * @var int statusType - RULE_TYPE_CODE or RULE_TYPE_MATCH defines whether - * rule should be checked by the shell exit code or regexp + * rule should be checked by the shell exit code or regexp */ protected $statusType; /** * * @var int result - shell exit code for rules - * of the type RULE_TYPE_CODE, 0 otherwise + * of the type RULE_TYPE_CODE, 0 otherwise */ protected $result; /** * * @var string match - regexp to match for rules - * of the type RULE_TYPE_MATCH, '' otherwise + * of the type RULE_TYPE_MATCH, '' otherwise */ protected $match; /** * * @var string description - shell exit code meaning for rules - * of the type RULE_TYPE_CODE, '' otherwise + * of the type RULE_TYPE_CODE, '' otherwise */ protected $description; /** * * @var int status - file check status. SCANRESULT_UNCHECKED, SCANRESULT_INFECTED, - * SCANRESULT_CLEAN are matching Unknown, Infected and Clean files accordingly. + * SCANRESULT_CLEAN are matching Unknown, Infected and Clean files accordingly. */ protected $status; diff --git a/lib/ICAP/ICAPRequest.php b/lib/ICAP/ICAPRequest.php index 69a1aba1..9bf8ae06 100644 --- a/lib/ICAP/ICAPRequest.php +++ b/lib/ICAP/ICAPRequest.php @@ -89,7 +89,7 @@ public function __construct( } if ($this->responseCallback) { - ($this->responseCallback)("ICAP Request headers:"); + ($this->responseCallback)('ICAP Request headers:'); ($this->responseCallback)($request); } @@ -107,7 +107,7 @@ public function finish(): IcapResponse { if ($this->responseCallback) { $response = stream_get_contents($this->stream); - ($this->responseCallback)("ICAP Response:"); + ($this->responseCallback)('ICAP Response:'); ($this->responseCallback)($response); $stream = fopen('php://temp', 'r+'); fwrite($stream, $response); diff --git a/lib/ICAP/ResponseParser.php b/lib/ICAP/ResponseParser.php index 5e2c3d95..7d3a3aa0 100644 --- a/lib/ICAP/ResponseParser.php +++ b/lib/ICAP/ResponseParser.php @@ -40,10 +40,10 @@ public function read_response($stream): IcapResponse { private function readIcapStatusLine($stream): IcapResponseStatus { $rawHeader = \fgets($stream); if (!$rawHeader) { - throw new RuntimeException("Empty ICAP response"); + throw new RuntimeException('Empty ICAP response'); } $icapHeader = \trim($rawHeader); - $numValues = \sscanf($icapHeader, "ICAP/%d.%d %d %s", $v1, $v2, $code, $status); + $numValues = \sscanf($icapHeader, 'ICAP/%d.%d %d %s', $v1, $v2, $code, $status); if ($numValues !== 4) { throw new RuntimeException("Unknown ICAP response: \"$icapHeader\""); } @@ -52,9 +52,9 @@ private function readIcapStatusLine($stream): IcapResponseStatus { private function parseEncapsulated(string $headerValue): array { $result = []; - $encapsulatedParts = \explode(",", $headerValue); + $encapsulatedParts = \explode(',', $headerValue); foreach ($encapsulatedParts as $encapsulatedPart) { - $pieces = \explode("=", \trim($encapsulatedPart)); + $pieces = \explode('=', \trim($encapsulatedPart)); $result[$pieces[0]] = (int)$pieces[1]; } return $result; @@ -85,7 +85,7 @@ private function readHeaders($stream): array { $headers = []; while (($headerString = \fgets($stream)) !== false) { $trimmedHeaderString = \trim($headerString); - if ($trimmedHeaderString === "") { + if ($trimmedHeaderString === '') { break; } [$headerName, $headerValue] = $this->parseHeader($trimmedHeaderString); diff --git a/lib/Scanner/ExternalClam.php b/lib/Scanner/ExternalClam.php index 738609b3..d919c44c 100644 --- a/lib/Scanner/ExternalClam.php +++ b/lib/Scanner/ExternalClam.php @@ -69,7 +69,7 @@ protected function shutdownScanner() { if ($info['timed_out']) { $this->status->setNumericStatus(Status::SCANRESULT_UNCHECKED); - $this->status->setDetails("Socket timed out while scanning"); + $this->status->setDetails('Socket timed out while scanning'); } else { $this->status->parseResponse($response); } diff --git a/lib/Scanner/ExternalKaspersky.php b/lib/Scanner/ExternalKaspersky.php index 84888111..35e87317 100644 --- a/lib/Scanner/ExternalKaspersky.php +++ b/lib/Scanner/ExternalKaspersky.php @@ -42,7 +42,7 @@ public function initScanner() { if (!($avHost && $avPort)) { throw new \RuntimeException('The Kaspersky port and host are not set up.'); } - $this->writeHandle = fopen("php://temp", 'w+'); + $this->writeHandle = fopen('php://temp', 'w+'); } /** @@ -51,7 +51,7 @@ public function initScanner() { protected function writeChunk($chunk) { if (ftell($this->writeHandle) > $this->chunkSize) { $this->scanBuffer(); - $this->writeHandle = fopen("php://temp", 'w+'); + $this->writeHandle = fopen('php://temp', 'w+'); } parent::writeChunk($chunk); } @@ -66,7 +66,7 @@ protected function scanBuffer(): void { $body = base64_encode($body); $response = $this->clientService->newClient()->post("$avHost:$avPort/api/v3.0/scanmemory", [ 'json' => [ - 'timeout' => "60000", + 'timeout' => '60000', 'object' => $body, ], 'connect_timeout' => 5, @@ -85,14 +85,14 @@ protected function scanBuffer(): void { } elseif (substr($scanResult, 0, 11) === 'NON_SCANNED' && $this->status->getNumericStatus() != Status::SCANRESULT_INFECTED) { if ($scanResult === 'NON_SCANNED (PASSWORD PROTECTED)') { // if we can't scan the file at all, there is no use in trying to scan it again later - $this->status->setNumericStatus(Status::SCANRESULT_CLEAN); + $this->status->setNumericStatus(Status::SCANRESULT_UNSCANNABLE); } else { $this->status->setNumericStatus(Status::SCANRESULT_UNCHECKED); } $this->status->setDetails($scanResult); } else { $this->status->setNumericStatus(Status::SCANRESULT_INFECTED); - if (strpos($scanResult, "DETECT ") === 0) { + if (strpos($scanResult, 'DETECT ') === 0) { $scanResult = substr($scanResult, 7); } if (isset($response['detectionName'])) { diff --git a/lib/Scanner/ICAP.php b/lib/Scanner/ICAP.php index 73caf8ff..d89c8e16 100644 --- a/lib/Scanner/ICAP.php +++ b/lib/Scanner/ICAP.php @@ -56,31 +56,31 @@ public function __construct( public function initScanner() { parent::initScanner(); - $this->writeHandle = fopen("php://temp", 'w+'); + $this->writeHandle = fopen('php://temp', 'w+'); $path = '/' . trim($this->path, '/'); if (str_contains($path, '.ocTransferId') && str_ends_with($path, '.part')) { [$path] = explode('.ocTransferId', $path, 2); } $remote = $this->request ? $this->request->getRemoteAddress() : null; - $encodedPath = implode("/", array_map("rawurlencode", explode("/", $path))); + $encodedPath = implode('/', array_map('rawurlencode', explode('/', $path))); if ($this->mode === ICAPClient::MODE_REQ_MOD) { $this->icapRequest = $this->icapClient->reqmod($this->service, [ 'Allow' => 204, - "X-Client-IP" => $remote, + 'X-Client-IP' => $remote, ], [ "PUT $encodedPath HTTP/1.0", - "Host: nextcloud" + 'Host: nextcloud' ]); } else { $this->icapRequest = $this->icapClient->respmod($this->service, [ 'Allow' => 204, - "X-Client-IP" => $remote, + 'X-Client-IP' => $remote, ], [ "GET $encodedPath HTTP/1.0", - "Host: nextcloud", + 'Host: nextcloud', ], [ - "HTTP/1.0 200 OK", - "Content-Length: 1", // a dummy, non-zero, content length seems to be enough + 'HTTP/1.0 200 OK', + 'Content-Length: 1', // a dummy, non-zero, content length seems to be enough ]); } } @@ -96,7 +96,7 @@ private function flushBuffer() { rewind($this->writeHandle); $data = stream_get_contents($this->writeHandle); $this->icapRequest->write($data); - $this->writeHandle = fopen("php://temp", 'w+'); + $this->writeHandle = fopen('php://temp', 'w+'); } protected function scanBuffer() { @@ -105,9 +105,10 @@ protected function scanBuffer() { $code = $response->getStatus()->getCode(); $this->status->setNumericStatus(Status::SCANRESULT_CLEAN); + $icapHeaders = $response->getIcapHeaders(); if ($code === 200 || $code === 204) { // c-icap/clamav reports this header - $virus = $response->getIcapHeaders()[$this->virusHeader] ?? false; + $virus = $icapHeaders[$this->virusHeader] ?? false; if ($virus) { $this->status->setNumericStatus(Status::SCANRESULT_INFECTED); $this->status->setDetails($virus); @@ -120,6 +121,17 @@ protected function scanBuffer() { } } elseif ($code === 202) { $this->status->setNumericStatus(Status::SCANRESULT_UNCHECKED); + } elseif ($code === 500 && isset($icapHeaders['X-Error-Code'])) { + $uncheckableErrors = ['decode_error', 'max_archive_layers_exceeded', 'password_protected']; + $blockedErrors = ['file_type_blocked', 'file_extension_blocked']; + $icapErrorCode = $icapHeaders['X-Error-Code']; + if (in_array($icapErrorCode, $uncheckableErrors)) { + $this->status->setNumericStatus(Status::SCANRESULT_UNSCANNABLE); + } elseif (in_array($icapErrorCode, $blockedErrors)) { + $this->status->setNumericStatus(Status::SCANRESULT_INFECTED); + } else { + throw new \RuntimeException('Invalid response from ICAP server, got error code ' . $icapErrorCode); + } } else { throw new \RuntimeException('Invalid response from ICAP server'); } diff --git a/lib/Scanner/ScannerBase.php b/lib/Scanner/ScannerBase.php index 46c08dbf..fb21b7d8 100644 --- a/lib/Scanner/ScannerBase.php +++ b/lib/Scanner/ScannerBase.php @@ -28,7 +28,7 @@ abstract class ScannerBase implements IScanner { protected int $byteCount; - /** @var resource */ + /** @var resource */ protected $writeHandle; protected AppConfig $appConfig; diff --git a/lib/Status.php b/lib/Status.php index d01beb24..1a7c6b01 100644 --- a/lib/Status.php +++ b/lib/Status.php @@ -12,23 +12,28 @@ use Psr\Log\LoggerInterface; class Status { - /* + /** * The file was not checked (e.g. because the AV daemon wasn't running). */ public const SCANRESULT_UNCHECKED = -1; - /* + /** * The file was checked and found to be clean. */ public const SCANRESULT_CLEAN = 0; - /* + /** * The file was checked and found to be infected. */ public const SCANRESULT_INFECTED = 1; - /* - * Should be SCANRESULT_UNCHECKED | SCANRESULT_INFECTED | SCANRESULT_CLEAN + /** + * The file cannot be checked (e.g. because it is password protected) + */ + public const SCANRESULT_UNSCANNABLE = 2; + + /** + * Should be SCANRESULT_UNCHECKED | SCANRESULT_INFECTED | SCANRESULT_CLEAN | SCANRESULT_UNSCANNABLE */ protected $numericStatus = self::SCANRESULT_UNCHECKED; @@ -37,12 +42,15 @@ class Status { */ protected $details = ''; + private bool $blockUnscannable = false; + protected RuleMapper $ruleMapper; protected LoggerInterface $logger; - public function __construct(RuleMapper $ruleMapper, LoggerInterface $logger) { + public function __construct(RuleMapper $ruleMapper, LoggerInterface $logger, AppConfig $config) { $this->ruleMapper = $ruleMapper; $this->logger = $logger; + $this->blockUnscannable = $config->getAvBlockUnscannable(); } /** @@ -71,18 +79,18 @@ public function setDetails(string $details): void { /** * @param string $rawResponse - * @param integer $result + * @param ?integer $result * * @return void */ - public function parseResponse($rawResponse, $result = null) { + public function parseResponse(string $rawResponse, ?int $result = null) { $matches = []; if (is_null($result)) { // Daemon or socket mode try { $allRules = $this->getResponseRules(); } catch (\Exception $e) { - $this->logger->error(__METHOD__.', exception: '.$e->getMessage(), ['app' => 'files_antivirus']); + $this->logger->error(__METHOD__ . ', exception: ' . $e->getMessage(), ['app' => 'files_antivirus']); return; } @@ -106,7 +114,7 @@ public function parseResponse($rawResponse, $result = null) { // Adding the ASCII text range 32..126 (excluding '`') of the raw socket response to the details. $response = filter_var($rawResponse, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK); if (strlen($response) > 512) { - $response = substr($response, 0, 509) . "..."; + $response = substr($response, 0, 509) . '...'; } $this->details = 'No matching rule for response [' . $response . ']. Please check antivirus rules configuration.'; } @@ -132,7 +140,7 @@ public function parseResponse($rawResponse, $result = null) { break; case self::SCANRESULT_UNCHECKED: if (!$this->details) { - $this->details = 'No matching rule for exit code ' . $this->numericStatus .'. Please check antivirus rules configuration.' ; + $this->details = 'No matching rule for exit code ' . $this->numericStatus . '. Please check antivirus rules configuration.' ; } } } @@ -165,6 +173,13 @@ public function dispatch(Item $item): void { case self::SCANRESULT_CLEAN: $item->processClean(); break; + case self::SCANRESULT_UNSCANNABLE: + if ($this->blockUnscannable) { + $item->processInfected($this); + } else { + $item->processClean(); + } + break; } } } diff --git a/lib/StatusFactory.php b/lib/StatusFactory.php index 8363f28a..e3c907a2 100644 --- a/lib/StatusFactory.php +++ b/lib/StatusFactory.php @@ -12,16 +12,23 @@ class StatusFactory { private RuleMapper $ruleMapper; private LoggerInterface $logger; + private AppConfig $config; - public function __construct(RuleMapper $ruleMapper, LoggerInterface $logger) { + public function __construct( + RuleMapper $ruleMapper, + LoggerInterface $logger, + AppConfig $config + ) { $this->ruleMapper = $ruleMapper; $this->logger = $logger; + $this->config = $config; } public function newStatus(): Status { return new Status( $this->ruleMapper, - $this->logger + $this->logger, + $this->config, ); } } diff --git a/templates/settings.php b/templates/settings.php index 7424d5e3..699dce23 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -121,6 +121,15 @@ + + + + "" + title="t('Block unscannable files (such as encrypted archives)'));?>" + /> + + + diff --git a/tests/AvirWrapperTest.php b/tests/AvirWrapperTest.php index dbfc44f9..86266178 100644 --- a/tests/AvirWrapperTest.php +++ b/tests/AvirWrapperTest.php @@ -76,6 +76,7 @@ protected function setUp(): void { 'eventDispatcher' => $this->createMock(EventDispatcherInterface::class), 'trashEnabled' => true, 'mount_point' => '/' . self::UID . '/files/', + 'block_unscannable' => false, ]); $this->config->expects($this->any()) diff --git a/tests/BackgroundScannerTest.php b/tests/BackgroundScannerTest.php index 773cf351..d9756d24 100644 --- a/tests/BackgroundScannerTest.php +++ b/tests/BackgroundScannerTest.php @@ -39,7 +39,7 @@ class BackgroundScannerTest extends TestBase { protected function setUp(): void { parent::setUp(); - $this->createUser("av", "av"); + $this->createUser('av', 'av'); $storage = new TemporaryHome(); $storage->mkdir('files'); $storage->getScanner()->scan(''); @@ -47,13 +47,13 @@ protected function setUp(): void { $external = new Temporary(); $external->getScanner()->scan(''); - $this->registerMount("av", $storage, "av"); - $this->registerMount("av", $external, "av/files/external"); + $this->registerMount('av', $storage, 'av'); + $this->registerMount('av', $external, 'av/files/external'); - $this->loginAsUser("av"); + $this->loginAsUser('av'); /** @var IRootFolder $root */ $root = \OC::$server->get(IRootFolder::class); - $this->homeDirectory = $root->getUserFolder("av"); + $this->homeDirectory = $root->getUserFolder('av'); $this->externalDirectory = $this->homeDirectory->get('external'); } @@ -87,7 +87,7 @@ private function getBackgroundScanner(): BackgroundScanner { ->disableOriginalConstructor() ->onlyMethods(['getNumericStatus', 'getDetails'])->getMock(); $status->method('getNumericStatus')->willReturn(Status::SCANRESULT_CLEAN); - $status->method('getDetails')->willReturn(""); + $status->method('getDetails')->willReturn(''); $scanner = $this->createMock(IScanner::class); $scanner->method('scan') ->willReturn($status); @@ -123,8 +123,8 @@ public function testGetUnscannedFiles() { $this->markAllScanned(); $scanner = $this->getBackgroundScanner(); - $newFileId = $this->homeDirectory->newFile("foo", "bar")->getId(); - $this->homeDirectory->getParent()->newFile("outside", "bar")->getId(); + $newFileId = $this->homeDirectory->newFile('foo', 'bar')->getId(); + $this->homeDirectory->getParent()->newFile('outside', 'bar')->getId(); $outdated = iterator_to_array($scanner->getUnscannedFiles()); $this->assertEquals([$newFileId], $outdated); @@ -134,14 +134,14 @@ public function testGetUnscannedFilesExternal() { $this->markAllScanned(); $scanner = $this->getBackgroundScanner(); - $newFileId = $this->homeDirectory->newFile("external/foo2", "bar2")->getId(); + $newFileId = $this->homeDirectory->newFile('external/foo2', 'bar2')->getId(); $outdated = iterator_to_array($scanner->getUnscannedFiles()); $this->assertEquals([$newFileId], $outdated); } public function testGetOutdatedFiles() { - $newFileId = $this->homeDirectory->newFile("foo", "bar")->getId(); + $newFileId = $this->homeDirectory->newFile('foo', 'bar')->getId(); $this->markAllScanned(); $scanner = $this->getBackgroundScanner(); @@ -158,7 +158,7 @@ public function testTestScanFiles() { $this->markAllScanned(); $scanner = $this->getBackgroundScanner(); - $newFileId = $this->homeDirectory->newFile("foo", "bar")->getId(); + $newFileId = $this->homeDirectory->newFile('foo', 'bar')->getId(); $outdated = iterator_to_array($scanner->getUnscannedFiles()); $this->assertEquals([$newFileId], $outdated); diff --git a/tests/Db/RuleTest.php b/tests/Db/RuleTest.php index ba4445d2..de520ffd 100644 --- a/tests/Db/RuleTest.php +++ b/tests/Db/RuleTest.php @@ -22,7 +22,7 @@ public function testJsonSerialize() { 'statusType' => Rule::RULE_TYPE_CODE, 'result' => 0, 'match' => '', - 'description' => "", + 'description' => '', 'status' => Status::SCANRESULT_CLEAN ]; $expected = [ @@ -30,7 +30,7 @@ public function testJsonSerialize() { 'status_type' => Rule::RULE_TYPE_CODE, 'result' => 0, 'match' => '', - 'description' => "", + 'description' => '', 'status' => Status::SCANRESULT_CLEAN ]; diff --git a/tests/DummyClam.php b/tests/DummyClam.php index b907278e..aeaf4b1e 100644 --- a/tests/DummyClam.php +++ b/tests/DummyClam.php @@ -74,7 +74,7 @@ protected function handleConnection($connection) { if (!$isAborted) { $response = strpos($buffer, self::TEST_SIGNATURE) !== false - ? "Ohoho: Criminal.Joboholic FOUND" + ? 'Ohoho: Criminal.Joboholic FOUND' : 'Scanned OK' ; //echo str_replace('0', '', $buffer) . $response; diff --git a/tests/ICAP/ICAPClientTest.php b/tests/ICAP/ICAPClientTest.php index a29e2ac2..1f7be0c6 100644 --- a/tests/ICAP/ICAPClientTest.php +++ b/tests/ICAP/ICAPClientTest.php @@ -15,7 +15,7 @@ class ICAPClientTest extends TestCase { public function testConnect_ShouldThrowRuntimeException() { $this->expectException(\RuntimeException::class); $this->expectExceptionMessageMatches('/Cannot connect to "tcp\:\/\/nothinghere\:8080"\: .*/'); - $icapClient = new ICAPClient("nothinghere", 8080, 2); - $icapClient->respmod("myservice", [], [], []); + $icapClient = new ICAPClient('nothinghere', 8080, 2); + $icapClient->respmod('myservice', [], [], []); } } diff --git a/tests/Scanner/ExternalKasperskyTest.php b/tests/Scanner/ExternalKasperskyTest.php index b5f4a7cc..9018b0af 100644 --- a/tests/Scanner/ExternalKasperskyTest.php +++ b/tests/Scanner/ExternalKasperskyTest.php @@ -21,7 +21,7 @@ class ExternalKasperskyTest extends ScannerBaseTest { protected function getScanner(): ScannerBase { if (!getenv('KASPERSKY_HOST') || !getenv('KASPERSKY_PORT')) { - $this->markTestSkipped("Set KASPERSKY_HOST and KASPERSKY_PORT to enable kaspersky tests"); + $this->markTestSkipped('Set KASPERSKY_HOST and KASPERSKY_PORT to enable kaspersky tests'); } $logger = $this->createMock(LoggerInterface::class); diff --git a/tests/Scanner/ICAPTest.php b/tests/Scanner/ICAPTest.php index 48cd89eb..de3dd767 100644 --- a/tests/Scanner/ICAPTest.php +++ b/tests/Scanner/ICAPTest.php @@ -20,7 +20,7 @@ class ICAPTest extends ScannerBaseTest { protected function getScanner(): ScannerBase { if (!getenv('ICAP_HOST') || !getenv('ICAP_PORT') || !getenv('ICAP_REQUEST') || !getenv('ICAP_HEADER') || !getenv('ICAP_MODE')) { - $this->markTestSkipped("Set ICAP_HOST, ICAP_PORT, ICAP_REQUEST, ICAP_MODE and ICAP_HEADER to enable icap tests"); + $this->markTestSkipped('Set ICAP_HOST, ICAP_PORT, ICAP_REQUEST, ICAP_MODE and ICAP_HEADER to enable icap tests'); } $logger = $this->createMock(LoggerInterface::class); diff --git a/tests/Scanner/ScannerBaseTest.php b/tests/Scanner/ScannerBaseTest.php index 76a42681..53ed838b 100644 --- a/tests/Scanner/ScannerBaseTest.php +++ b/tests/Scanner/ScannerBaseTest.php @@ -17,7 +17,7 @@ abstract protected function getScanner(): ScannerBase; public function testScanClean() { $scanner = $this->getScanner(); - $status = $scanner->scanString("foo"); + $status = $scanner->scanString('foo'); $this->assertEquals($status->getNumericStatus(), Status::SCANRESULT_CLEAN); } public function testScanEicar() { diff --git a/tests/StatusTest.php b/tests/StatusTest.php index 47262a7e..1e460d7d 100644 --- a/tests/StatusTest.php +++ b/tests/StatusTest.php @@ -15,46 +15,51 @@ * @group DB */ class StatusTest extends TestBase { - + // See OCA\Files_Antivirus\Status::init for details public const TEST_CLEAN = 0; public const TEST_INFECTED = 1; public const TEST_ERROR = 40; - - protected $ruleMapper; + protected RuleMapper $ruleMapper; + private bool $blockUnscannable = false; protected function setUp(): void { parent::setUp(); $this->ruleMapper = new RuleMapper($this->db); $this->ruleMapper->deleteAll(); $this->ruleMapper->populate(); + $this->config->method('getAvBlockUnscannable') + ->willReturnCallback(function () { + return $this->blockUnscannable; + }); } - + public function testParseResponse() { // Testing status codes $testStatus = new \OCA\Files_Antivirus\Status( $this->ruleMapper, - $this->createMock(LoggerInterface::class) + $this->createMock(LoggerInterface::class), + $this->config, ); - + $testStatus->parseResponse('dummy : OK', self::TEST_CLEAN); $cleanScan = $testStatus->getNumericStatus(); $this->assertEquals(\OCA\Files_Antivirus\Status::SCANRESULT_CLEAN, $cleanScan); - $this->assertEquals("", $testStatus->getDetails()); - - $scanOutput = "Thu Oct 28 13:02:19 2010 -> /tmp/kitten: Heuristics.Broken.Executable FOUND "; + $this->assertEquals('', $testStatus->getDetails()); + + $scanOutput = 'Thu Oct 28 13:02:19 2010 -> /tmp/kitten: Heuristics.Broken.Executable FOUND '; $testStatus->parseResponse($scanOutput, self::TEST_INFECTED); $infectedScan = $testStatus->getNumericStatus(); $this->assertEquals(\OCA\Files_Antivirus\Status::SCANRESULT_INFECTED, $infectedScan); $this->assertEquals('Heuristics.Broken.Executable', $testStatus->getDetails()); - + $testStatus->parseResponse('dummy', self::TEST_ERROR); $failedScan = $testStatus->getNumericStatus(); $this->assertEquals(\OCA\Files_Antivirus\Status::SCANRESULT_UNCHECKED, $failedScan); $this->assertEquals('Unknown option passed.', $testStatus->getDetails()); - - + + // Testing raw output (e.g. daemon mode) $assertDetailsWithResponse = function ($response) use ($testStatus) { $expected = "No matching rule for response [$response]. Please check antivirus rules configuration."; @@ -66,32 +71,32 @@ public function testParseResponse() { $failedScan2 = $testStatus->getNumericStatus(); $this->assertEquals(\OCA\Files_Antivirus\Status::SCANRESULT_UNCHECKED, $failedScan2); $assertDetailsWithResponse(''); - + // No rules matched result is unknown too $testStatus->parseResponse('123dc'); $failedScan3 = $testStatus->getNumericStatus(); $this->assertEquals(\OCA\Files_Antivirus\Status::SCANRESULT_UNCHECKED, $failedScan3); $assertDetailsWithResponse('123dc'); - + // Raw result is added to details when no rule matched (only ASCII text range 32..126 excluding '`'). for ($c = 0; $c < 256; $c++) { $testStatus->parseResponse(chr($c)); $expected = $c < 32 || $c > 126 || chr($c) == '`' ? '' : chr($c); $assertDetailsWithResponse($expected); } - + // Raw result in details is truncated at 512 chars. $testStatus->parseResponse(str_repeat('a', 512)); $assertDetailsWithResponse(str_repeat('a', 512)); $testStatus->parseResponse(str_repeat('a', 513)); $assertDetailsWithResponse(str_repeat('a', 509) . '...'); - + // File is clean $testStatus->parseResponse('Thu Oct 28 13:02:19 2010 -> /tmp/kitten : OK'); $cleanScan2 = $testStatus->getNumericStatus(); $this->assertEquals(\OCA\Files_Antivirus\Status::SCANRESULT_CLEAN, $cleanScan2); $this->assertEquals('', $testStatus->getDetails()); - + // File is infected $testStatus->parseResponse('Thu Oct 28 13:02:19 2010 -> /tmp/kitten: Heuristics.Broken.Kitten FOUND'); $infectedScan2 = $testStatus->getNumericStatus(); diff --git a/tests/TemporaryHome.php b/tests/TemporaryHome.php index 09c20f81..1af2b9e0 100644 --- a/tests/TemporaryHome.php +++ b/tests/TemporaryHome.php @@ -18,7 +18,7 @@ public function __construct($arguments = null) { } public function getId() { - return "home::" . $this->id; + return 'home::' . $this->id; } } diff --git a/tests/TestBase.php b/tests/TestBase.php index 4d22b78c..b5a008ae 100644 --- a/tests/TestBase.php +++ b/tests/TestBase.php @@ -39,7 +39,7 @@ protected function setUp(): void { $this->config = $this->getMockBuilder(AppConfig::class) ->disableOriginalConstructor() - ->setMethods(['getAvPath', 'getAvChunkSize', 'getAvMode', 'getAppValue', 'getAvHost', 'getAvPort']) + ->setMethods(['getAvPath', 'getAvChunkSize', 'getAvMode', 'getAppValue', 'getAvHost', 'getAvPort', 'getAvBlockUnscannable']) ->getMock(); $this->config->expects($this->any()) @@ -62,8 +62,8 @@ protected function setUp(): void { ->will($this->returnValue('5555')); $this->l10n = $this->getMockBuilder(IL10N::class) - ->disableOriginalConstructor() - ->getMock(); + ->disableOriginalConstructor() + ->getMock(); $this->l10n->method('t')->will($this->returnArgument(0)); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 0499cc64..09e4c540 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -13,7 +13,7 @@ define('PHPUNIT_RUN', 1); } -require_once __DIR__.'/../../../lib/base.php'; +require_once __DIR__ . '/../../../lib/base.php'; if (!class_exists('PHPUnit_Framework_TestCase') && !class_exists('\PHPUnit\Framework\TestCase')) { require_once('PHPUnit/Autoload.php');