diff --git a/README.md b/README.md index 19372532..d92c1b57 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,10 @@ folder "nextcloud-docker-dev" and running ```docker compose up nextcloud proxy`` ### Useful commands -| Description | Command | -|---------------------------|--------------------------------------------------------------------------------------| -| Trigger cronjobs manually | `docker exec --user www-data {nextcloud_container} php /var/www/html/cron.php` | -| Upgrade Nextcloud via CLI | `docker exec --user www-data {nextcloud_container} php occ upgrade` | -| Watch logs | `docker exec --user www-data {nextcloud_container} php occ log:watch` | -| Set log level to debug | `docker exec --user www-data {nextcloud_container} php occ log:manage --level DEBUG` | +| Description | Command | +|---------------------------|----------------------------------------------------------------------------------------------------------| +| Trigger cronjobs manually | `docker exec --user www-data {nextcloud_container} php /var/www/html/cron.php` | +| Upgrade Nextcloud via CLI | `docker exec --user www-data {nextcloud_container} php occ upgrade` | +| Watch logs | `docker exec --user www-data {nextcloud_container} php occ log:watch` | +| Set log level to debug | `docker exec --user www-data {nextcloud_container} php occ log:manage --level DEBUG` | +| Watch logs | `docker exec --user www-data nextcloud-container /bin/sh -c "tail -f data/nextcloud.log" \| jq .message` | diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index bc01e2f9..40253a71 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -7,6 +7,7 @@ use OC\Files\Filesystem; use OCA\GDataVaas\AvirWrapper; use OCA\GDataVaas\Service\VerdictService; +use OCA\GDataVaas\CacheEntryListener; use OCP\Activity\IManager; use OCP\App\IAppManager; use OCP\AppFramework\App; @@ -48,8 +49,10 @@ public function register(IRegistrationContext $context): void { if (file_exists($composerAutoloadFile)) { require_once $composerAutoloadFile; } - - // Util::connection is deprecated, but required ATM by FileSystem::addStorageWrapper + + CacheEntryListener::register($context); + + // Util::connection is deprecated, but required ATM by FileSystem::addStorageWrapper Util::connectHook('OC_Filesystem', 'preSetup', $this, 'setupWrapper'); } diff --git a/lib/AvirWrapper.php b/lib/AvirWrapper.php index 62732861..1b623e9f 100644 --- a/lib/AvirWrapper.php +++ b/lib/AvirWrapper.php @@ -39,7 +39,7 @@ class AvirWrapper extends Wrapper { /** @var ActivityManager */ protected $activityManager; - /** @var bool */ + /** @var bool */ protected $isHomeStorage; /** @var bool */ @@ -92,6 +92,15 @@ public function writeStream(string $path, $stream, ?int $size = null): int { return parent::writeStream($path, $stream, $size); } + public function rename($source, $target) { + if ($this->shouldWrap($source)) { + // After the upload apps/dav/lib/Connector/Sabre/File.php calls moveFromStorage which calls rename + $this->logger->debug(sprintf("rename(%s, %s)", $source, $target)); + $this->verdictService->onRename($this->getLocalFile($source), $this->getLocalFile($target)); + } + return parent::rename($source, $target); + } + private function shouldWrap(string $path): bool { return $this->shouldScan && (!$this->isHomeStorage diff --git a/lib/CacheEntryListener.php b/lib/CacheEntryListener.php new file mode 100644 index 00000000..15df297c --- /dev/null +++ b/lib/CacheEntryListener.php @@ -0,0 +1,55 @@ +logger = $logger; + $this->tagService = $tagService; + $this->verdictService = $verdictService; + } + + public static function register(IRegistrationContext $context): void { + $context->registerEventListener(CacheEntryInsertedEvent::class, CacheEntryListener::class); + $context->registerEventListener(CacheEntryUpdatedEvent::class, CacheEntryListener::class); + } + + public function handle(Event $event): void + { + if (!$event instanceof AbstractCacheEvent) { + return; + } + + $storage = $event->getStorage(); + $path = $event->getPath(); + $fileId = $event->getFileId(); + + if (self::shouldTag($path) && !$this->tagService->hasAnyVaasTag($fileId)) { + $this->logger->debug("Handling " . get_class($event) . " for " . $path); + + $this->verdictService->tagLastScannedFile($storage->getLocalFile($path), $fileId); + } + } + + private static function shouldTag(string $path): bool { + return str_starts_with($path, 'files/'); + } +} diff --git a/lib/Service/TagService.php b/lib/Service/TagService.php index 4fc2241e..f5bea13a 100644 --- a/lib/Service/TagService.php +++ b/lib/Service/TagService.php @@ -112,6 +112,15 @@ public function hasUnscannedTag(int $fileId): bool { return $this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::UNSCANNED)->getId()); } + /** + * Checks if a file has any Vaas tag. + * @param int $fileId + * @return bool + */ + public function hasAnyVaasTag(int $fileId): bool { + return $this->hasAnyButUnscannedTag($fileId) || $this->hasUnscannedTag($fileId); + } + /** * @param string $tagName * @param int $limit Count of object ids you want to get diff --git a/lib/Service/VerdictService.php b/lib/Service/VerdictService.php index fc87441a..c3a8f9f0 100644 --- a/lib/Service/VerdictService.php +++ b/lib/Service/VerdictService.php @@ -38,6 +38,9 @@ class VerdictService { private ?Vaas $vaas = null; private LoggerInterface $logger; + private string $lastLocalPath = ""; + private ?VaasVerdict $lastVaasVerdict = null; + public function __construct(LoggerInterface $logger, IConfig $appConfig, FileService $fileService, TagService $tagService) { $this->logger = $logger; $this->appConfig = $appConfig; @@ -70,7 +73,7 @@ public function __construct(LoggerInterface $logger, IConfig $appConfig, FileSer public function scanFileById(int $fileId): VaasVerdict { $node = $this->fileService->getNodeFromFileId($fileId); $filePath = $node->getStorage()->getLocalFile($node->getInternalPath()); - if ($node->getSize() > self::MAX_FILE_SIZE) { + if (self::isFileTooLargeToScan($filePath)) { $this->tagService->removeAllTagsFromFile($fileId); $this->tagService->setTag($fileId, TagService::WONT_SCAN); throw new EntityTooLargeException("File is too large"); @@ -100,32 +103,52 @@ public function scanFileById(int $fileId): VaasVerdict { . $verdict->Verdict->value . ", Detection: " . $verdict->Detection . ", SHA256: " . $verdict->Sha256 . ", FileType: " . $verdict->FileType . ", MimeType: " . $verdict->MimeType . ", UUID: " . $verdict->Guid); - $this->tagService->removeAllTagsFromFile($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; - } + $this->tagFile($fileId, $verdict->Verdict->value); return $verdict; } - public function scan(string $filePath): VaasVerdict { + private function tagFile(int $fileId, string $tagName) { + $this->tagService->removeAllTagsFromFile($fileId); + + switch ($tagName) { + case TagService::MALICIOUS: + $this->tagService->setTag($fileId, TagService::MALICIOUS); + try { + $this->fileService->setMaliciousPrefixIfActivated($fileId); + $this->fileService->moveFileToQuarantineFolderIfDefined($fileId); + } catch (Exception) { + } + break; + case TagService::CLEAN: + case TagService::PUP: + case TagService::WONT_SCAN: + default: + $this->tagService->setTag($fileId, $tagName); + break; + } + } + + /** + * Checks if a file is too large to be scanned. + * @param string $path + * @return bool + */ + public static function isFileTooLargeToScan(string $path): bool { + $size = filesize($path); + return !$size || $size > self::MAX_FILE_SIZE; + } + + + /** + * Scans a file for malicious content with G DATA Verdict-as-a-Service and returns the verdict. + * @param string $filePath The local path to the file to scan. + * @return VaasVerdict The verdict. + */ + public function scan(string $filePath): VaasVerdict { + $this->lastLocalPath = $filePath; + $this->lastVaasVerdict = null; + if ($this->vaas == null) { $this->vaas = $this->createAndConnectVaas(); } @@ -133,6 +156,8 @@ public function scan(string $filePath): VaasVerdict { try { $verdict = $this->vaas->ForFile($filePath); + $this->lastVaasVerdict = $verdict; + return $verdict; } catch (Exception $e) { $this->logger->error("Vaas for file: " . $e->getMessage()); @@ -141,6 +166,40 @@ public function scan(string $filePath): VaasVerdict { } } + /** + * Call this from a StorageWrapper, when a local file was renamed. This allows the scanner to track the name + * of the file that was scanned last. + * @param string $localSource The local source path. + * @param string $localTarget The local destination path. + */ + public function onRename(string $localSource, string $localTarget): void + { + if ($localSource === $this->lastLocalPath) { + $this->lastLocalPath = $localTarget; + } + } + + + /** + * Tag the file that was scanned last with it's verdict. Call this from an EventListener on CacheEntryInsertedEvent or + * CacheEntryUpdatedEvent. + * @param string $localPath The local path. + * @param int $fileId The corresponding file id to tag. + */ + public function tagLastScannedFile(string $localPath, int $fileId): void { + if (self::isFileTooLargeToScan($localPath)) { + $this->tagFile($fileId, TagService::WONT_SCAN); + return; + } + if ($localPath === $this->lastLocalPath) { + if ($this->lastVaasVerdict !== null) { + $this->tagFile($fileId, $this->lastVaasVerdict->Verdict->value); + } else { + $this->tagFile($fileId, TagService::UNSCANNED); + } + } + } + /** * Parses the allowlist from the app settings and returns it as an array. * @return array