diff options
author | Joas Schilling <coding@schilljs.com> | 2017-07-28 16:21:18 +0300 |
---|---|---|
committer | Joas Schilling <coding@schilljs.com> | 2017-07-28 16:21:18 +0300 |
commit | 882289a7cefd26bf9ef5317b37b13c2ef122a4ce (patch) | |
tree | ec934d8136bad854ab4462ffa951ffdb6091e62f /lib | |
parent | b3f6112806259637f87eca5be317752e243f26de (diff) |
Split analyze and striking
Signed-off-by: Joas Schilling <coding@schilljs.com>
Diffstat (limited to 'lib')
-rw-r--r-- | lib/Analyzer.php | 44 | ||||
-rw-r--r-- | lib/Striker.php | 166 |
2 files changed, 197 insertions, 13 deletions
diff --git a/lib/Analyzer.php b/lib/Analyzer.php index fabc279..b9f9607 100644 --- a/lib/Analyzer.php +++ b/lib/Analyzer.php @@ -27,6 +27,7 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\Files\ForbiddenException; use OCP\Files\Storage\IStorage; use OCP\IConfig; +use OCP\ILogger; class Analyzer { @@ -56,6 +57,15 @@ class Analyzer { /** @var IAppManager */ protected $appManager; + /** @var ILogger */ + protected $logger; + + /** @var Striker */ + protected $striker; + + /** @var string */ + protected $userId; + /** @var int */ protected $nestingLevel = 0; @@ -63,11 +73,17 @@ class Analyzer { * @param IConfig $config * @param ITimeFactory $time * @param IAppManager $appManager + * @param ILogger $logger + * @param Striker $striker + * @param string $userId */ - public function __construct(IConfig $config, ITimeFactory $time, IAppManager $appManager) { + public function __construct(IConfig $config, ITimeFactory $time, IAppManager $appManager, ILogger $logger, Striker $striker, $userId) { $this->config = $config; $this->time = $time; $this->appManager = $appManager; + $this->logger = $logger; + $this->striker = $striker; + $this->userId = $userId; } protected function parseResources() { @@ -126,12 +142,12 @@ class Analyzer { * @throws ForbiddenException */ public function checkPath(IStorage $storage, $path) { - if ($this->nestingLevel !== 0 || !$this->isBlockablePath($storage, $path) || $this->isCreatingSkeletonFiles()) { + if ($this->userId === null || $this->nestingLevel !== 0 || !$this->isBlockablePath($storage, $path) || $this->isCreatingSkeletonFiles()) { // Allow creating skeletons and theming return; } - if ($this->config->getUserValue(\OC_User::getUser(), 'ransomware_protection', 'disabled_until', 0) < $this->time->getTime()) { + if ($this->config->getUserValue($this->userId, 'ransomware_protection', 'disabled_until', 0) < $this->time->getTime()) { // Protection is currently disabled for the user return; } @@ -141,10 +157,10 @@ class Analyzer { $filePath = $this->translatePath($storage, $path); $fileName = basename($filePath); - $this->checkExtension($fileName, $this->extensionsPlain, $this->extensionsRegex, $this->extensionsPlainLength); - $this->checkNotes($fileName, $this->notesPlain, $this->notesRegex); + $this->checkExtension($fileName, $filePath, $this->extensionsPlain, $this->extensionsRegex, $this->extensionsPlainLength); + $this->checkNotes($fileName, $filePath, $this->notesPlain, $this->notesRegex); if ($this->config->getAppValue('ransomware_protection', 'check-all', 'no') === 'yes') { - $this->checkNotes($fileName, $this->notesBiasedPlain, $this->notesBiasedRegex); + $this->checkNotes($fileName, $filePath, $this->notesBiasedPlain, $this->notesBiasedRegex); } } @@ -152,25 +168,26 @@ class Analyzer { * Check if a file name matches the prefix/extension * * @param string $name + * @param string $path * @param string[] $plain * @param string[] $regex * @param int[] $plainLengths * @throws ForbiddenException */ - protected function checkExtension($name, array $plain, array $regex, array $plainLengths) { + protected function checkExtension($name, $path, array $plain, array $regex, array $plainLengths) { foreach ($plain as $ext) { if (strpos($ext, '.') === 0 || strpos($ext, '_') === 0) { if (isset($plainLengths[$ext]) && substr($name, $plainLengths[$ext]) === $ext) { - throw new ForbiddenException('Ransomware file detected', true); + $this->striker->handleMatch('extension', $path, $ext); } } else if (strpos($name, $ext) !== false) { - throw new ForbiddenException('Ransomware file detected', true); + $this->striker->handleMatch('extension', $path, $ext); } } foreach ($regex as $ext) { if (preg_match('/' . $ext . '/', $name) === 1) { - throw new ForbiddenException('Ransomware file detected', true); + $this->striker->handleMatch('extension', $path, $ext); } } } @@ -179,20 +196,21 @@ class Analyzer { * Check if a file name matches the info/notes file * * @param string $name + * @param string $path * @param string[] $plain * @param string[] $regex * @throws ForbiddenException */ - protected function checkNotes($name, array $plain, array $regex) { + protected function checkNotes($name, $path, array $plain, array $regex) { foreach ($plain as $note) { if ($name === $note) { - throw new ForbiddenException('Ransomware file detected', true); + $this->striker->handleMatch('note file', $path, $note); } } foreach ($regex as $note) { if (preg_match('/' . $note . '/', $name) === 1) { - throw new ForbiddenException('Ransomware file detected', true); + $this->striker->handleMatch('note file', $path, $note); } } } diff --git a/lib/Striker.php b/lib/Striker.php new file mode 100644 index 0000000..146f971 --- /dev/null +++ b/lib/Striker.php @@ -0,0 +1,166 @@ +<?php +/** + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\RansomwareProtection; + + +use OCP\App\IAppManager; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\ForbiddenException; +use OCP\Files\Storage\IStorage; +use OCP\IConfig; +use OCP\ILogger; + +class Striker { + + const FIRST_STRIKE = 1; + const ALREADY_STRIKED = 2; + const FIFTH_STRIKE = 3; + + /** @var IConfig */ + protected $config; + + /** @var ITimeFactory */ + protected $time; + + /** @var ILogger */ + protected $logger; + + /** @var string */ + protected $userId; + + /** + * @param IConfig $config + * @param ITimeFactory $time + * @param ILogger $logger + * @param string $userId + */ + public function __construct(IConfig $config, ITimeFactory $time, ILogger $logger, $userId) { + $this->config = $config; + $this->time = $time; + $this->logger = $logger; + $this->userId = $userId; + } + + /** + * @param string $case + * @param string $path + * @param string $pattern + * @throws ForbiddenException + */ + public function handleMatch($case, $path, $pattern) { + + $lastStrikes = $this->config->getUserValue($this->userId, 'ransomware_protection', 'last_strikes', '[]'); + $lastStrikes = json_decode($lastStrikes, true); + + $strikeType = $this->checkLastStrikes($lastStrikes, $path); + + if ($strikeType === self::ALREADY_STRIKED) { + $this->addRestrikeLog($case, $path, $pattern); + } else { + $this->addStrikeLog($case, $path, $pattern); + + $this->updateLastStrikes($lastStrikes, [ + 'path' => $path, + 'time' => $this->time->getTime(), + ]); + } + + if ($strikeType === self::FIFTH_STRIKE) { + // Block the user for 1 hour + $this->config->setUserValue($this->userId, 'ransomware_protection', 'client_blocked', '[]'); + } + + throw new ForbiddenException('Ransomware file detected', true); + } + + /** + * @param array $lastStrikes + * @param string $path + * @return int + */ + protected function checkLastStrikes(array $lastStrikes, $path) { + $thirtyMinutesAgo = $this->time->getTime() - 30 * 60; + + $recentStrikes = 0; + foreach ($lastStrikes as $strike) { + if ($strike['path'] === $path && $strike['time'] > $thirtyMinutesAgo) { + return self::ALREADY_STRIKED; + } + if ($strike['time'] > $thirtyMinutesAgo) { + $recentStrikes++; + } + } + + return $recentStrikes > 5 ? self::FIFTH_STRIKE : self::FIRST_STRIKE; + } + + /** + * @param array $lastStrikes + * @param array $newStrike + */ + protected function updateLastStrikes(array $lastStrikes, $newStrike) { + $thirtyMinutesAgo = $this->time->getTime() - 30 * 60; + + $lastStrikes = array_filter($lastStrikes, function($strike) use ($thirtyMinutesAgo) { + return $strike['time'] <= $thirtyMinutesAgo; + }); + + array_unshift($lastStrikes, $newStrike); + + $this->config->setUserValue($this->userId, 'ransomware_protection', 'last_strikes', json_encode($lastStrikes)); + } + + /** + * @param string $case + * @param string $path + * @param string $pattern + */ + protected function addStrikeLog($case, $path, $pattern) { + $this->logger->warning( + 'Prevented upload of {path} because it matches {case} pattern "{pattern}"', + [ + 'case' => $case, + 'path' => $path, + 'pattern' => $pattern, + 'app' => 'ransomware_protection', + ] + ); + } + + /** + * @param string $case + * @param string $path + * @param string $pattern + * @throws ForbiddenException + */ + protected function addRestrikeLog($case, $path, $pattern) { + $this->logger->info( + 'Prevented repeated upload of {path} because it matches {case} pattern "{pattern}"', + [ + 'case' => $case, + 'path' => $path, + 'pattern' => $pattern, + 'app' => 'ransomware_protection', + ] + ); + } +} |