Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tag on upload #41

Merged
merged 5 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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` |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are still multiple Watch logs

the log:tail command seems pretty good

$ docker exec -it --user www-data nextcloud-container php occ log:tail --help
Description:
  Tail the nextcloud logfile

Usage:
  log:tail [options] [--] [<lines>]

Arguments:
  lines                  The number of log entries to print [default: "10"]

Options:
  -f, --follow           Output new log entries as they appear
  -r, --raw              Output raw log json instead of formatted log item
      --output[=OUTPUT]  Output format (plain, json or json_pretty, default is plain) [default: "plain"]
  -h, --help             Display help for the given command. When no command is given display help for the list command
  -q, --quiet            Do not output any message
  -V, --version          Display this application version
      --ansi|--no-ansi   Force (or disable --no-ansi) ANSI output
  -n, --no-interaction   Do not ask any interactive question
      --no-warnings      Skip global warnings, show command output only
  -v|vv|vvv, --verbose   Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

same for the 'logs:watch' command:

$ docker exec -it --user www-data nextcloud-container php occ log:watch --help
Description:
  Watch the nextcloud logfile

Usage:
  log:watch [options]

Options:
  -r, --raw              Output raw log json instead of formatted log item
      --output[=OUTPUT]  Output format (plain, json or json_pretty, default is plain) [default: "plain"]
  -h, --help             Display help for the given command. When no command is given display help for the list command
  -q, --quiet            Do not output any message
  -V, --version          Display this application version
      --ansi|--no-ansi   Force (or disable --no-ansi) ANSI output
  -n, --no-interaction   Do not ask any interactive question
      --no-warnings      Skip global warnings, show command output only
  -v|vv|vvv, --verbose   Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

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\EventListener;
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

EventListener::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/EventListener.php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still a rather generic name

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 EventListener implements IEventListener
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class EventListener implements IEventListener
class CacheEventsListener implements IEventListener
Suggested change
class EventListener implements IEventListener
class PostUploadEventListener 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, EventListener::class);
$context->registerEventListener(CacheEntryUpdatedEvent::class, EventListener::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
79 changes: 57 additions & 22 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,50 @@ 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;
}

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;
}
}

public static function isFileTooLargeToScan($path) {
$size = filesize($path);
return !$size || $size > self::MAX_FILE_SIZE;
}

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 +155,27 @@ public function scan(string $filePath): VaasVerdict {
}
}

public function onRename(string $localSource, string $localTarget)
{
if ($localSource === $this->lastLocalPath) {
$this->lastLocalPath = $localTarget;
}
}

public function tagLastScannedFile(string $localPath, int $fileId) {
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
Loading