diff --git a/.devcontainer/dev/devcontainer.json b/.devcontainer/dev/devcontainer.json deleted file mode 100644 index 1638e2f9..00000000 --- a/.devcontainer/dev/devcontainer.json +++ /dev/null @@ -1,41 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose -{ - "name": "php container", - // Update the 'dockerComposeFile' list if you have more compose files or use different names. - // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. - "dockerComposeFile": [ - "../../devcontainer.yaml" - ], - // The 'service' property is the name of the service for the container that VS Code should - // use. Update this value and .devcontainer/docker-compose.yml to the real service name. - "service": "dev", - // The optional 'workspaceFolder' property is the path VS Code should open by default when - // connected. This is typically a file mount in .devcontainer/docker-compose.yml - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - "features": { - "ghcr.io/devcontainers/features/php:1": {}, - "ghcr.io/devcontainers-contrib/features/node-asdf:0": {} - }, - "remoteUser": "www-data", - "remoteEnv": { - "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}" - }, - "containerEnv": { - "SHELL": "/bin/bash" - } - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - // Uncomment the next line if you want start specific services in your Docker Compose config. - // "runServices": [], - // Uncomment the next line if you want to keep your containers running after VS Code shuts down. - // "shutdownAction": "none", - // Uncomment the next line to run commands after the container is created. - // "postCreateCommand": "cat /etc/os-release", - // Configure tool-specific properties. - // "customizations": {}, - // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "devcontainer" -} \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..11be65ca --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/php +{ + "name": "PHP", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/php:1-8.2-bullseye", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Configure tool-specific properties. + // "customizations": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [8080] + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "sudo chmod a+x \"$(pwd)\" && sudo rm -rf /var/www/html && sudo ln -s \"$(pwd)\" /var/www/html" + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.devcontainer/nextcloud/devcontainer.json b/.devcontainer/nextcloud/devcontainer.json deleted file mode 100644 index 0ae124b8..00000000 --- a/.devcontainer/nextcloud/devcontainer.json +++ /dev/null @@ -1,40 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose -{ - "name": "nextcloud", - // Update the 'dockerComposeFile' list if you have more compose files or use different names. - // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. - "dockerComposeFile": [ - "../../devcontainer.yaml" - ], - // The 'service' property is the name of the service for the container that VS Code should - // use. Update this value and .devcontainer/docker-compose.yml to the real service name. - "service": "nextcloud", - // The optional 'workspaceFolder' property is the path VS Code should open by default when - // connected. This is typically a file mount in .devcontainer/docker-compose.yml - "features": { - "ghcr.io/devcontainers/features/php:1": {} - }, - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - "remoteEnv": { - "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}" - }, - "remoteUser": "root", - "containerEnv": { - "SHELL": "/bin/bash" - } - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - // Uncomment the next line if you want start specific services in your Docker Compose config. - // "runServices": [], - // Uncomment the next line if you want to keep your containers running after VS Code shuts down. - // "shutdownAction": "none", - // Uncomment the next line to run commands after the container is created. - // "postCreateCommand": "cat /etc/os-release", - // Configure tool-specific properties. - // "customizations": {}, - // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "devcontainer" -} \ No newline at end of file diff --git a/Makefile b/Makefile index b79cea5e..32533a1f 100644 --- a/Makefile +++ b/Makefile @@ -110,7 +110,7 @@ distclean: clean appstore: rm -rf $(appstore_build_directory) mkdir -p $(appstore_build_directory) - tar cvzf $(appstore_package_name).tar.gz \ + tar czf $(appstore_package_name).tar.gz \ --transform s/$(app_directory_name)/$(app_real_name)/ \ --exclude-vcs \ --exclude="../$(app_directory_name)/build" \ diff --git a/appinfo/info.xml b/appinfo/info.xml index 26168b84..078ea436 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -27,4 +27,12 @@ pgsql mysql sqlite + + + OCA\GDataVaas\Activity\Setting + + + OCA\GDataVaas\Activity\Provider + + diff --git a/composer.json b/composer.json index 3d024c09..de0aab5e 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ } ], "require": { - "gdata/vaas": "^8.0.0" + "gdata/vaas": "^8.0.2" }, "require-dev": { "nextcloud/ocp": "dev-stable28", @@ -17,7 +17,7 @@ "nextcloud/coding-standard": "^v1.1.1" }, "scripts": { - "lint": "find . -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l", + "lint": "find lib -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l", "cs:check": "php-cs-fixer fix --dry-run --diff .", "cs:fix": "php-cs-fixer fix .", "psalm": "vendor/psalm/phar/psalm.phar --threads=1", diff --git a/img/favicon.svg b/img/favicon.svg new file mode 100644 index 00000000..3e369ff8 --- /dev/null +++ b/img/favicon.svg @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/install.sh b/install.sh index 008766f4..9eb637d7 100755 --- a/install.sh +++ b/install.sh @@ -1,17 +1,32 @@ #!/bin/bash docker stop nextcloud-container -docker run -d --name nextcloud-container --rm --publish 80:80 --publish 8080:8080 --publish 8443:8443 nextcloud:stable +sleep 1 +docker run -d --name nextcloud-container --rm --publish 80:80 nextcloud:28 echo "Waiting for sunrise..." -sleep 15 -docker exec --user www-data -it nextcloud-container php occ maintenance:install --admin-user=admin --admin-pass=admin + +until docker exec --user www-data -it nextcloud-container php occ maintenance:install --admin-user=admin --admin-pass=admin >/dev/null +do + echo "Try again waiting 2 seconds" + sleep 2 +done + +make build make appstore tar -xf ./build/artifacts/gdatavaas.tar.gz -C ./build/artifacts -docker exec --user www-data -it nextcloud-container mkdir apps/gdatavaas -docker cp ./build/artifacts/gdatavaas nextcloud-container:/var/www/html/apps/gdatavaas +docker cp ./build/artifacts/gdatavaas nextcloud-container:/var/www/html/apps/ +docker exec -it nextcloud-container chown -R www-data:www-data /var/www/html/apps/gdatavaas docker exec --user www-data -it nextcloud-container php occ app:update --all docker exec --user www-data -it nextcloud-container php occ app:enable gdatavaas -docker exec --user www-data -it nextcloud-container php occ config:app:set gdatavaas username --value=vaas-integration-test +docker exec --user www-data -it nextcloud-container php occ config:app:set gdatavaas clientId --value=$CLIENT_ID +docker exec --user www-data -it nextcloud-container php occ config:app:set gdatavaas clientSecret --value=$CLIENT_SECRET +docker exec --user www-data -it nextcloud-container php occ config:app:set gdatavaas authMethod --value=ClientCredentials +docker exec --user www-data -it nextcloud-container php occ config:app:set gdatavaas autoScanFiles --value=true +docker exec --user www-data -it nextcloud-container php occ config:app:set gdatavaas scanQueueLength --value=100 + +docker exec --user www-data -it nextcloud-container php occ log:manage --level DEBUG +docker exec --user www-data -it nextcloud-container php occ app:disable firstrunwizard + -docker exec --user www-data -it nextcloud-container php cron.php +# docker exec --user www-data -it nextcloud-container php cron.php diff --git a/lib/Activity/Provider.php b/lib/Activity/Provider.php new file mode 100644 index 00000000..05f8b7ea --- /dev/null +++ b/lib/Activity/Provider.php @@ -0,0 +1,139 @@ + + * + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\GDataVaas\Activity; + +use OCA\GDataVaas\AppInfo\Application; +use OCP\Activity\IEvent; +use OCP\Activity\IProvider; +use OCP\IURLGenerator; +use OCP\L10N\IFactory; +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); + } +} diff --git a/lib/Activity/Setting.php b/lib/Activity/Setting.php new file mode 100644 index 00000000..c07b1db8 --- /dev/null +++ b/lib/Activity/Setting.php @@ -0,0 +1,59 @@ + + * + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\GDataVaas\Activity; + +use OCP\Activity\ISetting; + +class Setting implements ISetting { + + public function __construct() { + } + + public function getIdentifier() { + return Provider::TYPE_VIRUS_DETECTED; + } + + public function getName() { + return 'Antivirus detected a virus'; + } + + public function getPriority() { + return 70; + } + + public function canChangeStream() { + return false; + } + + public function isDefaultEnabledStream() { + return true; + } + + public function canChangeMail() { + return false; + } + + public function isDefaultEnabledMail() { + return false; + } +} diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 2459d818..5c1db204 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -4,14 +4,26 @@ namespace OCA\GDataVaas\AppInfo; +use OC\Files\Filesystem; +use OCA\GDataVaas\AvirWrapper; +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\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 +class Application extends App implements IBootstrap { public const APP_ID = 'gdatavaas'; @@ -28,19 +40,61 @@ public function __construct() $eventDispatcher->addListener(LoadAdditionalScriptsEvent::class, function () { Util::addScript(self::APP_ID, 'gdatavaas-files-action'); }); - - $this->register(); } /** * Load the composer autoloader if it exists * @return void */ - public function register(): 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; + } + */ + + $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. } } diff --git a/lib/AvirWrapper.php b/lib/AvirWrapper.php new file mode 100644 index 00000000..6c0c3031 --- /dev/null +++ b/lib/AvirWrapper.php @@ -0,0 +1,183 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OCA\GDataVaas; + +use OC\Files\Storage\Wrapper\Wrapper; +use OCA\Files_Trashbin\Trash\ITrashManager; +use OCA\GDataVaas\Activity\Provider; +use OCA\GDataVaas\AppInfo\Application; +use OCA\GDataVaas\Service\VerdictService; +use OCP\Activity\IManager as ActivityManager; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\InvalidContentException; +use OCP\IL10N; +use Psr\Log\LoggerInterface; +use VaasSdk\Message\Verdict; + +class AvirWrapper extends Wrapper { + /** + * Modes that are used for writing + * @var array + */ + private $writingModes = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+']; + + protected VerdictService $verdictService; + + /** @var IL10N */ + protected $l10n; + + /** @var LoggerInterface */ + protected $logger; + + /** @var ActivityManager */ + protected $activityManager; + + /** @var bool */ + protected $isHomeStorage; + + /** @var bool */ + private $shouldScan = true; + + /** @var bool */ + private $trashEnabled; + + /** + * @param array $parameters + */ + public function __construct($parameters) { + parent::__construct($parameters); + $this->verdictService = $parameters['verdictService']; + $this->logger = $parameters['logger']; + $this->activityManager = $parameters['activityManager']; + $this->isHomeStorage = $parameters['isHomeStorage']; + $this->trashEnabled = $parameters['trashEnabled']; + + /** @var IEventDispatcher $eventDispatcher */ + $eventDispatcher = $parameters['eventDispatcher']; + } + + /** + * Asynchronously scan data that are written to the file + * @param string $path + * @param string $mode + * @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); + + /* + * Only check when + * - it is a resource + * - it is a writing mode + * - if it is a homestorage it starts with files/ + * - if it is not a homestorage we always wrap (external storages) + */ + if ($this->shouldWrap($path) && is_resource($stream) && $this->isWritingMode($mode)) { + $stream = $this->wrapSteam($path, $stream); + } + return $stream; + } + + public function writeStream(string $path, $stream, int $size = null): int { + if ($this->shouldWrap($path)) { + $stream = $this->wrapSteam($path, $stream); + } + return parent::writeStream($path, $stream, $size); + } + + private function shouldWrap(string $path): bool { + return $this->shouldScan + && (!$this->isHomeStorage + || (strpos($path, 'files/') === 0 + || strpos($path, '/files/') === 0) + ); + } + + private function wrapSteam(string $path, $stream) { + try { + $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); + + $verdict = $this->verdictService->scan($localPath); + $logger->debug("Verdict for " . $localPath . " is " . $verdict->Verdict->value); + + if ($verdict->Verdict == Verdict::MALICIOUS) { + $logger->debug("Removing malicious file " . $localPath); + + //prevent from going to trashbin + if ($this->trashEnabled) { + /** @var ITrashManager $trashManager */ + $trashManager = \OC::$server->query(ITrashManager::class); + $trashManager->pauseTrash(); + } + + $owner = $this->getOwner($path); + $this->unlink($path); + + if ($this->trashEnabled) { + /** @var ITrashManager $trashManager */ + $trashManager = \OC::$server->query(ITrashManager::class); + $trashManager->resumeTrash(); + } + + $this->logger->warning( + 'Infected file deleted. ' . $verdict->Detection + . ' 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"]) + ->setMessage(Provider::MESSAGE_FILE_DELETED) + ->setObject('', 0, $path) + ->setAffectedUser($owner) + ->setType(Provider::TYPE_VIRUS_DETECTED); + $this->activityManager->publish($activity); + + throw new InvalidContentException( + sprintf( + 'Virus %s is detected in the file. Upload cannot be completed.', + $verdict->Detection + ) + ); + } + } + ); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + } + return $stream; + } + + /** + * Checks whether passed mode is suitable for writing + * @param string $mode + * @return bool + */ + private function isWritingMode($mode) { + // Strip unessential binary/text flags + $cleanMode = str_replace( + ['t', 'b'], + ['', ''], + $mode + ); + return in_array($cleanMode, $this->writingModes); + } +} diff --git a/lib/CallbackReadDataWrapper.php b/lib/CallbackReadDataWrapper.php new file mode 100644 index 00000000..b854fe25 --- /dev/null +++ b/lib/CallbackReadDataWrapper.php @@ -0,0 +1,60 @@ + [ + 'source' => $source, + 'readData' => $read, + 'write' => $write, + 'close' => $close, + 'readDir' => $readDir, + 'preClose' => $preClose + ] + ]); + return Wrapper::wrapSource($source, $context, 'callbackReadData', self::class); + } + + /** + * @return true + */ + protected function open() { + $context = $this->loadContext('callbackReadData'); + + $this->readDataCallback = $context['readData']; + $this->writeCallback = $context['write']; + $this->closeCallback = $context['close']; + $this->readDirCallBack = $context['readDir']; + return true; + } + + public function stream_read($count) { + $result = parent::stream_read($count); + if (is_callable($this->readDataCallback)) { + call_user_func($this->readDataCallback, strlen($result), $result); + } + return $result; + } +} diff --git a/lib/Service/TagService.php b/lib/Service/TagService.php index 5b9b1b82..041de672 100644 --- a/lib/Service/TagService.php +++ b/lib/Service/TagService.php @@ -42,12 +42,12 @@ public function __construct(LoggerInterface $logger, ISystemTagManager $systemTa public function getTag(string $name, bool $create = true): ISystemTag { try { - $tag = $this->tagService->getTag($name, true, true); + $tag = $this->tagService->getTag($name, true, false); } catch (TagNotFoundException) { if (!$create) { throw new TagNotFoundException(); } - $tag = $this->tagService->createTag($name, true, true); + $tag = $this->tagService->createTag($name, true, false); $this->logger->debug("Tag created: " . $name); } return $tag; @@ -73,7 +73,7 @@ public function setTag(int $fileId, string $tagName): void public function removeTagFromFile(string $tagName, int $fileId): bool { try { - $tag = $this->tagService->getTag($tagName, true, true); + $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; @@ -89,9 +89,11 @@ public function removeTagFromFile(string $tagName, int $fileId): bool */ public function hasCleanMaliciousOrPupTag(int $fileId): bool { - if ($this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::CLEAN)->getId()) || + 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())) { + $this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::PUP)->getId()) + ) { return true; } return false; diff --git a/lib/Service/VerdictService.php b/lib/Service/VerdictService.php index 6263e503..3c467d96 100644 --- a/lib/Service/VerdictService.php +++ b/lib/Service/VerdictService.php @@ -18,6 +18,7 @@ use VaasSdk\Message\VaasVerdict; use VaasSdk\ResourceOwnerPasswordGrantAuthenticator; use VaasSdk\Vaas; +use VaasSdk\VaasOptions; class VerdictService { @@ -45,7 +46,7 @@ public function __construct(LoggerInterface $logger, IConfig $appConfig, FileSer $this->fileService = $fileService; $this->tagService = $tagService; - $this->authMethod = $this->appConfig->getAppValue(self::APP_ID, 'authMethod', 'ResourceOwnerPassword'); + $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'); @@ -94,17 +95,7 @@ public function scanFileById(int $fileId): VaasVerdict } } - if ($this->vaas == null) { - $this->vaas = $this->createAndConnectVaas(); - } - - try { - $verdict = $this->vaas->ForFile($filePath); - } catch (Exception $e) { - $this->logger->error("Vaas for file: " . $e->getMessage()); - $this->vaas = null; - throw $e; - } + $verdict = $this->scan($filePath); $this->logger->info("VaaS scan result for " . $node->getName() . " (" . $fileId . "): Verdict: " . $verdict->Verdict->value . ", Detection: " . $verdict->Detection . ", SHA256: " . $verdict->Sha256 . @@ -138,6 +129,23 @@ public function scanFileById(int $fileId): VaasVerdict 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 @@ -186,7 +194,8 @@ private function createAndConnectVaas(): Vaas ); } - $vaas = new Vaas($this->vaasUrl); + $options = new VaasOptions(false, false); + $vaas = new Vaas($this->vaasUrl, $this->logger, $options); $vaas->Connect($this->authenticator->getToken()); return $vaas; }