Issues (195)

lib/Monitor.php (17 issues)

1
<?php
2
3
/**
4
 * @copyright Copyright (c) 2017 Matthias Held <[email protected]>
5
 * @author Matthias Held <[email protected]>
6
 * @license GNU AGPL version 3 or any later version
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License as
10
 * published by the Free Software Foundation, either version 3 of the
11
 * License, or (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU Affero General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public License
19
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
20
 */
21
22
namespace OCA\RansomwareDetection;
23
24
use OCA\RansomwareDetection\AppInfo\Application;
25
use OCA\RansomwareDetection\Analyzer\EntropyAnalyzer;
26
use OCA\RansomwareDetection\Analyzer\EntropyResult;
27
use OCA\RansomwareDetection\Analyzer\FileCorruptionAnalyzer;
28
use OCA\RansomwareDetection\Analyzer\FileExtensionAnalyzer;
29
use OCA\RansomwareDetection\Analyzer\FileExtensionResult;
30
use OCA\RansomwareDetection\Db\FileOperation;
31
use OCA\RansomwareDetection\Db\FileOperationMapper;
32
use OCP\App\IAppManager;
0 ignored issues
show
The type OCP\App\IAppManager was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
33
use OCP\AppFramework\Utility\ITimeFactory;
0 ignored issues
show
The type OCP\AppFramework\Utility\ITimeFactory was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
34
use OCP\Files\File;
0 ignored issues
show
The type OCP\Files\File was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
35
use OCP\Files\Folder;
0 ignored issues
show
The type OCP\Files\Folder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
36
use OCP\Files\IRootFolder;
0 ignored issues
show
The type OCP\Files\IRootFolder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
37
use OCP\Files\NotFoundException;
0 ignored issues
show
The type OCP\Files\NotFoundException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
38
use OCP\Files\Storage\IStorage;
0 ignored issues
show
The type OCP\Files\Storage\IStorage was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
39
use OCP\Notification\IManager;
0 ignored issues
show
The type OCP\Notification\IManager was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
40
use OCP\IConfig;
0 ignored issues
show
The type OCP\IConfig was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
41
use OCP\ILogger;
0 ignored issues
show
The type OCP\ILogger was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
42
use OCP\IRequest;
0 ignored issues
show
The type OCP\IRequest was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
43
44
class Monitor
45
{
46
    /** File access
47
     * @var int
48
     */
49
    const DELETE = 1;
50
    const RENAME = 2;
51
    const WRITE = 3;
52
    const READ = 4;
53
    const CREATE = 5;
54
55
    /** @var IRequest */
56
    protected $request;
57
58
    /** @var IConfig */
59
    protected $config;
60
61
    /** @var ITimeFactory */
62
    protected $time;
63
64
    /** @var IAppManager */
65
    protected $appManager;
66
67
    /** @var ILogger */
68
    protected $logger;
69
70
    /** @var IRootFolder */
71
    protected $rootFolder;
72
73
    /** @var EntropyAnalyzer */
74
    protected $entropyAnalyzer;
75
76
    /** @var FileOperationMapper */
77
    protected $mapper;
78
79
    /** @var FileExtensionAnalyzer */
80
    protected $fileExtensionAnalyzer;
81
82
    /** @var FileCorruptionAnalyzer */
83
    protected $fileCorruptionAnalyzer;
84
85
    /** @var string */
86
    protected $userId;
87
88
    /** @var int */
89
    protected $nestingLevel = 0;
90
91
    /**
92
     * @param IRequest                  $request
93
     * @param IConfig                   $config
94
     * @param ITimeFactory              $time
95
     * @param IAppManager               $appManager
96
     * @param ILogger                   $logger
97
     * @param IRootFolder               $rootFolder
98
     * @param EntropyAnalyzer           $entropyAnalyzer
99
     * @param FileOperationMapper       $mapper
100
     * @param FileExtensionAnalyzer     $fileExtensionAnalyzer
101
     * @param FileCorruptionAnalyzer    $fileCorruptionAnalyzer
102
     * @param string                    $userId
103
     */
104
    public function __construct(
105
        IRequest $request,
106
        IConfig $config,
107
        ITimeFactory $time,
108
        IAppManager $appManager,
109
        ILogger $logger,
110
        IRootFolder $rootFolder,
111
        EntropyAnalyzer $entropyAnalyzer,
112
        FileOperationMapper $mapper,
113
        FileExtensionAnalyzer $fileExtensionAnalyzer,
114
        FileCorruptionAnalyzer $fileCorruptionAnalyzer,
115
        $userId
116
    ) {
117
        $this->request = $request;
118
        $this->config = $config;
119
        $this->time = $time;
120
        $this->appManager = $appManager;
121
        $this->logger = $logger;
122
        $this->rootFolder = $rootFolder;
123
        $this->entropyAnalyzer = $entropyAnalyzer;
124
        $this->mapper = $mapper;
125
        $this->fileExtensionAnalyzer = $fileExtensionAnalyzer;
126
        $this->fileCorruptionAnalyzer = $fileCorruptionAnalyzer;
127
        $this->userId = $userId;
128
    }
129
130
    /**
131
     * Analyze file.
132
     *
133
     * @param array    $paths
134
     * @param int      $mode
135
     */
136
    public function analyze($paths, $mode)
137
    {
138
        $path = $paths[0];
139
		
140
		if ($path === '') {
141
			$this->logger->debug("Path is empty.");
142
			return;
143
		}
144
145
        $storage = $this->rootFolder->getUserFolder($this->userId)->get(dirname($path))->getStorage();
0 ignored issues
show
The assignment to $storage is dead and can be removed.
Loading history...
146
        if ($this->userId === null || $this->nestingLevel !== 0 || /*!$this->isUploadedFile($storage, $path) ||*/ $this->isCreatingSkeletonFiles()) {
147
            // check only cloud files and no system files
148
            return;
149
        }
150
151
        if (!$this->request->isUserAgent([
152
            IRequest::USER_AGENT_CLIENT_DESKTOP,
153
            IRequest::USER_AGENT_CLIENT_ANDROID,
154
            IRequest::USER_AGENT_CLIENT_IOS,
155
        ])) {
156
            // not a sync client
157
            return;
158
        }
159
160
        $this->nestingLevel++;
161
162
        switch ($mode) {
163
            case self::RENAME:
164
                $path = $paths[1];
165
                $this->logger->debug("Rename ".$paths[0]." to ".$paths[1], ['app' =>  Application::APP_ID]);
166
                if (preg_match('/.+\.d[0-9]+/', pathinfo($paths[1])['basename']) > 0) {
167
                    return;
168
                }
169
                // reset PROPFIND_COUNT
170
                $this->resetProfindCount();
171
172
                try {
173
                    $userRoot = $this->rootFolder->getUserFolder($this->userId);
174
                    $node = $userRoot->get($path);
175
                } catch (\OCP\Files\NotFoundException $exception) {
176
                    $this->logger->error("File Not Found ".$path, ['app' =>  Application::APP_ID]);
177
                    return;
178
                }
179
180
                // not a file no need to analyze
181
                if (!($node instanceof File)) {
182
                    $this->addFolderOperation($paths, $node, self::RENAME);
183
                    $this->nestingLevel--;
184
185
                    return;
186
                }
187
188
                $this->addFileOperation($paths, $node, self::RENAME);
189
190
                $this->nestingLevel--;
191
192
                return;
193
            case self::WRITE:
194
                $this->logger->debug("Write ".$path, ['app' =>  Application::APP_ID]);
195
                // reset PROPFIND_COUNT
196
                $this->resetProfindCount();
197
198
                try {
199
                    $userRoot = $this->rootFolder->getUserFolder($this->userId);
200
                    $node = $userRoot->get($path);
201
                } catch (\OCP\Files\NotFoundException $exception) {
202
                    $this->logger->error("File Not Found ".$path, ['app' =>  Application::APP_ID]);
203
                    return;
204
                }
205
206
                // not a file no need to analyze
207
                if (!($node instanceof File)) {
208
                    $this->addFolderOperation($paths, $node, self::WRITE);
209
                    $this->nestingLevel--;
210
211
                    return;
212
                }
213
214
                $this->addFileOperation($paths, $node, self::WRITE);
215
216
                $this->nestingLevel--;
217
218
                return;
219
            case self::READ:
220
                $this->nestingLevel--;
221
222
                return;
223
            case self::DELETE:
224
                $this->logger->debug("Delete ".$path, ['app' =>  Application::APP_ID]);
225
                // reset PROPFIND_COUNT
226
                $this->resetProfindCount();
227
228
                try {
229
                    $userRoot = $this->rootFolder->getUserFolder($this->userId);
230
                    $node = $userRoot->get($path);
231
                } catch (\OCP\Files\NotFoundException $exception) {
232
                    $this->logger->error("File Not Found ".$path, ['app' =>  Application::APP_ID]);
233
                    return;
234
                }
235
236
                // not a file no need to analyze
237
                if (!($node instanceof File)) {
238
                    $this->addFolderOperation($paths, $node, self::DELETE);
239
                    $this->nestingLevel--;
240
241
                    return;
242
                }
243
244
                $this->addFileOperation($paths, $node, self::DELETE);
245
246
                $this->nestingLevel--;
247
248
                return;
249
            case self::CREATE:
250
                $this->logger->debug("Create ".$path, ['app' =>  Application::APP_ID]);
251
                // reset PROPFIND_COUNT
252
                $this->resetProfindCount();
253
254
                try {
255
                    $userRoot = $this->rootFolder->getUserFolder($this->userId);
256
                    $node = $userRoot->get($path);
257
                } catch (\OCP\Files\NotFoundException $exception) {
258
                    $this->logger->error("File Not Found ".$path, ['app' =>  Application::APP_ID]);
259
                    return;
260
                }
261
                if (!($node instanceof File)) {
262
263
                    $fileOperation = new FileOperation();
264
                    $fileOperation->setUserId($this->userId);
265
                    $fileOperation->setPath(str_replace('files', '', pathinfo($path)['dirname']));
266
                    $fileOperation->setOriginalName(pathinfo($path)['basename']);
267
                    $fileOperation->setType('folder');
268
                    $fileOperation->setMimeType('httpd/unix-directory');
269
                    $fileOperation->setSize(0);
270
                    $fileOperation->setTimestamp(time());
271
                    $fileOperation->setCorrupted(false);
272
                    $fileOperation->setCommand(self::CREATE);
273
                    $sequenceId = $this->config->getUserValue($this->userId, Application::APP_ID, 'sequence_id', 0);
274
                    $fileOperation->setSequence($sequenceId);
275
276
                    // entropy analysis
277
                    $fileOperation->setEntropy(0.0);
278
                    $fileOperation->setStandardDeviation(0.0);
279
                    $fileOperation->setFileClass(EntropyResult::NORMAL);
280
281
                    // file extension analysis
282
                    $fileOperation->setFileExtensionClass(FileExtensionResult::NOT_SUSPICIOUS);
283
284
                    $this->mapper->insert($fileOperation);
285
                    $this->nestingLevel--;
286
                } else {
287
                    $this->addFileOperation($paths, $node, self::CREATE);
288
289
                    $this->nestingLevel--;
290
                }
291
292
                return;
293
            default:
294
                $this->nestingLevel--;
295
296
                return;
297
        }
298
    }
299
300
    /**
301
     * Check if we are in the LoginController and if so, ignore the firewall.
302
     *
303
     * @return bool
304
     */
305
    protected function isCreatingSkeletonFiles()
306
    {
307
        $exception = new \Exception();
308
        $trace = $exception->getTrace();
309
        foreach ($trace as $step) {
310
            if (isset($step['class'], $step['function']) &&
311
                $step['class'] === 'OC\Core\Controller\LoginController' &&
312
                $step['function'] === 'tryLogin') {
313
                return true;
314
            }
315
        }
316
317
        return false;
318
    }
319
320
    /**
321
     * Reset PROPFIND_COUNT.
322
     */
323
    protected function resetProfindCount()
324
    {
325
        $userKeys = $this->config->getUserKeys($this->userId, Application::APP_ID);
326
        foreach ($userKeys as $key) {
327
            if (strpos($key, 'propfind_count') !== false) {
328
                $this->config->deleteUserValue($this->userId, Application::APP_ID, $key);
329
            }
330
        }
331
    }
332
333
    /**
334
     * Return file size of a path.
335
     *
336
     * @param string $path
337
     *
338
     * @return int
339
     */
340
    private function getFileSize($path)
0 ignored issues
show
The method getFileSize() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
341
    {
342
        if (strpos($path, 'files_trashbin') !== false) {
343
            $node = $this->rootFolder->get($path);
344
345
            if (!($node instanceof File)) {
346
                throw new NotFoundException();
347
            }
348
349
            return $node->getSize();
350
        } else {
351
            $userRoot = $this->rootFolder->getUserFolder($this->userId)->getParent();
352
            $node = $userRoot->get($path);
353
354
            if (!($node instanceof File)) {
355
                throw new NotFoundException();
356
            }
357
358
            return $node->getSize();
359
        }
360
    }
361
362
    /**
363
     * Check if file is a uploaded file.
364
     *
365
     * @param IStorage $storage
366
     * @param string   $path
367
     *
368
     * @return bool
369
     */
370
    private function isUploadedFile(IStorage $storage, $path)
0 ignored issues
show
The method isUploadedFile() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
371
    {
372
        $fullPath = $path;
373
        if (property_exists($storage, 'mountPoint')) {
374
            /* @var StorageWrapper $storage */
375
            try {
376
                $fullPath = $storage->mountPoint.$path;
377
            } catch (\Exception $ex) {
0 ignored issues
show
catch (\Exception $ex) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
378
                return true;
379
            }
380
        }
381
382
        // ignore transfer files
383
        if (strpos($fullPath, 'ocTransferId') > 0) {
384
            return false;
385
        }
386
387
        if (substr_count($fullPath, '/') < 3) {
388
            return false;
389
        }
390
391
        // '', admin, 'files', 'path/to/file.txt'
392
        $segment = explode('/', $fullPath, 4);
393
394
        return isset($segment[2]) && in_array($segment[2], [
395
            'files',
396
            'thumbnails',
397
            'files_versions',
398
        ], true);
399
    }
400
401
    /**
402
     * Add a folder to the operations.
403
     *
404
     * @param array $paths
405
     * @param INode $node
0 ignored issues
show
The type OCA\RansomwareDetection\INode was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
406
     * @param int   $operation
407
     */
408
    private function addFolderOperation($paths, $node, $operation)
409
    {
410
        $this->logger->debug("Add folder operation.", ['app' =>  Application::APP_ID]);
411
        $fileOperation = new FileOperation();
412
        $fileOperation->setUserId($this->userId);
413
        $fileOperation->setPath(str_replace('files', '', pathinfo($node->getInternalPath())['dirname']));
414
        $fileOperation->setOriginalName($node->getName());
415
        if ($operation === self::RENAME) {
416
            $fileOperation->setNewName(pathinfo($paths[1])['basename']);
417
        }
418
        $fileOperation->setType('folder');
419
        $fileOperation->setMimeType($node->getMimeType());
420
        $fileOperation->setSize(0);
421
        $fileOperation->setTimestamp(time());
422
        $fileOperation->setCorrupted(false);
423
        $fileOperation->setCommand($operation);
424
        $sequenceId = $this->config->getUserValue($this->userId, Application::APP_ID, 'sequence_id', 0);
425
        $fileOperation->setSequence($sequenceId);
426
427
        // entropy analysis
428
        $fileOperation->setEntropy(0.0);
429
        $fileOperation->setStandardDeviation(0.0);
430
        $fileOperation->setFileClass(EntropyResult::NORMAL);
431
432
        // file extension analysis
433
        $fileOperation->setFileExtensionClass(FileExtensionResult::NOT_SUSPICIOUS);
434
435
        $this->mapper->insert($fileOperation);
436
    }
437
438
    /**
439
     * Add a file to the operations.
440
     *
441
     * @param array $paths
442
     * @param INode $node
443
     * @param int   $operation
444
     */
445
    private function addFileOperation($paths, $node, $operation)
446
    {
447
        $this->logger->debug("Add file operation.", ['app' =>  Application::APP_ID]);
448
        $fileOperation = new FileOperation();
449
        $fileOperation->setUserId($this->userId);
450
        $fileOperation->setPath(str_replace('files', '', pathinfo($node->getInternalPath())['dirname']));
451
        $fileOperation->setOriginalName($node->getName());
452
        if ($operation === self::RENAME) {
453
            $fileOperation->setNewName(pathinfo($paths[1])['basename']);
454
        }
455
        $fileOperation->setType('file');
456
        $fileOperation->setMimeType($node->getMimeType());
457
        $fileOperation->setSize($node->getSize());
458
        $fileOperation->setTimestamp(time());
459
        $fileOperation->setCommand($operation);
460
        $sequenceId = $this->config->getUserValue($this->userId, Application::APP_ID, 'sequence_id', 0);
461
        $fileOperation->setSequence($sequenceId);
462
463
        // file extension analysis
464
        $fileExtensionResult = $this->fileExtensionAnalyzer->analyze($node->getInternalPath());
465
        $fileOperation->setFileExtensionClass($fileExtensionResult->getFileExtensionClass());
466
467
        $fileCorruptionResult = $this->fileCorruptionAnalyzer->analyze($node);
468
        $isCorrupted = $fileCorruptionResult->isCorrupted();
469
        $fileOperation->setCorrupted($isCorrupted);
470
        if ($isCorrupted) {
471
            $fileOperation->setFileExtensionClass(FileExtensionResult::SUSPICIOUS);
472
        }
473
474
        // entropy analysis
475
        $entropyResult = $this->entropyAnalyzer->analyze($node);
476
        $fileOperation->setEntropy($entropyResult->getEntropy());
477
        $fileOperation->setStandardDeviation($entropyResult->getStandardDeviation());
478
        $fileOperation->setFileClass($entropyResult->getFileClass());
479
480
        $entity = $this->mapper->insert($fileOperation);
0 ignored issues
show
The assignment to $entity is dead and can be removed.
Loading history...
481
    }
482
}
483