From 6f1cae6003a74883d7a3fde9665360c76bad74a2 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Tue, 4 Jun 2024 13:14:41 +0200 Subject: [PATCH 1/2] apply nextcloud coding standards --- .php-cs-fixer.php | 7 +- appinfo/routes.php | 32 +-- lib/Activity/Provider.php | 212 +++++++-------- lib/AppInfo/Application.php | 134 +++++----- lib/AvirWrapper.php | 28 +- lib/BackgroundJobs/ScanJob.php | 127 +++++---- lib/BackgroundJobs/TagUnscannedJob.php | 107 ++++---- lib/Controller/ScanController.php | 75 +++--- lib/Controller/SettingsController.php | 160 +++++------ lib/Db/DbFileMapper.php | 159 ++++++----- lib/Service/FileService.php | 178 ++++++------- lib/Service/TagService.php | 344 ++++++++++++------------ lib/Service/VerdictService.php | 351 ++++++++++++------------- lib/Settings/VaasAdmin.php | 81 +++--- lib/Settings/VaasAdminSection.php | 36 ++- templates/admin.php | 4 +- 16 files changed, 984 insertions(+), 1051 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 1a21f492..19e79aba 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -1,5 +1,10 @@ in(__DIR__) ->exclude([ @@ -9,6 +14,6 @@ ]) ; -return (new PhpCsFixer\Config()) +return (new Config()) ->setFinder($finder) ; \ No newline at end of file diff --git a/appinfo/routes.php b/appinfo/routes.php index fc7f5a5f..00d1f825 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -5,20 +5,20 @@ // SPDX-License-Identifier: AGPL-3.0-or-later return [ - 'resources' => [], - 'routes' => [ - ['name' => 'scan#scan', 'url' => '/scan', 'verb' => 'POST'], - ['name' => 'settings#setconfig', 'url' => '/setconfig', 'verb' => 'POST'], - ['name' => 'settings#setadvancedconfig', 'url' => '/setadvancedconfig', 'verb' => 'POST'], - ['name' => 'settings#setAutoScan', 'url' => '/setAutoScan', 'verb' => 'POST'], - ['name' => 'settings#getAutoScan', 'url' => '/getAutoScan', 'verb' => 'GET'], - ['name' => 'settings#setScanOnlyNewFiles', 'url' => '/setScanOnlyNewFiles', 'verb' => 'POST'], - ['name' => 'settings#getScanOnlyNewFiles', 'url' => '/getScanOnlyNewFiles', 'verb' => 'GET'], - ['name' => 'settings#setPrefixMalicious', 'url' => '/setPrefixMalicious', 'verb' => 'POST'], - ['name' => 'settings#getPrefixMalicious', 'url' => '/getPrefixMalicious', 'verb' => 'GET'], - ['name' => 'settings#getAuthMethod', 'url' => '/getAuthMethod', 'verb' => 'GET'], - ['name' => 'settings#setDisableUnscannedTag', 'url' => '/setDisableUnscannedTag', 'verb' => 'POST'], - ['name' => 'settings#getDisableUnscannedTag', 'url' => '/getDisableUnscannedTag', 'verb' => 'GET'], - ['name' => 'settings#resetAllTags', 'url' => '/resetalltags', 'verb' => 'POST'] - ] + 'resources' => [], + 'routes' => [ + ['name' => 'scan#scan', 'url' => '/scan', 'verb' => 'POST'], + ['name' => 'settings#setconfig', 'url' => '/setconfig', 'verb' => 'POST'], + ['name' => 'settings#setadvancedconfig', 'url' => '/setadvancedconfig', 'verb' => 'POST'], + ['name' => 'settings#setAutoScan', 'url' => '/setAutoScan', 'verb' => 'POST'], + ['name' => 'settings#getAutoScan', 'url' => '/getAutoScan', 'verb' => 'GET'], + ['name' => 'settings#setScanOnlyNewFiles', 'url' => '/setScanOnlyNewFiles', 'verb' => 'POST'], + ['name' => 'settings#getScanOnlyNewFiles', 'url' => '/getScanOnlyNewFiles', 'verb' => 'GET'], + ['name' => 'settings#setPrefixMalicious', 'url' => '/setPrefixMalicious', 'verb' => 'POST'], + ['name' => 'settings#getPrefixMalicious', 'url' => '/getPrefixMalicious', 'verb' => 'GET'], + ['name' => 'settings#getAuthMethod', 'url' => '/getAuthMethod', 'verb' => 'GET'], + ['name' => 'settings#setDisableUnscannedTag', 'url' => '/setDisableUnscannedTag', 'verb' => 'POST'], + ['name' => 'settings#getDisableUnscannedTag', 'url' => '/getDisableUnscannedTag', 'verb' => 'GET'], + ['name' => 'settings#resetAllTags', 'url' => '/resetalltags', 'verb' => 'POST'] + ] ]; diff --git a/lib/Activity/Provider.php b/lib/Activity/Provider.php index 05f8b7ea..f6fc5fb7 100644 --- a/lib/Activity/Provider.php +++ b/lib/Activity/Provider.php @@ -30,110 +30,110 @@ use Psr\Log\LoggerInterface; class Provider implements IProvider { - public const TYPE_VIRUS_DETECTED = 'virus_detected'; - - public const SUBJECT_VIRUS_DETECTED = 'virus_detected'; - public const SUBJECT_VIRUS_DETECTED_UPLOAD = 'virus_detected_upload'; - public const SUBJECT_VIRUS_DETECTED_SCAN = 'virus_detected_scan'; - - public const MESSAGE_FILE_DELETED = 'file_deleted'; - - /** @var IFactory */ - private $languageFactory; - - /** @var IURLGenerator */ - private $urlGenerator; - private LoggerInterface $logger; - - public function __construct(IFactory $languageFactory, IURLGenerator $urlGenerator, LoggerInterface $logger) { - $this->languageFactory = $languageFactory; - $this->urlGenerator = $urlGenerator; - $this->logger = $logger; - } - - public function parse($language, IEvent $event, IEvent $previousEvent = null) { - if ($event->getApp() !== Application::APP_ID || $event->getType() !== self::TYPE_VIRUS_DETECTED) { - throw new \InvalidArgumentException(); - } - - $parameters = []; - $subject = ''; - - if ($event->getSubject() === self::SUBJECT_VIRUS_DETECTED) { - $subject = 'File {file} is infected with {virus}'; - - $params = $event->getSubjectParameters(); - $parameters['virus'] = [ - 'type' => 'highlight', - 'id' => $params[1], - 'name' => $params[1], - ]; - - $parameters['file'] = [ - 'type' => 'highlight', - 'id' => $event->getObjectName(), - 'name' => basename($event->getObjectName()), - ]; - $event->setIcon($this->urlGenerator->imagePath('gdatavaas', 'favicon.svg')); - - if ($event->getMessage() === self::MESSAGE_FILE_DELETED) { - $event->setParsedMessage('The file has been removed'); - } - } elseif ($event->getSubject() === self::SUBJECT_VIRUS_DETECTED_UPLOAD) { - $subject = 'File containing {virus} detected'; - - $params = $event->getSubjectParameters(); - $parameters['virus'] = [ - 'type' => 'highlight', - 'id' => $params[0], - 'name' => $params[0], - ]; - - $event->setParsedSubject($subject); - $event->setRichSubject($subject); - $event->setIcon($this->urlGenerator->imagePath('gdatavaas', 'favicon.svg')); - - if ($event->getMessage() === self::MESSAGE_FILE_DELETED) { - $event->setParsedMessage('The file has been removed'); - } - } elseif ($event->getSubject() === self::SUBJECT_VIRUS_DETECTED_SCAN) { - $subject = 'File {file} is infected with {virus}'; - - $params = $event->getSubjectParameters(); - $parameters['virus'] = [ - 'type' => 'highlight', - 'id' => $params[0], - 'name' => $params[0], - ]; - $parameters['file'] = [ - 'type' => 'highlight', - 'id' => $event->getObjectName(), - 'name' => $event->getObjectName(), - ]; - $event->setIcon($this->urlGenerator->imagePath('gdatavaas', 'favicon.svg')); - - if ($event->getMessage() === self::MESSAGE_FILE_DELETED) { - $event->setParsedMessage('The file has been removed'); - } - } - - $this->setSubjects($event, $subject, $parameters); - - return $event; - } - - private function setSubjects(IEvent $event, string $subject, array $parameters): void { - $placeholders = $replacements = []; - foreach ($parameters as $placeholder => $parameter) { - $placeholders[] = '{' . $placeholder . '}'; - if ($parameter['type'] === 'file') { - $replacements[] = $parameter['path']; - } else { - $replacements[] = $parameter['name']; - } - } - - $event->setParsedSubject(str_replace($placeholders, $replacements, $subject)) - ->setRichSubject($subject, $parameters); - } + public const TYPE_VIRUS_DETECTED = 'virus_detected'; + + public const SUBJECT_VIRUS_DETECTED = 'virus_detected'; + public const SUBJECT_VIRUS_DETECTED_UPLOAD = 'virus_detected_upload'; + public const SUBJECT_VIRUS_DETECTED_SCAN = 'virus_detected_scan'; + + public const MESSAGE_FILE_DELETED = 'file_deleted'; + + /** @var IFactory */ + private $languageFactory; + + /** @var IURLGenerator */ + private $urlGenerator; + private LoggerInterface $logger; + + public function __construct(IFactory $languageFactory, IURLGenerator $urlGenerator, LoggerInterface $logger) { + $this->languageFactory = $languageFactory; + $this->urlGenerator = $urlGenerator; + $this->logger = $logger; + } + + public function parse($language, IEvent $event, ?IEvent $previousEvent = null) { + if ($event->getApp() !== Application::APP_ID || $event->getType() !== self::TYPE_VIRUS_DETECTED) { + throw new \InvalidArgumentException(); + } + + $parameters = []; + $subject = ''; + + if ($event->getSubject() === self::SUBJECT_VIRUS_DETECTED) { + $subject = 'File {file} is infected with {virus}'; + + $params = $event->getSubjectParameters(); + $parameters['virus'] = [ + 'type' => 'highlight', + 'id' => $params[1], + 'name' => $params[1], + ]; + + $parameters['file'] = [ + 'type' => 'highlight', + 'id' => $event->getObjectName(), + 'name' => basename($event->getObjectName()), + ]; + $event->setIcon($this->urlGenerator->imagePath('gdatavaas', 'favicon.svg')); + + if ($event->getMessage() === self::MESSAGE_FILE_DELETED) { + $event->setParsedMessage('The file has been removed'); + } + } elseif ($event->getSubject() === self::SUBJECT_VIRUS_DETECTED_UPLOAD) { + $subject = 'File containing {virus} detected'; + + $params = $event->getSubjectParameters(); + $parameters['virus'] = [ + 'type' => 'highlight', + 'id' => $params[0], + 'name' => $params[0], + ]; + + $event->setParsedSubject($subject); + $event->setRichSubject($subject); + $event->setIcon($this->urlGenerator->imagePath('gdatavaas', 'favicon.svg')); + + if ($event->getMessage() === self::MESSAGE_FILE_DELETED) { + $event->setParsedMessage('The file has been removed'); + } + } elseif ($event->getSubject() === self::SUBJECT_VIRUS_DETECTED_SCAN) { + $subject = 'File {file} is infected with {virus}'; + + $params = $event->getSubjectParameters(); + $parameters['virus'] = [ + 'type' => 'highlight', + 'id' => $params[0], + 'name' => $params[0], + ]; + $parameters['file'] = [ + 'type' => 'highlight', + 'id' => $event->getObjectName(), + 'name' => $event->getObjectName(), + ]; + $event->setIcon($this->urlGenerator->imagePath('gdatavaas', 'favicon.svg')); + + if ($event->getMessage() === self::MESSAGE_FILE_DELETED) { + $event->setParsedMessage('The file has been removed'); + } + } + + $this->setSubjects($event, $subject, $parameters); + + return $event; + } + + private function setSubjects(IEvent $event, string $subject, array $parameters): void { + $placeholders = $replacements = []; + foreach ($parameters as $placeholder => $parameter) { + $placeholders[] = '{' . $placeholder . '}'; + if ($parameter['type'] === 'file') { + $replacements[] = $parameter['path']; + } else { + $replacements[] = $parameter['name']; + } + } + + $event->setParsedSubject(str_replace($placeholders, $replacements, $subject)) + ->setRichSubject($subject, $parameters); + } } diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 5c1db204..bc01e2f9 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -9,92 +9,86 @@ use OCA\GDataVaas\Service\VerdictService; use OCP\Activity\IManager; use OCP\App\IAppManager; -use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\IHomeStorage; -use OCP\Files\Storage; use OCP\Files\Storage\IStorage; use OCP\Util; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; use Psr\Log\LoggerInterface; -class Application extends App implements IBootstrap -{ - public const APP_ID = 'gdatavaas'; +class Application extends App implements IBootstrap { + public const APP_ID = 'gdatavaas'; - /** - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ - public function __construct() - { - parent::__construct(self::APP_ID); + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function __construct() { + parent::__construct(self::APP_ID); - $container = $this->getContainer(); - $eventDispatcher = $container->get(IEventDispatcher::class); - $eventDispatcher->addListener(LoadAdditionalScriptsEvent::class, function () { - Util::addScript(self::APP_ID, 'gdatavaas-files-action'); - }); - } + $container = $this->getContainer(); + $eventDispatcher = $container->get(IEventDispatcher::class); + $eventDispatcher->addListener(LoadAdditionalScriptsEvent::class, function () { + Util::addScript(self::APP_ID, 'gdatavaas-files-action'); + }); + } - /** - * Load the composer autoloader if it exists - * @return void - */ - public function register(IRegistrationContext $context): void - { - $composerAutoloadFile = __DIR__ . '/../../vendor/autoload.php'; - if (file_exists($composerAutoloadFile)) { - require_once $composerAutoloadFile; - } - - // Util::connection is deprecated, but required ATM by FileSystem::addStorageWrapper - Util::connectHook('OC_Filesystem', 'preSetup', $this, 'setupWrapper'); - } + /** + * Load the composer autoloader if it exists + * @return void + */ + public function register(IRegistrationContext $context): void { + $composerAutoloadFile = __DIR__ . '/../../vendor/autoload.php'; + if (file_exists($composerAutoloadFile)) { + require_once $composerAutoloadFile; + } + + // Util::connection is deprecated, but required ATM by FileSystem::addStorageWrapper + Util::connectHook('OC_Filesystem', 'preSetup', $this, 'setupWrapper'); + } - /** - * * Add wrapper for local storages - */ - public function setupWrapper(): void { - Filesystem::addStorageWrapper( - 'oc_gdata_vaas', - function (string $mountPoint, IStorage $storage) { - /* - if ($storage->instanceOfStorage(Jail::class)) { - // No reason to wrap jails again - return $storage; - } - */ + /** + * * Add wrapper for local storages + */ + public function setupWrapper(): void { + Filesystem::addStorageWrapper( + 'oc_gdata_vaas', + function (string $mountPoint, IStorage $storage) { + /* + if ($storage->instanceOfStorage(Jail::class)) { + // No reason to wrap jails again + return $storage; + } + */ - $container = $this->getContainer(); - $verdictService = $container->get(VerdictService::class); - // $l10n = $container->get(IL10N::class); - $logger = $container->get(LoggerInterface::class); - $activityManager = $container->get(IManager::class); - $eventDispatcher = $container->get(IEventDispatcher::class); - $appManager = $container->get(IAppManager::class); - return new AvirWrapper([ - 'storage' => $storage, - 'verdictService' => $verdictService, - //'l10n' => $l10n, - 'logger' => $logger, - 'activityManager' => $activityManager, - 'isHomeStorage' => $storage->instanceOfStorage(IHomeStorage::class), - 'eventDispatcher' => $eventDispatcher, - 'trashEnabled' => $appManager->isEnabledForUser('files_trashbin'), - ]); - }, - 1 - ); - } + $container = $this->getContainer(); + $verdictService = $container->get(VerdictService::class); + // $l10n = $container->get(IL10N::class); + $logger = $container->get(LoggerInterface::class); + $activityManager = $container->get(IManager::class); + $eventDispatcher = $container->get(IEventDispatcher::class); + $appManager = $container->get(IAppManager::class); + return new AvirWrapper([ + 'storage' => $storage, + 'verdictService' => $verdictService, + //'l10n' => $l10n, + 'logger' => $logger, + 'activityManager' => $activityManager, + 'isHomeStorage' => $storage->instanceOfStorage(IHomeStorage::class), + 'eventDispatcher' => $eventDispatcher, + 'trashEnabled' => $appManager->isEnabledForUser('files_trashbin'), + ]); + }, + 1 + ); + } - public function boot(IBootContext $context): void - { - // TODO: Implement boot() method. - } + public function boot(IBootContext $context): void { + } } diff --git a/lib/AvirWrapper.php b/lib/AvirWrapper.php index 6c0c3031..6c645113 100644 --- a/lib/AvirWrapper.php +++ b/lib/AvirWrapper.php @@ -1,4 +1,5 @@ * This file is licensed under the Affero General Public License version 3 or @@ -69,9 +70,6 @@ public function __construct($parameters) { * @return resource | false */ public function fopen($path, $mode) { - $cache = $this->getCache($path); - $metadata = $cache->get($path); - $this->logger->debug(var_export($metadata, true)); $stream = $this->storage->fopen($path, $mode); /* @@ -87,7 +85,7 @@ public function fopen($path, $mode) { return $stream; } - public function writeStream(string $path, $stream, int $size = null): int { + public function writeStream(string $path, $stream, ?int $size = null): int { if ($this->shouldWrap($path)) { $stream = $this->wrapSteam($path, $stream); } @@ -104,22 +102,22 @@ private function shouldWrap(string $path): bool { private function wrapSteam(string $path, $stream) { try { - $logger = $this->logger; + $logger = $this->logger; return CallbackReadDataWrapper::wrap( $stream, null, null, function () use ($path, $logger) { - $localPath = $this->getLocalFile($path); - $filesize = $this->filesize($path); - $logger->debug("Closing " . $localPath . " with size " . $filesize); + $localPath = $this->getLocalFile($path); + $filesize = $this->filesize($path); + $logger->debug("Closing " . $localPath . " with size " . $filesize); $verdict = $this->verdictService->scan($localPath); - $logger->debug("Verdict for " . $localPath . " is " . $verdict->Verdict->value); + $logger->debug("Verdict for " . $localPath . " is " . $verdict->Verdict->value); if ($verdict->Verdict == Verdict::MALICIOUS) { - $logger->debug("Removing malicious file " . $localPath); - + $logger->debug("Removing malicious file " . $localPath); + //prevent from going to trashbin if ($this->trashEnabled) { /** @var ITrashManager $trashManager */ @@ -138,10 +136,10 @@ function () use ($path, $logger) { $this->logger->warning( 'Infected file deleted. ' . $verdict->Detection - . ' Account: ' . $owner . ' Path: ' . $path, + . ' Account: ' . $owner . ' Path: ' . $path, ['app' => 'gdatavaas'] ); - + $activity = $this->activityManager->generateEvent(); $activity->setApp(Application::APP_ID) ->setSubject(Provider::SUBJECT_VIRUS_DETECTED_UPLOAD, [$verdict->Detection ?? "no_detection_name"]) @@ -152,9 +150,9 @@ function () use ($path, $logger) { $this->activityManager->publish($activity); throw new InvalidContentException( - sprintf( + sprintf( 'Virus %s is detected in the file. Upload cannot be completed.', - $verdict->Detection + $verdict->Detection ) ); } diff --git a/lib/BackgroundJobs/ScanJob.php b/lib/BackgroundJobs/ScanJob.php index db6956f8..a68250aa 100644 --- a/lib/BackgroundJobs/ScanJob.php +++ b/lib/BackgroundJobs/ScanJob.php @@ -10,79 +10,76 @@ use OCP\IConfig; use Psr\Log\LoggerInterface; -class ScanJob extends TimedJob -{ - private const APP_ID = "gdatavaas"; +class ScanJob extends TimedJob { + private const APP_ID = "gdatavaas"; - private TagService $tagService; - private VerdictService $scanService; - private IConfig $appConfig; - private LoggerInterface $logger; + private TagService $tagService; + private VerdictService $scanService; + private IConfig $appConfig; + private LoggerInterface $logger; - public function __construct(LoggerInterface $logger, ITimeFactory $time, TagService $tagService, VerdictService $scanService, IConfig $appConfig) - { - parent::__construct($time); + public function __construct(LoggerInterface $logger, ITimeFactory $time, TagService $tagService, VerdictService $scanService, IConfig $appConfig) { + parent::__construct($time); - $this->logger = $logger; - $this->tagService = $tagService; - $this->scanService = $scanService; - $this->appConfig = $appConfig; + $this->logger = $logger; + $this->tagService = $tagService; + $this->scanService = $scanService; + $this->appConfig = $appConfig; - $this->setInterval(60); - $this->setAllowParallelRuns(false); - $this->setTimeSensitivity(self::TIME_SENSITIVE); - } + $this->setInterval(60); + $this->setAllowParallelRuns(false); + $this->setTimeSensitivity(self::TIME_SENSITIVE); + } - /** - * @param $argument - * @return void - * @throws \OCP\DB\Exception if the database platform is not supported - */ - protected function run($argument): void - { - $autoScan = $this->appConfig->getAppValue(self::APP_ID, 'autoScanFiles'); - if (!$autoScan) { - return; - } - $unscannedTagIsDisabled = $this->appConfig->getAppValue(self::APP_ID, 'disableUnscannedTag'); - $autoScanOnlyNewFiles = $this->appConfig->getAppValue(self::APP_ID, 'scanOnlyNewFiles'); - $quantity = $this->appConfig->getAppValue(self::APP_ID, 'scanQueueLength'); - try { - $quantity = intval($quantity); - } catch (Exception) { - $quantity = 5; - } + /** + * @param $argument + * @return void + * @throws \OCP\DB\Exception if the database platform is not supported + */ + protected function run($argument): void { + $autoScan = $this->appConfig->getAppValue(self::APP_ID, 'autoScanFiles'); + if (!$autoScan) { + return; + } + $unscannedTagIsDisabled = $this->appConfig->getAppValue(self::APP_ID, 'disableUnscannedTag'); + $autoScanOnlyNewFiles = $this->appConfig->getAppValue(self::APP_ID, 'scanOnlyNewFiles'); + $quantity = $this->appConfig->getAppValue(self::APP_ID, 'scanQueueLength'); + try { + $quantity = intval($quantity); + } catch (Exception) { + $quantity = 5; + } - $maliciousTag = $this->tagService->getTag(TagService::MALICIOUS); - $pupTag = $this->tagService->getTag(TagService::PUP); - $cleanTag = $this->tagService->getTag(TagService::CLEAN); - $unscannedTag = $this->tagService->getTag(TagService::UNSCANNED); + $maliciousTag = $this->tagService->getTag(TagService::MALICIOUS); + $pupTag = $this->tagService->getTag(TagService::PUP); + $cleanTag = $this->tagService->getTag(TagService::CLEAN); + $unscannedTag = $this->tagService->getTag(TagService::UNSCANNED); - if ($unscannedTagIsDisabled) { - if ($autoScanOnlyNewFiles) { - $excludedTagIds = [$unscannedTag->getId(), $maliciousTag->getId(), $cleanTag->getId(), $pupTag->getId()]; - } else { - $excludedTagIds = [$unscannedTag->getId()]; - } - $fileIds = $this->tagService->getFileIdsWithoutTags($excludedTagIds, $quantity); - } else { - if ($autoScanOnlyNewFiles) { - $fileIds = $this->tagService->getFileIdsWithTag(TagService::UNSCANNED, $quantity, 0); - } else { - $fileIds = $this->tagService->getRandomTaggedFileIds([$maliciousTag->getId(), $cleanTag->getId(), $unscannedTag->getId(), $pupTag->getId()], $quantity, $unscannedTag); - } - } + if ($unscannedTagIsDisabled) { + if ($autoScanOnlyNewFiles) { + $excludedTagIds = [$unscannedTag->getId(), $maliciousTag->getId(), $cleanTag->getId(), $pupTag->getId()]; + } else { + $excludedTagIds = [$unscannedTag->getId()]; + } + $fileIds = $this->tagService->getFileIdsWithoutTags($excludedTagIds, $quantity); + } else { + if ($autoScanOnlyNewFiles) { + $fileIds = $this->tagService->getFileIdsWithTag(TagService::UNSCANNED, $quantity, 0); + } else { + $fileIds = $this->tagService->getRandomTaggedFileIds([$maliciousTag->getId(), $cleanTag->getId(), $unscannedTag->getId(), $pupTag->getId()], $quantity, $unscannedTag); + } + } - $this->logger->debug("Scanning files"); + $this->logger->debug("Scanning files"); - foreach ($fileIds as $fileId) { - try { - $this->scanService->scanFileById($fileId); - } catch (Exception $e) { - $this->logger->error("Failed to scan file with id " . $fileId . ": " . $e->getMessage()); - } - } + foreach ($fileIds as $fileId) { + try { + $this->scanService->scanFileById($fileId); + } catch (Exception $e) { + $this->logger->error("Failed to scan file with id " . $fileId . ": " . $e->getMessage()); + } + } - $this->logger->debug("Scanned " . count($fileIds) . " files"); - } + $this->logger->debug("Scanned " . count($fileIds) . " files"); + } } diff --git a/lib/BackgroundJobs/TagUnscannedJob.php b/lib/BackgroundJobs/TagUnscannedJob.php index 2547f0b7..5b83e462 100644 --- a/lib/BackgroundJobs/TagUnscannedJob.php +++ b/lib/BackgroundJobs/TagUnscannedJob.php @@ -3,64 +3,61 @@ namespace OCA\GDataVaas\BackgroundJobs; use OCA\GDataVaas\Service\TagService; -use OCP\BackgroundJob\TimedJob; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; use OCP\DB\Exception; use OCP\IConfig; use Psr\Log\LoggerInterface; -class TagUnscannedJob extends TimedJob -{ - private const APP_ID = "gdatavaas"; - - private TagService $tagService; - private IConfig $appConfig; - private LoggerInterface $logger; - - public function __construct(ITimeFactory $time, IConfig $appConfig, TagService $tagService, LoggerInterface $logger) - { - parent::__construct($time); - - $this->appConfig = $appConfig; - $this->tagService = $tagService; - $this->logger = $logger; - - $this->setInterval(60); - $this->setAllowParallelRuns(false); - $this->setTimeSensitivity(self::TIME_SENSITIVE); - } - - /** - * @param $argument - * @return void - * @throws Exception if the database platform is not supported - */ - protected function run($argument): void - { - $unscannedTagIsDisabled = $this->appConfig->getAppValue(self::APP_ID, 'disableUnscannedTag'); - if ($unscannedTagIsDisabled) { - $this->tagService->removeTag(TagService::UNSCANNED); - return; - } - - $this->logger->debug("Tagging unscanned files"); - - $unscannedTag = $this->tagService->getTag(TagService::UNSCANNED); - $maliciousTag = $this->tagService->getTag(TagService::MALICIOUS); - $pupTag = $this->tagService->getTag(TagService::PUP); - $cleanTag = $this->tagService->getTag(TagService::CLEAN); - - $excludedTagIds = [$unscannedTag->getId(), $maliciousTag->getId(), $cleanTag->getId(), $pupTag->getId()]; - - $fileIds = $this->tagService->getFileIdsWithoutTags($excludedTagIds, 10000); - - foreach ($fileIds as $fileId) { - if ($this->tagService->hasCleanMaliciousOrPupTag($fileId)) { - continue; - } - $this->tagService->setTag($fileId, TagService::UNSCANNED); - } - - $this->logger->debug("Tagged " . count($fileIds) . " unscanned files"); - } +class TagUnscannedJob extends TimedJob { + private const APP_ID = "gdatavaas"; + + private TagService $tagService; + private IConfig $appConfig; + private LoggerInterface $logger; + + public function __construct(ITimeFactory $time, IConfig $appConfig, TagService $tagService, LoggerInterface $logger) { + parent::__construct($time); + + $this->appConfig = $appConfig; + $this->tagService = $tagService; + $this->logger = $logger; + + $this->setInterval(60); + $this->setAllowParallelRuns(false); + $this->setTimeSensitivity(self::TIME_SENSITIVE); + } + + /** + * @param $argument + * @return void + * @throws Exception if the database platform is not supported + */ + protected function run($argument): void { + $unscannedTagIsDisabled = $this->appConfig->getAppValue(self::APP_ID, 'disableUnscannedTag'); + if ($unscannedTagIsDisabled) { + $this->tagService->removeTag(TagService::UNSCANNED); + return; + } + + $this->logger->debug("Tagging unscanned files"); + + $unscannedTag = $this->tagService->getTag(TagService::UNSCANNED); + $maliciousTag = $this->tagService->getTag(TagService::MALICIOUS); + $pupTag = $this->tagService->getTag(TagService::PUP); + $cleanTag = $this->tagService->getTag(TagService::CLEAN); + + $excludedTagIds = [$unscannedTag->getId(), $maliciousTag->getId(), $cleanTag->getId(), $pupTag->getId()]; + + $fileIds = $this->tagService->getFileIdsWithoutTags($excludedTagIds, 10000); + + foreach ($fileIds as $fileId) { + if ($this->tagService->hasCleanMaliciousOrPupTag($fileId)) { + continue; + } + $this->tagService->setTag($fileId, TagService::UNSCANNED); + } + + $this->logger->debug("Tagged " . count($fileIds) . " unscanned files"); + } } diff --git a/lib/Controller/ScanController.php b/lib/Controller/ScanController.php index 5677820c..a3b4d285 100644 --- a/lib/Controller/ScanController.php +++ b/lib/Controller/ScanController.php @@ -3,58 +3,55 @@ namespace OCA\GDataVaas\Controller; use OCA\GDataVaas\Service\VerdictService; +use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; use OCP\Files\EntityTooLargeException; use OCP\Files\InvalidPathException; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IRequest; -use OCP\AppFramework\Controller; use VaasSdk\Exceptions\FileDoesNotExistException; use VaasSdk\Exceptions\InvalidSha256Exception; use VaasSdk\Exceptions\TimeoutException; use VaasSdk\Exceptions\UploadFailedException; use VaasSdk\Exceptions\VaasAuthenticationException; -class ScanController extends Controller -{ - private VerdictService $verdictService; +class ScanController extends Controller { + private VerdictService $verdictService; - public function __construct($appName, IRequest $request, VerdictService $verdictService) - { - parent::__construct($appName, $request); + public function __construct($appName, IRequest $request, VerdictService $verdictService) { + parent::__construct($appName, $request); - $this->verdictService = $verdictService; - } + $this->verdictService = $verdictService; + } - /** - * Scans a file for malicious content with G DATA Verdict-as-a-Service and handles the result. - * @param int $fileId - * @return JSONResponse - */ - public function scan(int $fileId): JSONResponse - { - try { - $verdict = $this->verdictService->scanFileById($fileId); - return new JSONResponse(['verdict' => $verdict->Verdict->value], 200); - } catch (EntityTooLargeException) { - return new JSONResponse(['error' => 'File is too large'], 413); - } catch (FileDoesNotExistException) { - return new JSONResponse(['error' => 'File does not exist'], 404); - } catch (InvalidPathException) { - return new JSONResponse(['error' => 'Invalid path'], 400); - } catch (InvalidSha256Exception) { - return new JSONResponse(['error' => 'Invalid SHA256'], 400); - } catch (NotFoundException) { - return new JSONResponse(['error' => 'Not found'], 404); - } catch (NotPermittedException) { - return new JSONResponse(['error' => 'Current settings do not permit scanning this.'], 403); - } catch (TimeoutException) { - return new JSONResponse(['error' => 'Scanning timed out'], 408); - } catch (UploadFailedException) { - return new JSONResponse(['error' => 'File upload failed'], 500); - } catch (VaasAuthenticationException) { - return new JSONResponse(['error' => 'Authentication failed. Please check your credentials.'], 401); - } - } + /** + * Scans a file for malicious content with G DATA Verdict-as-a-Service and handles the result. + * @param int $fileId + * @return JSONResponse + */ + public function scan(int $fileId): JSONResponse { + try { + $verdict = $this->verdictService->scanFileById($fileId); + return new JSONResponse(['verdict' => $verdict->Verdict->value], 200); + } catch (EntityTooLargeException) { + return new JSONResponse(['error' => 'File is too large'], 413); + } catch (FileDoesNotExistException) { + return new JSONResponse(['error' => 'File does not exist'], 404); + } catch (InvalidPathException) { + return new JSONResponse(['error' => 'Invalid path'], 400); + } catch (InvalidSha256Exception) { + return new JSONResponse(['error' => 'Invalid SHA256'], 400); + } catch (NotFoundException) { + return new JSONResponse(['error' => 'Not found'], 404); + } catch (NotPermittedException) { + return new JSONResponse(['error' => 'Current settings do not permit scanning this.'], 403); + } catch (TimeoutException) { + return new JSONResponse(['error' => 'Scanning timed out'], 408); + } catch (UploadFailedException) { + return new JSONResponse(['error' => 'File upload failed'], 500); + } catch (VaasAuthenticationException) { + return new JSONResponse(['error' => 'Authentication failed. Please check your credentials.'], 401); + } + } } diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index c82c9022..e0ecb861 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -8,91 +8,77 @@ use OCP\IConfig; use OCP\IRequest; -class SettingsController extends Controller -{ - private IConfig $config; - private TagService $tagService; - - public function __construct($appName, IRequest $request, IConfig $config, TagService $tagService) - { - parent::__construct($appName, $request); - $this->config = $config; - $this->tagService = $tagService; - } - - public function setconfig($username, $password, $clientId, $clientSecret, $authMethod, $quarantineFolder, $allowlist, $blocklist, $scanQueueLength): JSONResponse - { - $this->config->setAppValue($this->appName, 'username', $username); - $this->config->setAppValue($this->appName, 'password', $password); - $this->config->setAppValue($this->appName, 'clientId', $clientId); - $this->config->setAppValue($this->appName, 'clientSecret', $clientSecret); - $this->config->setAppValue($this->appName, 'authMethod', $authMethod); - $this->config->setAppValue($this->appName, 'quarantineFolder', $quarantineFolder); - $this->config->setAppValue($this->appName, 'allowlist', $allowlist); - $this->config->setAppValue($this->appName, 'blocklist', $blocklist); - $this->config->setAppValue($this->appName, 'scanQueueLength', $scanQueueLength); - return new JSONResponse(['status' => 'success']); - } - - public function setadvancedconfig($tokenEndpoint, $vaasUrl): JSONResponse - { - $this->config->setAppValue($this->appName, 'tokenEndpoint', $tokenEndpoint); - $this->config->setAppValue($this->appName, 'vaasUrl', $vaasUrl); - return new JSONResponse(['status' => 'success']); - } - - public function setAutoScan(bool $autoScanFiles): JSONResponse - { - $this->config->setAppValue($this->appName, 'autoScanFiles', $autoScanFiles); - return new JSONResponse(['status' => 'success']); - } - - public function getAutoScan(): JSONResponse - { - return new JSONResponse(['status' => $this->config->getAppValue($this->appName, 'autoScanFiles')]); - } - - public function setScanOnlyNewFiles(bool $scanOnlyNewFiles): JSONResponse - { - $this->config->setAppValue($this->appName, 'scanOnlyNewFiles', $scanOnlyNewFiles); - return new JSONResponse(['status' => 'success']); - } - - public function getScanOnlyNewFiles(): JSONResponse - { - return new JSONResponse(['status' => $this->config->getAppValue($this->appName, 'scanOnlyNewFiles')]); - } - - public function setPrefixMalicious(bool $prefixMalicious): JSONResponse - { - $this->config->setAppValue($this->appName, 'prefixMalicious', $prefixMalicious); - return new JSONResponse(['status' => 'success']); - } - - public function getPrefixMalicious(): JSONResponse - { - return new JSONResponse(['status' => $this->config->getAppValue($this->appName, 'prefixMalicious')]); - } - - public function getAuthMethod(): JSONResponse - { - return new JSONResponse(['status' => $this->config->getAppValue($this->appName, 'authMethod')]); - } - - public function setDisableUnscannedTag(bool $disableUnscannedTag): JSONResponse - { - $this->config->setAppValue($this->appName, 'disableUnscannedTag', $disableUnscannedTag); - return new JSONResponse(['status' => 'success']); - } - - public function getDisableUnscannedTag(): JSONResponse - { - return new JSONResponse(['status' => $this->config->getAppValue($this->appName, 'disableUnscannedTag')]); - } - - public function resetAllTags(): JSONResponse - { - $this->tagService->resetAllTags(); - return new JSONResponse(['status' => 'success']); - } +class SettingsController extends Controller { + private IConfig $config; + private TagService $tagService; + + public function __construct($appName, IRequest $request, IConfig $config, TagService $tagService) { + parent::__construct($appName, $request); + $this->config = $config; + $this->tagService = $tagService; + } + + public function setconfig($username, $password, $clientId, $clientSecret, $authMethod, $quarantineFolder, $allowlist, $blocklist, $scanQueueLength): JSONResponse { + $this->config->setAppValue($this->appName, 'username', $username); + $this->config->setAppValue($this->appName, 'password', $password); + $this->config->setAppValue($this->appName, 'clientId', $clientId); + $this->config->setAppValue($this->appName, 'clientSecret', $clientSecret); + $this->config->setAppValue($this->appName, 'authMethod', $authMethod); + $this->config->setAppValue($this->appName, 'quarantineFolder', $quarantineFolder); + $this->config->setAppValue($this->appName, 'allowlist', $allowlist); + $this->config->setAppValue($this->appName, 'blocklist', $blocklist); + $this->config->setAppValue($this->appName, 'scanQueueLength', $scanQueueLength); + return new JSONResponse(['status' => 'success']); + } + + public function setadvancedconfig($tokenEndpoint, $vaasUrl): JSONResponse { + $this->config->setAppValue($this->appName, 'tokenEndpoint', $tokenEndpoint); + $this->config->setAppValue($this->appName, 'vaasUrl', $vaasUrl); + return new JSONResponse(['status' => 'success']); + } + + public function setAutoScan(bool $autoScanFiles): JSONResponse { + $this->config->setAppValue($this->appName, 'autoScanFiles', $autoScanFiles); + return new JSONResponse(['status' => 'success']); + } + + public function getAutoScan(): JSONResponse { + return new JSONResponse(['status' => $this->config->getAppValue($this->appName, 'autoScanFiles')]); + } + + public function setScanOnlyNewFiles(bool $scanOnlyNewFiles): JSONResponse { + $this->config->setAppValue($this->appName, 'scanOnlyNewFiles', $scanOnlyNewFiles); + return new JSONResponse(['status' => 'success']); + } + + public function getScanOnlyNewFiles(): JSONResponse { + return new JSONResponse(['status' => $this->config->getAppValue($this->appName, 'scanOnlyNewFiles')]); + } + + public function setPrefixMalicious(bool $prefixMalicious): JSONResponse { + $this->config->setAppValue($this->appName, 'prefixMalicious', $prefixMalicious); + return new JSONResponse(['status' => 'success']); + } + + public function getPrefixMalicious(): JSONResponse { + return new JSONResponse(['status' => $this->config->getAppValue($this->appName, 'prefixMalicious')]); + } + + public function getAuthMethod(): JSONResponse { + return new JSONResponse(['status' => $this->config->getAppValue($this->appName, 'authMethod')]); + } + + public function setDisableUnscannedTag(bool $disableUnscannedTag): JSONResponse { + $this->config->setAppValue($this->appName, 'disableUnscannedTag', $disableUnscannedTag); + return new JSONResponse(['status' => 'success']); + } + + public function getDisableUnscannedTag(): JSONResponse { + return new JSONResponse(['status' => $this->config->getAppValue($this->appName, 'disableUnscannedTag')]); + } + + public function resetAllTags(): JSONResponse { + $this->tagService->resetAllTags(); + return new JSONResponse(['status' => 'success']); + } } diff --git a/lib/Db/DbFileMapper.php b/lib/Db/DbFileMapper.php index dec809a7..45d3d81f 100644 --- a/lib/Db/DbFileMapper.php +++ b/lib/Db/DbFileMapper.php @@ -8,93 +8,88 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; -class DbFileMapper extends QBMapper -{ - public function __construct(IDBConnection $db) - { - parent::__construct($db, 'filecache'); - } +class DbFileMapper extends QBMapper { + public function __construct(IDBConnection $db) { + parent::__construct($db, 'filecache'); + } - /** - * Get file ids that do not have any of the given tags - * @param array $excludedTagIds - * @param int $limit - * @return array of file ids - * @throws Exception if the database platform is not supported - */ - public function getFileIdsWithoutTags(array $excludedTagIds, int $limit): array - { - $qb = $this->db->getQueryBuilder(); - $qb->automaticTablePrefix(true); + /** + * Get file ids that do not have any of the given tags + * @param array $excludedTagIds + * @param int $limit + * @return array of file ids + * @throws Exception if the database platform is not supported + */ + public function getFileIdsWithoutTags(array $excludedTagIds, int $limit): array { + $qb = $this->db->getQueryBuilder(); + $qb->automaticTablePrefix(true); - $qb->select('f.fileid') - ->from($this->getTableName(), 'f') - ->leftJoin('f', 'systemtag_object_mapping', 'o', $qb->expr()->eq('f.fileid', $qb->createFunction($this->getPlatformSpecificCast()))) - ->leftJoin('f', 'mimetypes', 'm', $qb->expr()->eq('f.mimetype', 'm.id')) - ->where($qb->expr()->notIn('o.systemtagid', $qb->createNamedParameter($excludedTagIds, IQueryBuilder::PARAM_INT_ARRAY))) - ->orWhere($qb->expr()->isNull('o.systemtagid')) - ->andWhere($qb->expr()->notLike('m.mimetype', $qb->createNamedParameter('%unix-directory%'))) - ->andWhere($qb->expr()->lte('f.size', $qb->createNamedParameter(VerdictService::MAX_FILE_SIZE))) - ->andWhere($qb->expr()->like('f.path', $qb->createNamedParameter('files/%'))) - ->orderBy('f.fileid', 'DESC') - ->setMaxResults($limit); + $qb->select('f.fileid') + ->from($this->getTableName(), 'f') + ->leftJoin('f', 'systemtag_object_mapping', 'o', $qb->expr()->eq('f.fileid', $qb->createFunction($this->getPlatformSpecificCast()))) + ->leftJoin('f', 'mimetypes', 'm', $qb->expr()->eq('f.mimetype', 'm.id')) + ->where($qb->expr()->notIn('o.systemtagid', $qb->createNamedParameter($excludedTagIds, IQueryBuilder::PARAM_INT_ARRAY))) + ->orWhere($qb->expr()->isNull('o.systemtagid')) + ->andWhere($qb->expr()->notLike('m.mimetype', $qb->createNamedParameter('%unix-directory%'))) + ->andWhere($qb->expr()->lte('f.size', $qb->createNamedParameter(VerdictService::MAX_FILE_SIZE))) + ->andWhere($qb->expr()->like('f.path', $qb->createNamedParameter('files/%'))) + ->orderBy('f.fileid', 'DESC') + ->setMaxResults($limit); - $fileIds = []; - $result = $qb->executeQuery(); - while ($row = $result->fetch()) { - $fileIds[] = $row['fileid']; - } - return $fileIds; - } + $fileIds = []; + $result = $qb->executeQuery(); + while ($row = $result->fetch()) { + $fileIds[] = $row['fileid']; + } + return $fileIds; + } - /** - * Get file ids that have at least one of the given tags - * @param array $includedTagIds - * @param int $limit - * @return array of file ids - * @throws Exception if the database platform is not supported - */ - public function getFileIdsWithTags(array $includedTagIds, int $limit): array - { - $qb = $this->db->getQueryBuilder(); - $qb->automaticTablePrefix(true); + /** + * Get file ids that have at least one of the given tags + * @param array $includedTagIds + * @param int $limit + * @return array of file ids + * @throws Exception if the database platform is not supported + */ + public function getFileIdsWithTags(array $includedTagIds, int $limit): array { + $qb = $this->db->getQueryBuilder(); + $qb->automaticTablePrefix(true); - $qb->select('f.fileid') - ->from($this->getTableName(), 'f') - ->leftJoin('f', 'systemtag_object_mapping', 'o', $qb->expr()->eq('f.fileid', $qb->createFunction($this->getPlatformSpecificCast()))) - ->leftJoin('f', 'mimetypes', 'm', $qb->expr()->eq('f.mimetype', 'm.id')) - ->where($qb->expr()->in('o.systemtagid', $qb->createNamedParameter($includedTagIds, IQueryBuilder::PARAM_INT_ARRAY))) - ->andWhere($qb->expr()->notLike('m.mimetype', $qb->createNamedParameter('%unix-directory%'))) - ->andWhere($qb->expr()->lte('f.size', $qb->createNamedParameter(VerdictService::MAX_FILE_SIZE))) - ->andWhere($qb->expr()->like('f.path', $qb->createNamedParameter('files/%'))) - ->orderBy('f.fileid', 'DESC') - ->setMaxResults($limit); + $qb->select('f.fileid') + ->from($this->getTableName(), 'f') + ->leftJoin('f', 'systemtag_object_mapping', 'o', $qb->expr()->eq('f.fileid', $qb->createFunction($this->getPlatformSpecificCast()))) + ->leftJoin('f', 'mimetypes', 'm', $qb->expr()->eq('f.mimetype', 'm.id')) + ->where($qb->expr()->in('o.systemtagid', $qb->createNamedParameter($includedTagIds, IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere($qb->expr()->notLike('m.mimetype', $qb->createNamedParameter('%unix-directory%'))) + ->andWhere($qb->expr()->lte('f.size', $qb->createNamedParameter(VerdictService::MAX_FILE_SIZE))) + ->andWhere($qb->expr()->like('f.path', $qb->createNamedParameter('files/%'))) + ->orderBy('f.fileid', 'DESC') + ->setMaxResults($limit); - $fileIds = []; - $result = $qb->executeQuery(); - while ($row = $result->fetch()) { - $fileIds[] = $row['fileid']; - } - return $fileIds; - } + $fileIds = []; + $result = $qb->executeQuery(); + while ($row = $result->fetch()) { + $fileIds[] = $row['fileid']; + } + return $fileIds; + } - /** - * Create a platform-specific cast function - * @return string the database platform-specific cast function - * @throws Exception if the database platform is not supported - */ - private function getPlatformSpecificCast(): string - { - $platform = $this->db->getDatabasePlatform()->getName(); - if ($platform === 'mysql') { - $cast = 'CAST(' . 'o.objectid' . ' AS UNSIGNED)'; - } elseif ($platform === 'sqlite') { - $cast = 'CAST(' . 'o.objectid' . ' AS INTEGER)'; - } elseif ($platform === 'postgresql') { - $cast = 'CAST(' . 'o.objectid' . ' AS BIGINT)'; - } else { - throw new Exception('Unsupported database platform: ' . $platform); - } - return $cast; - } + /** + * Create a platform-specific cast function + * @return string the database platform-specific cast function + * @throws Exception if the database platform is not supported + */ + private function getPlatformSpecificCast(): string { + $platform = $this->db->getDatabasePlatform()->getName(); + if ($platform === 'mysql') { + $cast = 'CAST(' . 'o.objectid' . ' AS UNSIGNED)'; + } elseif ($platform === 'sqlite') { + $cast = 'CAST(' . 'o.objectid' . ' AS INTEGER)'; + } elseif ($platform === 'postgresql') { + $cast = 'CAST(' . 'o.objectid' . ' AS BIGINT)'; + } else { + throw new Exception('Unsupported database platform: ' . $platform); + } + return $cast; + } } diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index 1989f30d..71a778da 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -4,107 +4,101 @@ use OCP\Files\Config\IUserMountCache; use OCP\Files\InvalidPathException; -use OCP\Files\NotPermittedException; -use OCP\IConfig; +use OCP\Files\IRootFolder; use OCP\Files\Node; use OCP\Files\NotFoundException; -use OCP\Files\IRootFolder; +use OCP\Files\NotPermittedException; +use OCP\IConfig; use OCP\Lock\LockedException; use Psr\Log\LoggerInterface; -class FileService -{ - private const APP_ID = "gdatavaas"; +class FileService { + private const APP_ID = "gdatavaas"; - private IUserMountCache $userMountCache; - private IRootFolder $rootFolder; - private IConfig $appConfig; - private LoggerInterface $logger; + private IUserMountCache $userMountCache; + private IRootFolder $rootFolder; + private IConfig $appConfig; + private LoggerInterface $logger; - public function __construct(LoggerInterface $logger, IUserMountCache $userMountCache, IRootFolder $rootFolder, IConfig $appConfig) - { - $this->userMountCache = $userMountCache; - $this->rootFolder = $rootFolder; - $this->appConfig = $appConfig; - $this->logger = $logger; - } + public function __construct(LoggerInterface $logger, IUserMountCache $userMountCache, IRootFolder $rootFolder, IConfig $appConfig) { + $this->userMountCache = $userMountCache; + $this->rootFolder = $rootFolder; + $this->appConfig = $appConfig; + $this->logger = $logger; + } - /** - * Checks if the 'Set prefix for malicious files' setting is activated and sets the prefix if it is. - * @param int $fileId - * @return void - * @throws NotFoundException - * @throws InvalidPathException - * @throws NotPermittedException - * @throws LockedException - */ - public function setMaliciousPrefixIfActivated(int $fileId): void - { - if ($this->appConfig->getAppValue(self::APP_ID, 'prefixMalicious')) { - $file = $this->getNodeFromFileId($fileId); - if (!str_starts_with($file->getName(), '[MALICIOUS] ')) { - $newFileName = "[MALICIOUS] " . $file->getName(); - $file->move($file->getParent()->getPath() . '/' . $newFileName); - $this->logger->info("Malicious prefix added to file " . $file->getName() . " (" . $fileId . ")"); - } - } - } + /** + * Checks if the 'Set prefix for malicious files' setting is activated and sets the prefix if it is. + * @param int $fileId + * @return void + * @throws NotFoundException + * @throws InvalidPathException + * @throws NotPermittedException + * @throws LockedException + */ + public function setMaliciousPrefixIfActivated(int $fileId): void { + if ($this->appConfig->getAppValue(self::APP_ID, 'prefixMalicious')) { + $file = $this->getNodeFromFileId($fileId); + if (!str_starts_with($file->getName(), '[MALICIOUS] ')) { + $newFileName = "[MALICIOUS] " . $file->getName(); + $file->move($file->getParent()->getPath() . '/' . $newFileName); + $this->logger->info("Malicious prefix added to file " . $file->getName() . " (" . $fileId . ")"); + } + } + } - /** - * @param int $fileId - * @return Node - * @throws NotFoundException - * @throws NotPermittedException - */ - public function getNodeFromFileId(int $fileId): Node - { - $mounts = $this->userMountCache->getMountsForFileId($fileId); - foreach ($mounts as $mount) { - if ($node = $this->findNodeInMount($mount, $fileId)) { - return $node; - } - } - throw new NotFoundException(); - } + /** + * @param int $fileId + * @return Node + * @throws NotFoundException + * @throws NotPermittedException + */ + public function getNodeFromFileId(int $fileId): Node { + $mounts = $this->userMountCache->getMountsForFileId($fileId); + foreach ($mounts as $mount) { + if ($node = $this->findNodeInMount($mount, $fileId)) { + return $node; + } + } + throw new NotFoundException(); + } - /** - * @param $mount - * @param int $fileId - * @return Node|null - * @throws NotPermittedException - */ - private function findNodeInMount(\OCP\Files\Config\ICachedMountFileInfo $mount, int $fileId): ?Node - { - $mountUserFolder = $this->rootFolder->getUserFolder($mount->getUser()->getUID()); - $nodes = $mountUserFolder->getById($fileId); - return $nodes[0] ?? null; - } + /** + * @param $mount + * @param int $fileId + * @return Node|null + * @throws NotPermittedException + */ + private function findNodeInMount(\OCP\Files\Config\ICachedMountFileInfo $mount, int $fileId): ?Node { + $mountUserFolder = $this->rootFolder->getUserFolder($mount->getUser()->getUID()); + $nodes = $mountUserFolder->getById($fileId); + return $nodes[0] ?? null; + } - /** - * Moves a file to the quarantine folder if it is defined in the app settings. - * @param int $fileId - * @return void - * @throws InvalidPathException - * @throws LockedException - * @throws NotFoundException - * @throws NotPermittedException - */ - public function moveFileToQuarantineFolderIfDefined(int $fileId): void - { - $quarantineFolderPath = $this->appConfig->getAppValue(self::APP_ID, 'quarantineFolder'); - if (empty($quarantineFolderPath)) { - throw new InvalidPathException('Quarantine folder path is not defined'); - } - $mounts = $this->userMountCache->getMountsForFileId($fileId); - $mountUserFolder = $this->rootFolder->getUserFolder($mounts[0]->getUser()->getUID()); - try { - $quarantine = $mountUserFolder->get($quarantineFolderPath); - } catch (NotFoundException) { - $quarantine = $mountUserFolder->newFolder($quarantineFolderPath); - $this->logger->info("Quarantine folder created at " . $quarantine->getPath()); - } - $file = $this->getNodeFromFileId($fileId); - $file->move($quarantine->getPath() . '/' . $file->getName()); - $this->logger->info("File " . $file->getName() . " (" . $fileId . ") moved to quarantine folder."); - } + /** + * Moves a file to the quarantine folder if it is defined in the app settings. + * @param int $fileId + * @return void + * @throws InvalidPathException + * @throws LockedException + * @throws NotFoundException + * @throws NotPermittedException + */ + public function moveFileToQuarantineFolderIfDefined(int $fileId): void { + $quarantineFolderPath = $this->appConfig->getAppValue(self::APP_ID, 'quarantineFolder'); + if (empty($quarantineFolderPath)) { + throw new InvalidPathException('Quarantine folder path is not defined'); + } + $mounts = $this->userMountCache->getMountsForFileId($fileId); + $mountUserFolder = $this->rootFolder->getUserFolder($mounts[0]->getUser()->getUID()); + try { + $quarantine = $mountUserFolder->get($quarantineFolderPath); + } catch (NotFoundException) { + $quarantine = $mountUserFolder->newFolder($quarantineFolderPath); + $this->logger->info("Quarantine folder created at " . $quarantine->getPath()); + } + $file = $this->getNodeFromFileId($fileId); + $file->move($quarantine->getPath() . '/' . $file->getName()); + $this->logger->info("File " . $file->getName() . " (" . $fileId . ") moved to quarantine folder."); + } } diff --git a/lib/Service/TagService.php b/lib/Service/TagService.php index 041de672..b3878eb7 100644 --- a/lib/Service/TagService.php +++ b/lib/Service/TagService.php @@ -5,187 +5,175 @@ use OCA\GDataVaas\Db\DbFileMapper; use OCP\DB\Exception; use OCP\SystemTag\ISystemTag; +use OCP\SystemTag\ISystemTagManager; use OCP\SystemTag\ISystemTagObjectMapper; use OCP\SystemTag\TagAlreadyExistsException; use OCP\SystemTag\TagNotFoundException; -use OCP\SystemTag\ISystemTagManager; use Psr\Log\LoggerInterface; -class TagService -{ - public const CLEAN = 'Clean'; - public const MALICIOUS = 'Malicious'; - public const PUP = 'Pup'; - public const UNSCANNED = 'Unscanned'; - - private ISystemTagManager $tagService; - private ISystemTagObjectMapper $tagMapper; - private DbFileMapper $dbFileMapper; - private LoggerInterface $logger; - - - public function __construct(LoggerInterface $logger, ISystemTagManager $systemTagManager, ISystemTagObjectMapper $objectMapper, DbFileMapper $dbFileMapper) - { - $this->tagService = $systemTagManager; - $this->tagMapper = $objectMapper; - $this->dbFileMapper = $dbFileMapper; - $this->logger = $logger; - } - - /** - * @param string $name - * @param bool $create - * @return ISystemTag - * @throws TagAlreadyExistsException if tag already exists - * @throws TagNotFoundException if tag does not exist and $create=false - */ - public function getTag(string $name, bool $create = true): ISystemTag - { - try { - $tag = $this->tagService->getTag($name, true, false); - } catch (TagNotFoundException) { - if (!$create) { - throw new TagNotFoundException(); - } - $tag = $this->tagService->createTag($name, true, false); - $this->logger->debug("Tag created: " . $name); - } - return $tag; - } - - /** - * @param int $fileId - * @param string $tagName - * @return void - */ - public function setTag(int $fileId, string $tagName): void - { - $tag = $this->getTag($tagName); - $this->tagMapper->assignTags(strval($fileId), 'files', [$tag->getId()]); - $this->logger->debug("Tag set: " . $tagName . " for file " . $fileId); - } - - /** - * @param string $tagName - * @param int $fileId - * @return bool - */ - public function removeTagFromFile(string $tagName, int $fileId): bool - { - try { - $tag = $this->tagService->getTag($tagName, true, false); - $this->tagMapper->unassignTags(strval($fileId), 'files', [$tag->getId()]); - $this->logger->debug("Tag removed: " . $tagName . " for file " . $fileId); - return true; - } catch (TagNotFoundException) { - return false; - } - } - - /** - * Checks if a file has either CLEAN or MALICIOUS tag and creates these. - * @param int $fileId - * @return bool - */ - public function hasCleanMaliciousOrPupTag(int $fileId): bool - { - if ( - $this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::CLEAN)->getId()) || - $this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::MALICIOUS)->getId()) || - $this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::PUP)->getId()) - ) { - return true; - } - return false; - } - - /** - * Checks if a file has UNSCANNED tag and creates it. - * @param int $fileId - * @return bool - */ - public function hasUnscannedTag(int $fileId): bool - { - return $this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::UNSCANNED)->getId()); - } - - /** - * @param string $tagName - * @param int $limit Count of object ids you want to get - * @return array - * @throws Exception if the database platform is not supported - */ - public function getFileIdsWithTag(string $tagName, int $limit): array - { - try { - $tag = $this->getTag($tagName, false); - } catch (TagNotFoundException) { - return []; - } - return $this->dbFileMapper->getFileIdsWithTags([$tag->getId()], $limit); - } - - /** - * @param array $excludedTagIds - * @param int $limit - * @return array - * @throws Exception if the database platform is not supported - */ - public function getFileIdsWithoutTags(array $excludedTagIds, int $limit): array - { - return $this->dbFileMapper->getFileIdsWithoutTags($excludedTagIds, $limit); - } - - /** - * Get file ids that have any of the given tags - * @param array $tagIds The tags to get the file ids for - * @param int $limit The count of file ids you want to get - * @param ISystemTag|null $priorityTagId Tag id to prioritize over the others - * @return array - * @throws TagNotFoundException if a tag does not exist - * @throws Exception If the database platform is not supported - */ - public function getRandomTaggedFileIds(array $tagIds, int $limit, ?ISystemTag $priorityTagId = null): array - { - $objectIdsPriority = []; - if ($priorityTagId !== null) { - $objectIdsPriority = $this->dbFileMapper->getFileIdsWithTags([$priorityTagId->getId()], $limit); - shuffle($objectIdsPriority); - } - if (count($objectIdsPriority) < $limit) { - $objectIds = $this->dbFileMapper->getFileIdsWithTags($tagIds, $limit - count($objectIdsPriority)); - shuffle($objectIds); - return array_merge($objectIdsPriority, $objectIds); - } - return $objectIdsPriority; - } - - /** - * Delete a tag by name if it exists - * @param string $tagName - * @return void - */ - public function removeTag(string $tagName): void - { - try { - $tag = $this->getTag($tagName, false); - } catch (TagNotFoundException) { - return; - } - $this->tagService->deleteTags([$tag->getId()]); - $this->logger->debug("Tag removed: " . $tagName); - } - - /** - * Removes all tags - * @return void - */ - public function resetAllTags(): void - { - $this->removeTag(self::CLEAN); - $this->removeTag(self::MALICIOUS); - $this->removeTag(self::UNSCANNED); - $this->removeTag(self::PUP); - $this->logger->info("All tags removed"); - } +class TagService { + public const CLEAN = 'Clean'; + public const MALICIOUS = 'Malicious'; + public const PUP = 'Pup'; + public const UNSCANNED = 'Unscanned'; + + private ISystemTagManager $tagService; + private ISystemTagObjectMapper $tagMapper; + private DbFileMapper $dbFileMapper; + private LoggerInterface $logger; + + + public function __construct(LoggerInterface $logger, ISystemTagManager $systemTagManager, ISystemTagObjectMapper $objectMapper, DbFileMapper $dbFileMapper) { + $this->tagService = $systemTagManager; + $this->tagMapper = $objectMapper; + $this->dbFileMapper = $dbFileMapper; + $this->logger = $logger; + } + + /** + * @param string $name + * @param bool $create + * @return ISystemTag + * @throws TagAlreadyExistsException if tag already exists + * @throws TagNotFoundException if tag does not exist and $create=false + */ + public function getTag(string $name, bool $create = true): ISystemTag { + try { + $tag = $this->tagService->getTag($name, true, false); + } catch (TagNotFoundException) { + if (!$create) { + throw new TagNotFoundException(); + } + $tag = $this->tagService->createTag($name, true, false); + $this->logger->debug("Tag created: " . $name); + } + return $tag; + } + + /** + * @param int $fileId + * @param string $tagName + * @return void + */ + public function setTag(int $fileId, string $tagName): void { + $tag = $this->getTag($tagName); + $this->tagMapper->assignTags(strval($fileId), 'files', [$tag->getId()]); + $this->logger->debug("Tag set: " . $tagName . " for file " . $fileId); + } + + /** + * @param string $tagName + * @param int $fileId + * @return bool + */ + public function removeTagFromFile(string $tagName, int $fileId): bool { + try { + $tag = $this->tagService->getTag($tagName, true, false); + $this->tagMapper->unassignTags(strval($fileId), 'files', [$tag->getId()]); + $this->logger->debug("Tag removed: " . $tagName . " for file " . $fileId); + return true; + } catch (TagNotFoundException) { + return false; + } + } + + /** + * Checks if a file has either CLEAN or MALICIOUS tag and creates these. + * @param int $fileId + * @return bool + */ + public function hasCleanMaliciousOrPupTag(int $fileId): bool { + if ( + $this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::CLEAN)->getId()) || + $this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::MALICIOUS)->getId()) || + $this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::PUP)->getId()) + ) { + return true; + } + return false; + } + + /** + * Checks if a file has UNSCANNED tag and creates it. + * @param int $fileId + * @return bool + */ + public function hasUnscannedTag(int $fileId): bool { + return $this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::UNSCANNED)->getId()); + } + + /** + * @param string $tagName + * @param int $limit Count of object ids you want to get + * @return array + * @throws Exception if the database platform is not supported + */ + public function getFileIdsWithTag(string $tagName, int $limit): array { + try { + $tag = $this->getTag($tagName, false); + } catch (TagNotFoundException) { + return []; + } + return $this->dbFileMapper->getFileIdsWithTags([$tag->getId()], $limit); + } + + /** + * @param array $excludedTagIds + * @param int $limit + * @return array + * @throws Exception if the database platform is not supported + */ + public function getFileIdsWithoutTags(array $excludedTagIds, int $limit): array { + return $this->dbFileMapper->getFileIdsWithoutTags($excludedTagIds, $limit); + } + + /** + * Get file ids that have any of the given tags + * @param array $tagIds The tags to get the file ids for + * @param int $limit The count of file ids you want to get + * @param ISystemTag|null $priorityTagId Tag id to prioritize over the others + * @return array + * @throws TagNotFoundException if a tag does not exist + * @throws Exception If the database platform is not supported + */ + public function getRandomTaggedFileIds(array $tagIds, int $limit, ?ISystemTag $priorityTagId = null): array { + $objectIdsPriority = []; + if ($priorityTagId !== null) { + $objectIdsPriority = $this->dbFileMapper->getFileIdsWithTags([$priorityTagId->getId()], $limit); + shuffle($objectIdsPriority); + } + if (count($objectIdsPriority) < $limit) { + $objectIds = $this->dbFileMapper->getFileIdsWithTags($tagIds, $limit - count($objectIdsPriority)); + shuffle($objectIds); + return array_merge($objectIdsPriority, $objectIds); + } + return $objectIdsPriority; + } + + /** + * Delete a tag by name if it exists + * @param string $tagName + * @return void + */ + public function removeTag(string $tagName): void { + try { + $tag = $this->getTag($tagName, false); + } catch (TagNotFoundException) { + return; + } + $this->tagService->deleteTags([$tag->getId()]); + $this->logger->debug("Tag removed: " . $tagName); + } + + /** + * Removes all tags + * @return void + */ + public function resetAllTags(): void { + $this->removeTag(self::CLEAN); + $this->removeTag(self::MALICIOUS); + $this->removeTag(self::UNSCANNED); + $this->removeTag(self::PUP); + $this->logger->info("All tags removed"); + } } diff --git a/lib/Service/VerdictService.php b/lib/Service/VerdictService.php index 3c467d96..7bce835a 100644 --- a/lib/Service/VerdictService.php +++ b/lib/Service/VerdictService.php @@ -20,183 +20,176 @@ use VaasSdk\Vaas; use VaasSdk\VaasOptions; -class VerdictService -{ - public const MAX_FILE_SIZE = 2147483646; - private const APP_ID = "gdatavaas"; - - private string $username; - private string $password; - private string $clientId; - private string $clientSecret; - private string $authMethod; - private string $tokenEndpoint; - private string $vaasUrl; - private ResourceOwnerPasswordGrantAuthenticator|ClientCredentialsGrantAuthenticator $authenticator; - private IConfig $appConfig; - private FileService $fileService; - private TagService $tagService; - private ?Vaas $vaas = null; - private LoggerInterface $logger; - - public function __construct(LoggerInterface $logger, IConfig $appConfig, FileService $fileService, TagService $tagService) - { - $this->logger = $logger; - $this->appConfig = $appConfig; - $this->fileService = $fileService; - $this->tagService = $tagService; - - $this->authMethod = $this->appConfig->getAppValue(self::APP_ID, 'authMethod', 'ClientCredentials'); - $this->tokenEndpoint = $this->appConfig->getAppValue(self::APP_ID, 'tokenEndpoint', 'https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token'); - $this->vaasUrl = $this->appConfig->getAppValue(self::APP_ID, 'vaasUrl', 'wss://gateway.staging.vaas.gdatasecurity.de'); - $this->clientId = $this->appConfig->getAppValue(self::APP_ID, 'clientId'); - $this->clientSecret = $this->appConfig->getAppValue(self::APP_ID, 'clientSecret'); - $this->username = $this->appConfig->getAppValue(self::APP_ID, 'username'); - $this->password = $this->appConfig->getAppValue(self::APP_ID, 'password'); - } - - /** - * Scans a file for malicious content with G DATA Verdict-as-a-Service and handles the result. - * @param int $fileId - * @return VaasVerdict - * @throws InvalidPathException - * @throws InvalidSha256Exception - * @throws NotFoundException - * @throws UploadFailedException - * @throws TimeoutException - * @throws NotPermittedException - * @throws FileDoesNotExistException if the VaaS SDK could not find the file - * @throws EntityTooLargeException if the file that should be scanned is too large - * @throws VaasAuthenticationException if the authentication with the VaaS service fails - */ - public function scanFileById(int $fileId): VaasVerdict - { - $node = $this->fileService->getNodeFromFileId($fileId); - $filePath = $node->getStorage()->getLocalFile($node->getInternalPath()); - if ($node->getSize() > self::MAX_FILE_SIZE) { - throw new EntityTooLargeException("File is too large"); - } - - $blocklist = $this->getBlocklist(); - $this->logger->info("Blocklist: " . implode(", ", $blocklist)); - foreach ($blocklist as $blocklistItem) { - if (str_contains(strtolower($filePath), strtolower($blocklistItem))) { - $this->logger->info("File " . $node->getName() . " (" . $fileId . ") is in the blocklist and will not be scanned."); - throw new NotPermittedException("File is in the blocklist"); - } - } - - $allowlist = $this->getAllowlist(); - $this->logger->info("Allowlist: " . implode(", ", $allowlist)); - foreach ($allowlist as $allowlistItem) { - if (!str_contains(strtolower($filePath), strtolower($allowlistItem))) { - $this->logger->info("File " . $node->getName() . " (" . $fileId . ") is not in the allowlist and will not be scanned."); - throw new NotPermittedException("File is not in the allowlist"); - } - } - - $verdict = $this->scan($filePath); - - $this->logger->info("VaaS scan result for " . $node->getName() . " (" . $fileId . "): Verdict: " - . $verdict->Verdict->value . ", Detection: " . $verdict->Detection . ", SHA256: " . $verdict->Sha256 . - ", FileType: " . $verdict->FileType . ", MimeType: " . $verdict->MimeType . ", UUID: " . $verdict->Guid); - - $this->tagService->removeTagFromFile(TagService::CLEAN, $fileId); - $this->tagService->removeTagFromFile(TagService::MALICIOUS, $fileId); - $this->tagService->removeTagFromFile(TagService::PUP, $fileId); - $this->tagService->removeTagFromFile(TagService::UNSCANNED, $fileId); - - switch ($verdict->Verdict->value) { - case TagService::CLEAN: - $this->tagService->setTag($fileId, TagService::CLEAN); - break; - case TagService::MALICIOUS: - $this->tagService->setTag($fileId, TagService::MALICIOUS); - try { - $this->fileService->setMaliciousPrefixIfActivated($fileId); - $this->fileService->moveFileToQuarantineFolderIfDefined($fileId); - } catch (Exception) { - } - break; - case TagService::PUP: - $this->tagService->setTag($fileId, TagService::PUP); - break; - default: - $this->tagService->setTag($fileId, TagService::UNSCANNED); - break; - } - - return $verdict; - } - - public function scan(string $filePath): VaasVerdict - { - if ($this->vaas == null) { - $this->vaas = $this->createAndConnectVaas(); - } - - try { - $verdict = $this->vaas->ForFile($filePath); - - return $verdict; - } catch (Exception $e) { - $this->logger->error("Vaas for file: " . $e->getMessage()); - $this->vaas = null; - throw $e; - } - } - - /** - * Parses the allowlist from the app settings and returns it as an array. - * @return array - */ - private function getAllowlist(): array - { - $allowlist = $this->appConfig->getAppValue(self::APP_ID, 'allowlist'); - $allowlist = preg_replace('/\s+/', '', $allowlist); - if (empty($allowlist)) { - return []; - } - return explode(",", $allowlist); - } - - /** - * Parses the blocklist from the app settings and returns it as an array. - * @return array - */ - private function getBlocklist(): array - { - $blocklist = $this->appConfig->getAppValue(self::APP_ID, 'blocklist'); - $blocklist = preg_replace('/\s+/', '', $blocklist); - if (empty($blocklist)) { - return []; - } - return explode(",", $blocklist); - } - - /** - * @throws VaasAuthenticationException - */ - private function createAndConnectVaas(): Vaas - { - if ($this->authMethod === 'ResourceOwnerPassword') { - $this->authenticator = new ResourceOwnerPasswordGrantAuthenticator( - "nextcloud-customer", - $this->username, - $this->password, - $this->tokenEndpoint - ); - } elseif ($this->authMethod === 'ClientCredentials') { - $this->authenticator = new ClientCredentialsGrantAuthenticator( - $this->clientId, - $this->clientSecret, - $this->tokenEndpoint - ); - } - - $options = new VaasOptions(false, false); - $vaas = new Vaas($this->vaasUrl, $this->logger, $options); - $vaas->Connect($this->authenticator->getToken()); - return $vaas; - } +class VerdictService { + public const MAX_FILE_SIZE = 2147483646; + private const APP_ID = "gdatavaas"; + + private string $username; + private string $password; + private string $clientId; + private string $clientSecret; + private string $authMethod; + private string $tokenEndpoint; + private string $vaasUrl; + private ResourceOwnerPasswordGrantAuthenticator|ClientCredentialsGrantAuthenticator $authenticator; + private IConfig $appConfig; + private FileService $fileService; + private TagService $tagService; + private ?Vaas $vaas = null; + private LoggerInterface $logger; + + public function __construct(LoggerInterface $logger, IConfig $appConfig, FileService $fileService, TagService $tagService) { + $this->logger = $logger; + $this->appConfig = $appConfig; + $this->fileService = $fileService; + $this->tagService = $tagService; + + $this->authMethod = $this->appConfig->getAppValue(self::APP_ID, 'authMethod', 'ClientCredentials'); + $this->tokenEndpoint = $this->appConfig->getAppValue(self::APP_ID, 'tokenEndpoint', 'https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token'); + $this->vaasUrl = $this->appConfig->getAppValue(self::APP_ID, 'vaasUrl', 'wss://gateway.staging.vaas.gdatasecurity.de'); + $this->clientId = $this->appConfig->getAppValue(self::APP_ID, 'clientId'); + $this->clientSecret = $this->appConfig->getAppValue(self::APP_ID, 'clientSecret'); + $this->username = $this->appConfig->getAppValue(self::APP_ID, 'username'); + $this->password = $this->appConfig->getAppValue(self::APP_ID, 'password'); + } + + /** + * Scans a file for malicious content with G DATA Verdict-as-a-Service and handles the result. + * @param int $fileId + * @return VaasVerdict + * @throws InvalidPathException + * @throws InvalidSha256Exception + * @throws NotFoundException + * @throws UploadFailedException + * @throws TimeoutException + * @throws NotPermittedException + * @throws FileDoesNotExistException if the VaaS SDK could not find the file + * @throws EntityTooLargeException if the file that should be scanned is too large + * @throws VaasAuthenticationException if the authentication with the VaaS service fails + */ + public function scanFileById(int $fileId): VaasVerdict { + $node = $this->fileService->getNodeFromFileId($fileId); + $filePath = $node->getStorage()->getLocalFile($node->getInternalPath()); + if ($node->getSize() > self::MAX_FILE_SIZE) { + throw new EntityTooLargeException("File is too large"); + } + + $blocklist = $this->getBlocklist(); + $this->logger->info("Blocklist: " . implode(", ", $blocklist)); + foreach ($blocklist as $blocklistItem) { + if (str_contains(strtolower($filePath), strtolower($blocklistItem))) { + $this->logger->info("File " . $node->getName() . " (" . $fileId . ") is in the blocklist and will not be scanned."); + throw new NotPermittedException("File is in the blocklist"); + } + } + + $allowlist = $this->getAllowlist(); + $this->logger->info("Allowlist: " . implode(", ", $allowlist)); + foreach ($allowlist as $allowlistItem) { + if (!str_contains(strtolower($filePath), strtolower($allowlistItem))) { + $this->logger->info("File " . $node->getName() . " (" . $fileId . ") is not in the allowlist and will not be scanned."); + throw new NotPermittedException("File is not in the allowlist"); + } + } + + $verdict = $this->scan($filePath); + + $this->logger->info("VaaS scan result for " . $node->getName() . " (" . $fileId . "): Verdict: " + . $verdict->Verdict->value . ", Detection: " . $verdict->Detection . ", SHA256: " . $verdict->Sha256 . + ", FileType: " . $verdict->FileType . ", MimeType: " . $verdict->MimeType . ", UUID: " . $verdict->Guid); + + $this->tagService->removeTagFromFile(TagService::CLEAN, $fileId); + $this->tagService->removeTagFromFile(TagService::MALICIOUS, $fileId); + $this->tagService->removeTagFromFile(TagService::PUP, $fileId); + $this->tagService->removeTagFromFile(TagService::UNSCANNED, $fileId); + + switch ($verdict->Verdict->value) { + case TagService::CLEAN: + $this->tagService->setTag($fileId, TagService::CLEAN); + break; + case TagService::MALICIOUS: + $this->tagService->setTag($fileId, TagService::MALICIOUS); + try { + $this->fileService->setMaliciousPrefixIfActivated($fileId); + $this->fileService->moveFileToQuarantineFolderIfDefined($fileId); + } catch (Exception) { + } + break; + case TagService::PUP: + $this->tagService->setTag($fileId, TagService::PUP); + break; + default: + $this->tagService->setTag($fileId, TagService::UNSCANNED); + break; + } + + return $verdict; + } + + public function scan(string $filePath): VaasVerdict { + if ($this->vaas == null) { + $this->vaas = $this->createAndConnectVaas(); + } + + try { + $verdict = $this->vaas->ForFile($filePath); + + return $verdict; + } catch (Exception $e) { + $this->logger->error("Vaas for file: " . $e->getMessage()); + $this->vaas = null; + throw $e; + } + } + + /** + * Parses the allowlist from the app settings and returns it as an array. + * @return array + */ + private function getAllowlist(): array { + $allowlist = $this->appConfig->getAppValue(self::APP_ID, 'allowlist'); + $allowlist = preg_replace('/\s+/', '', $allowlist); + if (empty($allowlist)) { + return []; + } + return explode(",", $allowlist); + } + + /** + * Parses the blocklist from the app settings and returns it as an array. + * @return array + */ + private function getBlocklist(): array { + $blocklist = $this->appConfig->getAppValue(self::APP_ID, 'blocklist'); + $blocklist = preg_replace('/\s+/', '', $blocklist); + if (empty($blocklist)) { + return []; + } + return explode(",", $blocklist); + } + + /** + * @throws VaasAuthenticationException + */ + private function createAndConnectVaas(): Vaas { + if ($this->authMethod === 'ResourceOwnerPassword') { + $this->authenticator = new ResourceOwnerPasswordGrantAuthenticator( + "nextcloud-customer", + $this->username, + $this->password, + $this->tokenEndpoint + ); + } elseif ($this->authMethod === 'ClientCredentials') { + $this->authenticator = new ClientCredentialsGrantAuthenticator( + $this->clientId, + $this->clientSecret, + $this->tokenEndpoint + ); + } + + $options = new VaasOptions(false, false); + $vaas = new Vaas($this->vaasUrl, $this->logger, $options); + $vaas->Connect($this->authenticator->getToken()); + return $vaas; + } } diff --git a/lib/Settings/VaasAdmin.php b/lib/Settings/VaasAdmin.php index 82221288..15c3f8a5 100644 --- a/lib/Settings/VaasAdmin.php +++ b/lib/Settings/VaasAdmin.php @@ -6,47 +6,42 @@ use OCP\IConfig; use OCP\Settings\ISettings; -class VaasAdmin implements ISettings -{ - private const APP_ID = 'gdatavaas'; - - private IConfig $config; - - public function __construct(IConfig $config) - { - $this->config = $config; - } - - public function getForm(): TemplateResponse - { - $params = [ - 'username' => $this->config->getAppValue(self::APP_ID, 'username'), - 'password' => $this->config->getAppValue(self::APP_ID, 'password'), - 'clientId' => $this->config->getAppValue(self::APP_ID, 'clientId'), - 'clientSecret' => $this->config->getAppValue(self::APP_ID, 'clientSecret'), - 'authMethod' => $this->config->getAppValue(self::APP_ID, 'authMethod', 'ClientCredentials'), - 'tokenEndpoint' => $this->config->getAppValue(self::APP_ID, 'tokenEndpoint', 'https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token'), - 'vaasUrl' => $this->config->getAppValue(self::APP_ID, 'vaasUrl', 'wss://gateway.staging.vaas.gdatasecurity.de'), - 'quarantineFolder' => $this->config->getAppValue(self::APP_ID, 'quarantineFolder', 'Quarantine'), - 'autoScanFiles' => $this->config->getAppValue(self::APP_ID, 'autoScanFiles', false), - 'scanOnlyNewFiles' => $this->config->getAppValue(self::APP_ID, 'scanOnlyNewFiles', true), - 'prefixMalicious' => $this->config->getAppValue(self::APP_ID, 'prefixMalicious', true), - 'disableUnscannedTag' => $this->config->getAppValue(self::APP_ID, 'disableUnscannedTag', false), - 'allowlist' => $this->config->getAppValue(self::APP_ID, 'allowlist'), - 'blocklist' => $this->config->getAppValue(self::APP_ID, 'blocklist'), - 'scanQueueLength' => $this->config->getAppValue(self::APP_ID, 'scanQueueLength', 5), - ]; - - return new TemplateResponse(self::APP_ID, 'admin', $params); - } - - public function getSection(): string - { - return 'vaas'; - } - - public function getPriority(): int - { - return 10; - } +class VaasAdmin implements ISettings { + private const APP_ID = 'gdatavaas'; + + private IConfig $config; + + public function __construct(IConfig $config) { + $this->config = $config; + } + + public function getForm(): TemplateResponse { + $params = [ + 'username' => $this->config->getAppValue(self::APP_ID, 'username'), + 'password' => $this->config->getAppValue(self::APP_ID, 'password'), + 'clientId' => $this->config->getAppValue(self::APP_ID, 'clientId'), + 'clientSecret' => $this->config->getAppValue(self::APP_ID, 'clientSecret'), + 'authMethod' => $this->config->getAppValue(self::APP_ID, 'authMethod', 'ClientCredentials'), + 'tokenEndpoint' => $this->config->getAppValue(self::APP_ID, 'tokenEndpoint', 'https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token'), + 'vaasUrl' => $this->config->getAppValue(self::APP_ID, 'vaasUrl', 'wss://gateway.staging.vaas.gdatasecurity.de'), + 'quarantineFolder' => $this->config->getAppValue(self::APP_ID, 'quarantineFolder', 'Quarantine'), + 'autoScanFiles' => $this->config->getAppValue(self::APP_ID, 'autoScanFiles', false), + 'scanOnlyNewFiles' => $this->config->getAppValue(self::APP_ID, 'scanOnlyNewFiles', true), + 'prefixMalicious' => $this->config->getAppValue(self::APP_ID, 'prefixMalicious', true), + 'disableUnscannedTag' => $this->config->getAppValue(self::APP_ID, 'disableUnscannedTag', false), + 'allowlist' => $this->config->getAppValue(self::APP_ID, 'allowlist'), + 'blocklist' => $this->config->getAppValue(self::APP_ID, 'blocklist'), + 'scanQueueLength' => $this->config->getAppValue(self::APP_ID, 'scanQueueLength', 5), + ]; + + return new TemplateResponse(self::APP_ID, 'admin', $params); + } + + public function getSection(): string { + return 'vaas'; + } + + public function getPriority(): int { + return 10; + } } diff --git a/lib/Settings/VaasAdminSection.php b/lib/Settings/VaasAdminSection.php index 07a2ce15..8b3a306a 100644 --- a/lib/Settings/VaasAdminSection.php +++ b/lib/Settings/VaasAdminSection.php @@ -5,29 +5,23 @@ use OCP\IURLGenerator; use OCP\Settings\IIconSection; -class VaasAdminSection implements IIconSection -{ - public function __construct(private IURLGenerator $urlGenerator) - { - } +class VaasAdminSection implements IIconSection { + public function __construct(private IURLGenerator $urlGenerator) { + } - public function getName(): string - { - return 'Verdict-as-a-Service'; - } + public function getName(): string { + return 'Verdict-as-a-Service'; + } - public function getID(): string - { - return 'vaas'; - } + public function getID(): string { + return 'vaas'; + } - public function getPriority(): int - { - return 40; - } + public function getPriority(): int { + return 40; + } - public function getIcon(): string - { - return $this->urlGenerator->imagePath('gdatavaas', 'gdatalogo.svg'); - } + public function getIcon(): string { + return $this->urlGenerator->imagePath('gdatavaas', 'gdatalogo.svg'); + } } diff --git a/templates/admin.php b/templates/admin.php index 83e3bd9a..fac685b9 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -18,10 +18,10 @@ From a449c4241d33f513b71092341d9ad59cbff110bc Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Tue, 4 Jun 2024 11:17:20 +0000 Subject: [PATCH 2/2] apply cs-fixer rules --- lib/Activity/Provider.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/Activity/Provider.php b/lib/Activity/Provider.php index f6fc5fb7..6d190cf6 100644 --- a/lib/Activity/Provider.php +++ b/lib/Activity/Provider.php @@ -1,4 +1,5 @@ * @@ -20,6 +21,7 @@ * along with this program. If not, see . * */ + namespace OCA\GDataVaas\Activity; use OCA\GDataVaas\AppInfo\Application; @@ -29,7 +31,8 @@ use OCP\L10N\IFactory; use Psr\Log\LoggerInterface; -class Provider implements IProvider { +class Provider implements IProvider +{ public const TYPE_VIRUS_DETECTED = 'virus_detected'; public const SUBJECT_VIRUS_DETECTED = 'virus_detected'; @@ -45,17 +48,19 @@ class Provider implements IProvider { private $urlGenerator; private LoggerInterface $logger; - public function __construct(IFactory $languageFactory, IURLGenerator $urlGenerator, LoggerInterface $logger) { + public function __construct(IFactory $languageFactory, IURLGenerator $urlGenerator, LoggerInterface $logger) + { $this->languageFactory = $languageFactory; $this->urlGenerator = $urlGenerator; $this->logger = $logger; } - public function parse($language, IEvent $event, ?IEvent $previousEvent = null) { + public function parse($language, IEvent $event, ?IEvent $previousEvent = null) + { if ($event->getApp() !== Application::APP_ID || $event->getType() !== self::TYPE_VIRUS_DETECTED) { throw new \InvalidArgumentException(); } - + $parameters = []; $subject = ''; @@ -122,7 +127,8 @@ public function parse($language, IEvent $event, ?IEvent $previousEvent = null) { return $event; } - private function setSubjects(IEvent $event, string $subject, array $parameters): void { + private function setSubjects(IEvent $event, string $subject, array $parameters): void + { $placeholders = $replacements = []; foreach ($parameters as $placeholder => $parameter) { $placeholders[] = '{' . $placeholder . '}';