Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/ransomware_protection.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorJoas Schilling <coding@schilljs.com>2017-07-28 16:21:18 +0300
committerJoas Schilling <coding@schilljs.com>2017-07-28 16:21:18 +0300
commit882289a7cefd26bf9ef5317b37b13c2ef122a4ce (patch)
treeec934d8136bad854ab4462ffa951ffdb6091e62f /lib
parentb3f6112806259637f87eca5be317752e243f26de (diff)
Split analyze and striking
Signed-off-by: Joas Schilling <coding@schilljs.com>
Diffstat (limited to 'lib')
-rw-r--r--lib/Analyzer.php44
-rw-r--r--lib/Striker.php166
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',
+ ]
+ );
+ }
+}