Skip to content

Commit

Permalink
Merge pull request #41 from GDATASoftwareAG/tag_on_upload
Browse files Browse the repository at this point in the history
Tag on upload
  • Loading branch information
pstadermann authored Jun 13, 2024
2 parents 7dd3fdf + 5d77499 commit 14997b7
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 32 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
7 changes: 5 additions & 2 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
}

Expand Down
11 changes: 10 additions & 1 deletion lib/AvirWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class AvirWrapper extends Wrapper {
/** @var ActivityManager */
protected $activityManager;

/** @var bool */
/** @var bool */
protected $isHomeStorage;

/** @var bool */
Expand Down Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions lib/CacheEntryListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace OCA\GDataVaas;

use OCA\GDataVaas\Service\TagService;
use OCA\GDataVaas\Service\VerdictService;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Cache\AbstractCacheEvent;
use OCP\Files\Cache\CacheEntryInsertedEvent;
use OCP\Files\Cache\CacheEntryUpdatedEvent;
use Psr\Log\LoggerInterface;

class CacheEntryListener implements IEventListener
{
private LoggerInterface $logger;

private TagService $tagService;

private VerdictService $verdictService;

public function __construct(LoggerInterface $logger, TagService $tagService, VerdictService $verdictService)
{
$this->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/');
}
}
9 changes: 9 additions & 0 deletions lib/Service/TagService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
105 changes: 82 additions & 23 deletions lib/Service/VerdictService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -100,39 +103,61 @@ 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();
}

try {
$verdict = $this->vaas->ForFile($filePath);

$this->lastVaasVerdict = $verdict;

return $verdict;
} catch (Exception $e) {
$this->logger->error("Vaas for file: " . $e->getMessage());
Expand All @@ -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
Expand Down

0 comments on commit 14997b7

Please sign in to comment.