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

github.com/undo-ransomware/ransomware_detection.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md12
-rw-r--r--appinfo/routes.php22
-rw-r--r--css/style.scss12
-rw-r--r--js/app.js33
-rw-r--r--js/scan.js522
-rw-r--r--lib/Analyzer/EntropyAnalyzer.php1
-rw-r--r--lib/AppInfo/Application.php17
-rw-r--r--lib/Controller/BasicController.php107
-rw-r--r--lib/Controller/MonitoringController.php324
-rw-r--r--lib/Controller/RecoverController.php13
-rw-r--r--lib/Controller/ScanController.php416
-rw-r--r--lib/Scanner/StorageStructure.php97
-rw-r--r--templates/index.php18
-rw-r--r--templates/scan.php52
-rw-r--r--tests/Unit/Controller/BasicControllerTest.php101
-rw-r--r--tests/Unit/Controller/MonitoringControllerTest.php341
-rw-r--r--tests/Unit/Controller/ScanControllerTest.php274
-rw-r--r--tests/Unit/Scanner/StorageStructureTest.php72
18 files changed, 2405 insertions, 29 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a28d5c7..815aebf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,18 +1,6 @@
# Changelog
All notable changes to this project will be documented in this file.
-## 0.2.4
-
-### Added
-
-- Added Travis CI.
-
-## 0.2.3
-
-### Fixed
-
-- Fix CSRF settings.
-
## 0.2.2
### Added
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 3914b5a..8aa2fce 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -11,15 +11,21 @@
return [
'routes' => [
['name' => 'recover#index', 'url' => '/', 'verb' => 'GET'],
+ ['name' => 'recover#scan', 'url' => '/scan', 'verb' => 'GET'],
],
'ocs' => [
- ['name' => 'api#listFileOperations', 'url' => '/api/{apiVersion}/list', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
- ['name' => 'api#export', 'url' => '/api/{apiVersion}/export', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
- ['name' => 'api#deleteSequence', 'url' => '/api/{apiVersion}/delete-sequence/{sequence}', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
- ['name' => 'api#recover', 'url' => '/api/{apiVersion}/recover', 'verb' => 'POST', 'requirements' => ['apiVersion' => 'v1']],
- ['name' => 'api#changeColorMode', 'url' => '/api/{apiVersion}/change-color-mode/{colorMode}', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
- ['name' => 'api#getColorMode', 'url' => '/api/{apiVersion}/get-color-mode', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
- ['name' => 'api#getDebugMode', 'url' => '/api/{apiVersion}/get-debug-mode', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
- ['name' => 'analyzer#analyze', 'url' => '/analyzer/{apiVersion}/analyze/{operationId}/{userId}', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
+ // Basic controller
+ ['name' => 'basic#changeColorMode', 'url' => '/api/{apiVersion}/change-color-mode/{colorMode}', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
+ ['name' => 'basic#getColorMode', 'url' => '/api/{apiVersion}/get-color-mode', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
+ ['name' => 'basic#getDebugMode', 'url' => '/api/{apiVersion}/get-debug-mode', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
+ // Monitoring controller
+ ['name' => 'monitoring#listFileOperations', 'url' => '/api/{apiVersion}/list', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
+ ['name' => 'monitoring#export', 'url' => '/api/{apiVersion}/export', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
+ ['name' => 'monitoring#deleteSequence', 'url' => '/api/{apiVersion}/delete-sequence/{sequence}', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
+ ['name' => 'monitoring#recover', 'url' => '/api/{apiVersion}/recover', 'verb' => 'POST', 'requirements' => ['apiVersion' => 'v1']],
+ // Scan controller
+ ['name' => 'scan#recover', 'url' => '/api/{apiVersion}/scan-recover', 'verb' => 'POST', 'requirements' => ['apiVersion' => 'v1']],
+ ['name' => 'scan#filesToScan', 'url' => '/api/{apiVersion}/files-to-scan', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
+ ['name' => 'scan#scanSequence', 'url' => '/api/{apiVersion}/scan-sequence', 'verb' => 'POST', 'requirements' => ['apiVersion' => 'v1']],
],
];
diff --git a/css/style.scss b/css/style.scss
index 404eef4..4fb97d5 100644
--- a/css/style.scss
+++ b/css/style.scss
@@ -64,6 +64,10 @@
color: #2A2A2A;
}
+.scan-header span {
+ font-weight: bold;
+}
+
.file-list {
tr {
&:hover {
@@ -259,7 +263,7 @@ table {
background-color: rgba(11, 85, 159, 1);
}
&.selected {
- background-color: rgba(153, 153, 153, .1) !important;
+ background-color: rgba(153, 153, 153, .1) !important;
}
}
}
@@ -346,3 +350,9 @@ table {
.suspicion-level-3 {
background-color: #FF6347;
}
+
+.disabled {
+ pointer-events: none;
+ cursor: default !important;
+ opacity: 0.6;
+}
diff --git a/js/app.js b/js/app.js
index a72064d..f32eba2 100644
--- a/js/app.js
+++ b/js/app.js
@@ -40,15 +40,30 @@
* @member {OCA.RansomwareDetection.FileList}
*/
fileList: null,
+ /**
+ * Scan for the "Ransomware detection" section
+ *
+ * @member {OCA.RansomwareDetection.Scan}
+ */
+ scan: null,
/**
* Initializes the ransomware detection app
*/
initialize: function() {
- this.fileList = new OCA.RansomwareDetection.FileList(
- $('#app-content-ransomware-detection'), {}
- );
- window.FileList = this.fileList;
+ if (typeof OCA.RansomwareDetection.FileList != 'undefined') {
+ this.fileList = new OCA.RansomwareDetection.FileList(
+ $('#app-content-ransomware-detection-filelist'), {}
+ );
+ window.FileList = this.fileList;
+ }
+
+ if (typeof OCA.RansomwareDetection.Scan != 'undefined') {
+ this.scan = new OCA.RansomwareDetection.Scan(
+ $('#app-content-ransomware-detection-scan'), {}
+ );
+ window.Scan = this.scan;
+ }
OC.Plugins.attach('OCA.RansomwareDetection.App', this);
},
@@ -57,8 +72,14 @@
* Destroy the app
*/
destroy: function() {
- this.fileList.destroy();
- this.fileList = null;
+ if (typeof OCA.RansomwareDetection.Scan != 'undefined') {
+ this.fileList.destroy();
+ this.fileList = null;
+ }
+ if (typeof OCA.RansomwareDetection.Scan != 'undefined') {
+ this.scan.destroy();
+ this.scan = null;
+ }
}
}
})();
diff --git a/js/scan.js b/js/scan.js
new file mode 100644
index 0000000..e339bc4
--- /dev/null
+++ b/js/scan.js
@@ -0,0 +1,522 @@
+/**
+ * @copyright Copyright (c) 2018 Matthias Held <matthias.held@uni-konstanz.de>
+ *
+ * @author Matthias Held <matthias.held@uni-konstanz.de>
+ *
+ * @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 <https://www.gnu.org/licenses/>.
+ */
+
+(function() {
+
+ /**
+ * @class OCA.RansomwareDetection.Scan
+ */
+ var Scan = function($el, options) {
+ this.initialize($el, options);
+ };
+
+ /**
+ * @memberof OCA.RansomwareDetection
+ */
+ Scan.prototype = {
+ id: 'ransomware_detection',
+ appName: t('ransomware_detection', 'Ransomware Detection'),
+ $el: null,
+ $section: null,
+ $table: null,
+ $fileList: null,
+ debug: 0,
+ filesToScan: {},
+ sequencesToScan: {},
+ colors: {red: 'red', orange: 'orange', yellow: 'yellow', green: 'green'},
+ colorsText: {red: 'red-text', orange: 'orange-text', yellow: 'yellow-text', green: 'green-text'},
+
+ /**
+ * Map of file id to file data
+ * @type Object.<int, Object>
+ */
+ _selectedFiles: {},
+
+ /**
+ * Map of files in the current folder.
+ * The entries are of file data.
+ *
+ * @type Object.<int, Object>
+ */
+ files: {},
+
+ /**
+ * Initialize the file list and its components
+ */
+ initialize: function($el, options) {
+ var self = this;
+ options = options || {};
+ if (this.initialized) {
+ return;
+ }
+ this.$el = $el;
+ if (options.id) {
+ this.id = options.id;
+ }
+
+ this.filesUrl = '/ocs/v2.php/apps/' + this.id + '/api/v1/files-to-scan';
+ this.recoveryUrl = '/ocs/v2.php/apps/' + this.id + '/api/v1/scan-recover';
+ this.scanSequenceUrl = '/ocs/v2.php/apps/' + this.id + '/api/v1/scan-sequence';
+ this.getColorModeUrl = '/ocs/v2.php/apps/' + this.id + '/api/v1/get-color-mode';
+ this.getDebugModeUrl = '/ocs/v2.php/apps/' + this.id + '/api/v1/get-debug-mode';
+ this.$container = options.scrollContainer || $(window);
+ this.$section = {};
+ this.$table = {};
+ this.$fileList = {};
+
+ $.getJSON(self.getDebugModeUrl, function(debug) {
+ if (debug.debug_mode == 1) {
+ console.log('Debug mode active.');
+ self.debug = 1;
+ }
+ $.getJSON(self.filesUrl, function(data) {
+ console.log("Create scan header.");
+ $('#section-loading').remove();
+ self.$el.append(self._createScanHeader(data.sequences.length));
+ self.sequencesToScan = data.sequences;
+ });
+ });
+
+ this.$el.on('click', '.start-scan', _.bind(this._onClickStartScan, this));
+ this.$el.on('change', 'td.selection>.selectCheckBox', _.bind(this._onClickFileCheckbox, this));
+ this.$el.on('click', '.select-all', _.bind(this._onClickSelectAll, this));
+ this.$el.on('click', '.recover-selected', _.bind(this._onClickRecover, this));
+ },
+
+ /**
+ * Destroy this instance
+ */
+ destroy: function() {
+ OC.Plugins.detach('OCA.RansomwareDetection.FileList', this);
+ },
+
+ /**
+ * Event handler for when selecting/deselecting all files
+ */
+ _onClickSelectAll: function(e) {
+ var self = this;
+
+ var checked = $(e.target).prop('checked');
+ console.log("Sequence: " + $(e.target).data('sequence'));
+ this.$fileList[$(e.target).data('sequence')].find('td.selection>.selectCheckBox').prop('checked', checked)
+ .closest('tr').toggleClass('selected', checked);
+ this._selectedFiles = {};
+ if (checked) {
+ console.log("Target is checked.");
+ Object.keys(this.files[$(e.target).data('sequence')]).forEach(function(key) {
+ console.log("Add " + key + " to selected files.");
+ var fileData = self.files[$(e.target).data('sequence')][key];
+ self._selectedFiles[fileData.id] = fileData;
+ });
+ }
+ this.updateSelectionSummary($(e.target).data('sequence'));
+ },
+
+ /**
+ * Event handler for when clicking on a file's checkbox
+ */
+ _onClickFileCheckbox: function(e) {
+ console.log('File selected.');
+ var $tr = $(e.target).closest('tr');
+ var state = !$tr.hasClass('selected');
+ var fileData = this.files[$tr.data('sequence')][$tr.data('id')];
+ if (state) {
+ $tr.addClass('selected');
+ this._selectedFiles[fileData.id] = fileData;
+ } else {
+ $tr.removeClass('selected');
+ delete this._selectedFiles[fileData.id];
+ }
+ this.updateSelectionSummary($tr.data('sequence'));
+ },
+
+ /**
+ * Create the App header.
+ */
+ _createScanHeader: function(numberOfSequences) {
+ if (this.debug == 1) {
+ header = $('<div class="section"><div class="pull-right"><span><a class="action" href="/ocs/v2.php/apps/ransomware_detection/api/v1/export"><span class="icon icon-download"></span>' + t('ransomware_detection', 'Export data') + '</a></span></div></div>');
+ } else {
+ header = $('<div class="section scan-header"><a href="#" class="button start-scan primary" data-original-title="" title=""><span>Start scan</span></a><div class="pull-right"><span>Sequences scanned: </span><span id="scanned">0</span>/<span id="total-files">' + numberOfSequences + '</span></div>')
+ }
+ return header;
+ },
+
+ /**
+ * Event handler to recover files
+ */
+ _onClickRecover: function(e) {
+ var self = this;
+
+ var numberOfFiles = Object.keys(self._selectedFiles).length;
+ var sequence = $(e.target).parent().data('sequence');
+
+ OC.dialogs.confirm(t('ransomware_detection', 'Are your sure you want to recover the selected files?'), t('ransomware_detection', 'Confirmation'), function (e) {
+ if (e === true) {
+ $.each(self._selectedFiles, function(index, value) {
+ $.ajax({
+ url: self.recoveryUrl,
+ type: 'POST',
+ contentType: 'application/json',
+ data: JSON.stringify({command: parseInt(value.command), path: value.path, timestamp: value.timestamp})
+ }).done(function(response) {
+ console.log("Recovery was a success.");
+ self.$el.find("tr[data-id='" + response['id'] + "']").remove();
+ numberOfFiles = numberOfFiles - 1;
+ delete self._selectedFiles[index];
+ if (numberOfFiles === 0) {
+ self.$section[sequence].remove();
+ delete self.$section[sequence];
+ if (Object.keys(self._selectedFiles).length === 0) {
+ self.$el.append(self._createAllFilesRecovered);
+ }
+ }
+ self.updateSelectionSummary(sequence);
+ }).fail(function(response, code) {
+ console.log("Recovery failed.");
+ });
+ });
+ }
+ });
+ },
+
+ /**
+ * On click listener for start scan.
+ */
+ _onClickStartScan: function(e) {
+ var self = this;
+
+ self.$el.find('#scan-results').parent().parent().remove();
+ self.$el.find('#section-suspicious-files-text').remove();
+ self.$el.find(".start-scan span").text("Scan running...");
+ self.$el.find(".start-scan").addClass("disabled");
+ self.$el.append(self._createNoSuspiciousFilesFound());
+
+
+ if (self.sequencesToScan.length > 0) {
+ var count = 0;
+ $.getJSON(self.getColorModeUrl, function(schema) {
+ if (schema.color_mode == 1) {
+ console.log('Color blind mode active.');
+ self.colors = {red: 'color-blind-red', orange: 'color-blind-orange', yellow: 'color-blind-yellow', green: 'color-blind-green'};
+ self.colorsText = {red: 'color-blind-red-text', orange: 'color-blind-orange', yellow: 'color-blind-yellow-text', green: 'color-blind-green-text'};
+ }
+ $.each(self.sequencesToScan, function(index, sequence) {
+ $.ajax({
+ url: self.scanSequenceUrl,
+ type: 'POST',
+ contentType: 'application/json',
+ data: JSON.stringify({sequence: sequence})
+ }).done(function(response) {
+ count = count + 1;
+ $('#scanned').text(count);
+ self.$section[index] = self._createSection(index);
+ self.$table[index] = self._createTableSkeleton(index, response.suspicion_score);
+ self.$fileList[index] = self.$table[index].find('tbody.file-list');
+ self.files[index] = [];
+ $.each(response.sequence, function(i, file) {
+ self.files[index][file.id] = file;
+ self.$fileList[index].append(self._createFileRow(file, index));
+ self.$el.find('#section-suspicious-files-text').remove();
+ self.$el.find('#scan-results').show();
+ });
+ self.$section[index].append(self.$table[index]);
+ self.$el.append(self.$section[index]);
+ self.updateSelectionSummary(index);
+ }).fail(function(response, code) {
+ console.log("Scan failed.");
+ count = count + 1;
+ $('#scanned').text(count);
+ }).always(function() {
+ if (count >= self.sequencesToScan.length) {
+ self.$el.find(".start-scan span").text("Scan finished");
+ }
+ });
+ });
+ });
+ } else {
+ console.log("No files to scan.");
+ }
+ },
+
+ /**
+ * Creates the section.
+ */
+ _createSection: function() {
+ var section = $('<div class="section group" id="section-results"></div>');
+ return section;
+ },
+
+ /**
+ * Creates the section.
+ */
+ _createSection: function(sequence) {
+ var section = $('<div class="section group" data-sequence="' + sequence + '"></div>');
+ return section;
+ },
+
+ /**
+ * All files recovered text.
+ */
+ _createAllFilesRecovered: function() {
+ var text = $('<div class="section"><h2>' + t('ransomware_detection', 'All files successfully recovered.') + '</h2></div>');
+ return text;
+ },
+
+ /**
+ * No suspicious files found text.
+ */
+ _createNoSuspiciousFilesFound: function() {
+ var text = $('<div class="section" id="section-suspicious-files-text"><h2>' + t('ransomware_detection', 'No suspicious files found.') + '</h2></div>');
+ return text;
+ },
+
+ /**
+ * Creates a new table skeleton.
+ */
+ _createTableSkeleton: function(sequence, suspicionScore) {
+ var color = this.colors.green;
+ if (suspicionScore >= 6) {
+ color = this.colors.red;
+ } else if (suspicionScore >= 5) {
+ color = this.colors.orange;
+ } else if (suspicionScore >= 3) {
+ color = this.colors.yellow;
+ }
+ var table =
+ $('<div class="row">' +
+ '<div class="sequence-color"><div class="color-box ' + color + '"></div></div>' +
+ '<div class="sequence-table"><table class="ransomware-files" data-sequence="' + sequence + '"><thead>' +
+ '<th><input type="checkbox" data-sequence="' + sequence + '" id="select_all_files_' + sequence + '" class="select-all checkbox"/>' +
+ '<label for="select_all_files_' + sequence + '"><span class="hidden-visually">' + t('ransomware_detection', 'Select all') + '</span></label></th>' +
+ '<th><a class="column-title name">' + t('ransomware_detection', 'Name') + '</a></th>' +
+ '<th><a class="column-title hide-selected"><p>' + t('ransomware_detection', 'Operation') + '</p></a></th>' +
+ '<th><a class="column-title hide-selected"><p>' + t('ransomware_detection', 'Size') + '</p></a></th>' +
+ '<th><a class="column-title hide-selected"><p>' + t('ransomware_detection', 'File class') + '</p></a></th>' +
+ '<th><a class="column-title hide-selected"><p>' + t('ransomware_detection', 'File name class') + '</p></a></th>' +
+ '<th class="controls"><a class="column-title detected">' + t('ransomware_detection', 'Time') + '</a><span class="column-title selected-actions"><a class="recover-selected" data-sequence="' + sequence + '"><span class="icon icon-history"></span><span>' + t('ransomware_detection', 'Recover') + '</span></a></span></th> ' +
+ '</thead><tbody class="file-list"></tbody><tfoot></tfoot></table></div>' +
+ '</div>');
+ return table;
+ },
+
+ /**
+ * Creates a new row in the table.
+ */
+ _createFileRow: function(fileData, sequence) {
+ var self = this;
+ var td, tr = $('<tr data-id="' + fileData.id + '" data-sequence="' + sequence + '"></tr>'),
+ mtime = parseInt(fileData.timestamp, 10) * 1000,
+ basename, extension, simpleSize, sizeColor;
+
+ if (isNaN(mtime)) {
+ mtime = new Date().getTime();
+ }
+
+ // size
+ if (typeof(fileData.size) !== 'undefined' && fileData.size >= 0) {
+ simpleSize = humanFileSize(parseInt(fileData.size, 10), true);
+ sizeColor = Math.round(160-Math.pow((fileData.size/(1024*1024)),2));
+ } else {
+ simpleSize = t('ransomware_detection', 'Pending');
+ }
+
+ td = $('<td></td>').attr({ "class": "selection"});
+ td.append('<input id="select-' + this.id + '-' + fileData.id +
+ '" type="checkbox" class="selectCheckBox checkbox"/>' +
+ '<label for="select-' + this.id + '-' + fileData.id + '">' +
+ '<div class="thumbnail" style="background-image:url(' + OC.MimeType.getIconUrl(fileData.type) + '); background-size: 32px;"></div>' +
+ '<span class="hidden-visually">' + t('ransomware_detection', 'Select') + '</span>' +
+ '</label>');
+ tr.append(td);
+
+ // file name
+ filename = fileData.originalName;
+ if (fileData.command === 2) {
+ filename = fileData.newName
+ }
+
+ if (filename !== null) {
+ if (filename.indexOf('.') === 0) {
+ basename = '';
+ extension = name;
+ } else {
+ basename = filename.substr(0, filename.lastIndexOf('.'));
+ extension = filename.substr(filename.lastIndexOf('.'));
+ }
+
+ var nameSpan = $('<span></span>').addClass('name-text');
+ var innernameSpan = $('<span></span>').addClass('inner-name-text').text(basename);
+
+ nameSpan.append(innernameSpan);
+
+ if (extension) {
+ nameSpan.append($('<span></span>').addClass('extension').text(extension));
+ }
+ } else {
+ nameSpan = $('<span></span>').addClass('name-text');
+ innernameSpan = $('<span></span>').addClass('inner-name-text').text(t('ransomware_detection', 'Not found.'));
+
+ nameSpan.append(innernameSpan);
+ }
+
+ td = $('<td></td>').attr({ "class": "file-name"});
+ td.append(nameSpan);
+ tr.append(td);
+
+ if (fileData.command === 1) {
+ // delete
+ td = $('<td></td>').append($('<p></p>').attr({"title": "DELETE"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-trash-alt fa-fw"></span>'));
+ } else if (fileData.command === 2) {
+ // rename
+ td = $('<td></td>').append($('<p></p>').attr({"title": "RENAME"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-font fa-fw"></span>'));
+ } else if (fileData.command === 3) {
+ // write
+ td = $('<td></td>').append($('<p></p>').attr({"title": "WRITE"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-pencil-alt fa-fw"></span>'));
+ } else if (fileData.command === 4) {
+ // read
+ td = $('<td></td>').append($('<p></p>').attr({"title": "READ"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-book fa-fw"></span>'));
+ } else if (fileData.command === 5) {
+ // create
+ td = $('<td></td>').append($('<p></p>').attr({"title": "CREATE"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-pencil-alt fa-fw"></span>'));
+ } else {
+ // error
+ td = $('<td></td>').append($('<p></p>').attr({"title": "ERROR"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-times fa-fw"></span>'));
+ }
+ tr.append(td);
+
+ // size
+ if (typeof(fileData.size) !== 'undefined' && fileData.size >= 0) {
+ simpleSize = humanFileSize(parseInt(fileData.size, 10), true);
+ sizeColor = Math.round(120-Math.pow((fileData.size/(1024*1024)),2));
+ } else {
+ simpleSize = t('ransomware_detection', 'Pending');
+ }
+
+ td = $('<td></td>').append($('<p></p>').attr({
+ "class": "filesize"
+ }).text(simpleSize));
+ tr.append(td);
+
+ if (fileData.fileClass === 1) {
+ // encrypted
+ td = $('<td></td>').append($('<p></p>').attr({"title": "ENCRYPTED"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-lock fa-fw"></span>'));
+ } else if (fileData.fileClass === 2) {
+ // compressed
+ td = $('<td></td>').append($('<p></p>').attr({"title": "COMPRESSED"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-file-archive fa-fw"></span>'));
+ } else if (fileData.fileClass === 3) {
+ // normal
+ td = $('<td></td>').append($('<p></p>').attr({"title": "NORMAL"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-file fa-fw"></span>'));
+ } else {
+ // error
+ td = $('<td></td>').append($('<p></p>').attr({"title": "ERROR"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-times fa-fw"></span>'));
+ }
+ tr.append(td);
+
+ if (fileData.fileNameClass === 0) {
+ // normal
+ td = $('<td></td>').append($('<p></p>').attr({"title": "NORMAL"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-check-circle fa-fw"></span>'));
+ } else if (fileData.fileNameClass === 1) {
+ // suspicious
+ td = $('<td></td>').append($('<p></p>').attr({"title": "SUSPICIOUS FILE EXTENSION"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-exclamation-triangle fa-fw"></span>'));
+ } else if (fileData.fileNameClass === 2) {
+ // suspicious
+ td = $('<td></td>').append($('<p></p>').attr({"title": "SUSPICIOUS FILE NAME"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-exclamation-triangle fa-fw"></span>'));
+ } else if (fileData.fileNameClass === 3) {
+ // suspicious
+ td = $('<td></td>').append($('<p></p>').attr({"title": "SUSPICIOUS"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-exclamation-triangle fa-fw"></span>'));
+ } else {
+ // error
+ td = $('<td></td>').append($('<p></p>').attr({"title": "ERROR"}).tooltip({placement: 'top'}).prepend('<span class="fas fa-times fa-fw"></span>'));
+ }
+ tr.append(td);
+
+ // date column (1000 milliseconds to seconds, 60 seconds, 60 minutes, 24 hours)
+ // difference in days multiplied by 5 - brightest shade for files older than 32 days (160/5)
+ var modifiedColor = Math.round(((new Date()).getTime() - mtime )/1000/60/60/24*5 );
+ // ensure that the brightest color is still readable
+ if (modifiedColor >= '160') {
+ modifiedColor = 160;
+ }
+ var formatted;
+ var text;
+ if (mtime > 0) {
+ formatted = OC.Util.formatDate(mtime);
+ text = OC.Util.relativeModifiedDate(mtime);
+ } else {
+ formatted = t('ransomware_detection', 'Unable to determine date');
+ text = '?';
+ }
+
+ td = $('<td></td>').attr({ "class": "date" });
+ td.append($('<span></span>').attr({
+ "class": "modified live-relative-timestamp",
+ "title": formatted,
+ "data-timestamp": mtime,
+ "style": 'color:rgb('+modifiedColor+','+modifiedColor+','+modifiedColor+')'
+ }).text(text)
+ .tooltip({placement: 'top'})
+ );
+ tr.append(td);
+
+ // Color row according to suspicion level
+ if (fileData.suspicionClass === 1) {
+ tr.attr({ 'class': self.colors.red});
+ } else if (fileData.suspicionClass === 2) {
+ tr.attr({ 'class': self.colors.orange});
+ } else if (fileData.suspicionClass === 3) {
+ tr.attr({ 'class': self.colors.yellow});
+ } else if (fileData.suspicionClass === 4) {
+ tr.attr({ 'class': self.colors.green});
+ }
+
+ return tr;
+ },
+
+ /**
+ * Update UI based on the current selection
+ */
+ updateSelectionSummary: function(sequence) {
+ if (Object.keys(this._selectedFiles).length === 0) {
+ console.log("No files selected.");
+ this.$el.find('.selected-actions').css('display', 'none');
+ this.$el.find('.detected').css('display', 'block');
+ this.$el.find('.name').text(t('ransomware_detection', 'Name')).removeClass('bold');
+ this.$el.find('.hide-selected').css('color', '#999');
+ }
+ else {
+ console.log(Object.keys(this._selectedFiles).length + " files selected.");
+ this.$table[sequence].find('.selected-actions').css('display', 'block');
+ this.$table[sequence].find('.detected').css('display', 'none');
+ if (Object.keys(this._selectedFiles).length > 1) {
+ this.$table[sequence].find('.name').text(t('ransomware_detection', '{files} files', {files: Object.keys(this._selectedFiles).length})).addClass('bold');
+ } else {
+ this.$table[sequence].find('.name').text(t('ransomware_detection', '{files} file', {files: Object.keys(this._selectedFiles).length})).addClass('bold');
+ }
+ this.$table[sequence].find('.hide-selected').css('color', '#fff');
+ }
+ }
+ };
+
+ OCA.RansomwareDetection.Scan = Scan;
+})();
+
+$(document).ready(function() {});
diff --git a/lib/Analyzer/EntropyAnalyzer.php b/lib/Analyzer/EntropyAnalyzer.php
index d4f2699..4d6d682 100644
--- a/lib/Analyzer/EntropyAnalyzer.php
+++ b/lib/Analyzer/EntropyAnalyzer.php
@@ -97,7 +97,6 @@ class EntropyAnalyzer
* NORMAL
*
* @param File $node
- * @param IStorage $storage
*
* @return EntropyResult
*/
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index d0c81fe..42f9bce 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -28,6 +28,8 @@ use OCA\RansomwareDetection\Analyzer\SequenceAnalyzer;
use OCA\RansomwareDetection\Analyzer\SequenceSizeAnalyzer;
use OCA\RansomwareDetection\Analyzer\FileTypeFunnellingAnalyzer;
use OCA\RansomwareDetection\Analyzer\EntropyFunnellingAnalyzer;
+use OCA\RansomwareDetection\Analyzer\FileNameAnalyzer;
+use OCA\RansomwareDetection\Entropy\Entropy;
use OCA\RansomwareDetection\Notification\Notifier;
use OCA\RansomwareDetection\StorageWrapper;
use OCA\RansomwareDetection\Connector\Sabre\RequestPlugin;
@@ -77,6 +79,13 @@ class Application extends App
);
});
+ // entropy
+ $container->registerService('Entropy', function ($c) {
+ return new Entropy(
+ $c->query(ILogger::class)
+ );
+ });
+
// analyzer
$container->registerService('SequenceSizeAnalyzer', function ($c) {
return new SequenceSizeAnalyzer();
@@ -92,6 +101,14 @@ class Application extends App
);
});
+ $container->registerService('FileNameAnalyzer', function ($c) {
+ return new FileNameAnalyzer(
+ $c->query(ILogger::class),
+ $c->query(Entropy::class)
+
+ );
+ });
+
$container->registerService('SequenceAnalyzer', function ($c) {
return new SequenceAnalyzer(
$c->query(SequenceSizeAnalyzer::class),
diff --git a/lib/Controller/BasicController.php b/lib/Controller/BasicController.php
new file mode 100644
index 0000000..f63f978
--- /dev/null
+++ b/lib/Controller/BasicController.php
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * @copyright Copyright (c) 2018 Matthias Held <matthias.held@uni-konstanz.de>
+ * @author Matthias Held <matthias.held@uni-konstanz.de>
+ * @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 <https://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\RansomwareDetection\Controller;
+
+use OCA\RansomwareDetection\AppInfo\Application;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\AppFramework\OCSController;
+use OCP\IConfig;
+use OCP\IUserSession;
+use OCP\IRequest;
+
+class BasicController extends OCSController
+{
+ /** @var IConfig */
+ protected $config;
+
+ /** @var IUserSession */
+ protected $userSession;
+
+ /** @var int */
+ private $userId;
+
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param IUserSession $userSession
+ * @param IConfig $config
+ * @param string $userId
+ */
+ public function __construct(
+ $appName,
+ IRequest $request,
+ IUserSession $userSession,
+ IConfig $config,
+ $userId
+ ) {
+ parent::__construct($appName, $request);
+
+ $this->config = $config;
+ $this->userSession = $userSession;
+ $this->userId = $userId;
+ }
+
+ /**
+ * Get debug mode.
+ *
+ * @NoAdminRequired
+ *
+ * @return JSONResponse
+ */
+ public function getDebugMode()
+ {
+ $debugMode = $this->config->getAppValue(Application::APP_ID, 'debug', 0);
+
+ return new JSONResponse(['status' => 'success', 'message' => 'Get debug mode.', 'debug_mode' => $debugMode], Http::STATUS_ACCEPTED);
+ }
+
+ /**
+ * Get color mode.
+ *
+ * @NoAdminRequired
+ *
+ * @return JSONResponse
+ */
+ public function getColorMode()
+ {
+ $colorMode = $this->config->getUserValue($this->userId, Application::APP_ID, 'colorMode', 0);
+
+ return new JSONResponse(['status' => 'success', 'message' => 'Get color mode.', 'color_mode' => $colorMode], Http::STATUS_ACCEPTED);
+ }
+
+ /**
+ * Changes color mode.
+ *
+ * @NoAdminRequired
+ *
+ * @param int $colorMode
+ *
+ * @return JSONResponse
+ */
+ public function changeColorMode($colorMode)
+ {
+ $this->config->setUserValue($this->userId, Application::APP_ID, 'colorMode', $colorMode);
+
+ return new JSONResponse(['status' => 'success', 'message' => 'Color mode changed.'], Http::STATUS_ACCEPTED);
+ }
+}
diff --git a/lib/Controller/MonitoringController.php b/lib/Controller/MonitoringController.php
new file mode 100644
index 0000000..f42fd27
--- /dev/null
+++ b/lib/Controller/MonitoringController.php
@@ -0,0 +1,324 @@
+<?php
+
+/**
+ * @copyright Copyright (c) 2018 Matthias Held <matthias.held@uni-konstanz.de>
+ * @author Matthias Held <matthias.held@uni-konstanz.de>
+ * @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 <https://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\RansomwareDetection\Controller;
+
+use OCA\RansomwareDetection\Monitor;
+use OCA\RansomwareDetection\Classifier;
+use OCA\RansomwareDetection\Analyzer\SequenceAnalyzer;
+use OCA\RansomwareDetection\AppInfo\Application;
+use OCA\RansomwareDetection\Db\FileOperation;
+use OCA\RansomwareDetection\Service\FileOperationService;
+use OCA\Files_Trashbin\Trashbin;
+use OCA\Files_Trashbin\Helper;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\AppFramework\OCSController;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\IConfig;
+use OCP\IUserSession;
+use OCP\IRequest;
+use OCP\ILogger;
+
+class MonitoringController extends OCSController
+{
+ /** @var IConfig */
+ protected $config;
+
+ /** @var IUserSession */
+ protected $userSession;
+
+ /** @var Classifier */
+ protected $classifier;
+
+ /** @var ILogger */
+ protected $logger;
+
+ /** @var Folder */
+ protected $userFolder;
+
+ /** @var FileOperationService */
+ protected $service;
+
+ /** @var SequenceAnalyzer */
+ protected $sequenceAnalyzer;
+
+ /** @var string */
+ protected $userId;
+
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param IUserSession $userSession
+ * @param IConfig $config
+ * @param Classifier $classifier
+ * @param ILogger $logger
+ * @param Folder $userFolder
+ * @param FileOperationService $service
+ * @param SequenceAnalyzer $sequenceAnalyzer
+ * @param string $userId
+ */
+ public function __construct(
+ $appName,
+ IRequest $request,
+ IUserSession $userSession,
+ IConfig $config,
+ Classifier $classifier,
+ ILogger $logger,
+ Folder $userFolder,
+ FileOperationService $service,
+ SequenceAnalyzer $sequenceAnalyzer,
+ $userId
+ ) {
+ parent::__construct($appName, $request);
+
+ $this->config = $config;
+ $this->userSession = $userSession;
+ $this->classifier = $classifier;
+ $this->userFolder = $userFolder;
+ $this->logger = $logger;
+ $this->service = $service;
+ $this->sequenceAnalyzer = $sequenceAnalyzer;
+ $this->userId = $userId;
+ }
+
+ /**
+ * Lists the classified files and sequences.
+ *
+ * @NoAdminRequired
+ *
+ * @return JSONResponse
+ */
+ public function listFileOperations()
+ {
+ $files = $this->service->findAll();
+
+ $sequences = [];
+
+ // Classify files and put together the sequences.
+ foreach ($files as $file) {
+ $this->classifier->classifyFile($file);
+ $sequences[$file->getSequence()][] = $file;
+ }
+
+ $result = [];
+
+ foreach ($sequences as $sequenceId => $sequence) {
+ if (sizeof($sequence) >= $this->config->getAppValue(Application::APP_ID, 'minimum_sequence_length', 0)) {
+ usort($sequence, function ($a, $b) {
+ return $b->getId() - $a->getId();
+ });
+ $sequenceResult = $this->sequenceAnalyzer->analyze($sequenceId, $sequence);
+ $sequenceInformation = ['id' => $sequenceId, 'suspicionScore' => $sequenceResult->getSuspicionScore(), 'sequence' => $sequence];
+ $result[] = $sequenceInformation;
+ }
+ }
+
+ usort($result, function ($a, $b) {
+ return $b['id'] - $a['id'];
+ });
+
+ return new JSONResponse($result, Http::STATUS_ACCEPTED);
+ }
+
+ /**
+ * Exports classification and analysis data.
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @param int $sequence
+ *
+ * @return JSONResponse
+ */
+ public function export()
+ {
+ $files = $this->service->findAll();
+
+ $sequences = [];
+
+ // Classify files and put together the sequences.
+ foreach ($files as $file) {
+ $this->classifier->classifyFile($file);
+ $sequences[$file->getSequence()][] = $file;
+ }
+
+ $result = [];
+
+ foreach ($sequences as $sequenceId => $sequence) {
+ if (sizeof($sequence) >= $this->config->getAppValue(Application::APP_ID, 'minimum_sequence_length', 0)) {
+ $result[] = $this->sequenceAnalyzer->analyze($sequenceId, $sequence)->toArray();
+ }
+ }
+
+ return new JSONResponse($result, Http::STATUS_ACCEPTED);
+ }
+
+ /**
+ * Deletes a sequence from the database.
+ *
+ * @NoAdminRequired
+ *
+ * @param int $sequence
+ *
+ * @return JSONResponse
+ */
+ public function deleteSequence($sequence)
+ {
+ $files = $this->service->deleteSequenceById($sequence);
+
+ return new JSONResponse(['status' => 'success'], Http::STATUS_ACCEPTED);
+ }
+
+ /**
+ * Recover files from trashbin or remove them from normal storage.
+ *
+ * @NoAdminRequired
+ *
+ * @param int $id file operation id
+ *
+ * @return JSONResponse
+ */
+ public function recover($id)
+ {
+ try {
+ $file = $this->service->find($id);
+ if ($file->getCommand() === Monitor::WRITE) {
+ // Recover new created files by deleting them
+ $filePath = $file->getPath().'/'.$file->getOriginalName();
+ if ($this->deleteFromStorage($filePath)) {
+ $this->service->deleteById($id);
+
+ return new JSONResponse(['status' => 'success', 'id' => $id], Http::STATUS_OK);
+ } else {
+ return new JSONResponse(['status' => 'error', 'message' => 'File cannot be deleted.'], Http::STATUS_BAD_REQUEST);
+ }
+ } elseif ($file->getCommand() === Monitor::DELETE) {
+ // Recover deleted files by restoring them from the trashbin
+ // It's not necessary to use the real path
+ $dir = '/';
+ $candidate = $this->findCandidateToRestore($dir, $file->getOriginalName());
+ if ($candidate !== null) {
+ $path = $dir.'/'.$candidate['name'].'.d'.$candidate['mtime'];
+ if (Trashbin::restore($path, $candidate['name'], $candidate['mtime']) !== false) {
+ $this->service->deleteById($id);
+
+ return new JSONResponse(['status' => 'success', 'id' => $id], Http::STATUS_OK);
+ }
+
+ return new JSONResponse(['status' => 'error', 'message' => 'File does not exist.', 'path' => $path, 'name' => $candidate['name'], 'mtime' => $candidate['mtime']], Http::STATUS_BAD_REQUEST);
+ } else {
+ return new JSONResponse(['status' => 'error', 'message' => 'No candidate found.'], Http::STATUS_BAD_REQUEST);
+ }
+ } elseif ($file->getCommand() === Monitor::RENAME) {
+ $this->service->deleteById($id);
+
+ return new JSONResponse(['status' => 'success', 'id' => $id], Http::STATUS_OK);
+ } elseif ($file->getCommand() === Monitor::CREATE) {
+ // Recover new created files by deleting them
+ $filePath = $file->getPath().'/'.$file->getOriginalName();
+ if ($this->deleteFromStorage($filePath)) {
+ $this->service->deleteById($id);
+
+ return new JSONResponse(['status' => 'success', 'id' => $id], Http::STATUS_OK);
+ } else {
+ return new JSONResponse(['status' => 'error', 'message' => 'File cannot be deleted.'], Http::STATUS_BAD_REQUEST);
+ }
+ } else {
+ // All other commands need no recovery
+ $this->service->deleteById($id);
+
+ return new JSONResponse(['id' => $id], Http::STATUS_OK);
+ }
+ } catch (\OCP\AppFramework\Db\MultipleObjectsReturnedException $exception) {
+ // Found more than one with the same file name
+ $this->logger->debug('recover: Found more than one with the same file name.', array('app' => Application::APP_ID));
+
+ return new JSONResponse(['status' => 'error', 'message' => 'Found more than one with the same file name.'], Http::STATUS_BAD_REQUEST);
+ } catch (\OCP\AppFramework\Db\DoesNotExistException $exception) {
+ // Nothing found
+ $this->logger->debug('recover: Files does not exist.', array('app' => Application::APP_ID));
+
+ return new JSONResponse(['status' => 'error', 'message' => 'Files does not exist.'], Http::STATUS_BAD_REQUEST);
+ }
+ }
+
+ /**
+ * Deletes a file from the storage.
+ *
+ * @param string $path
+ *
+ * @return bool
+ */
+ private function deleteFromStorage($path)
+ {
+ try {
+ $node = $this->userFolder->get($path);
+ if ($node->isDeletable()) {
+ $node->delete();
+ } else {
+ return false;
+ }
+
+ return true;
+ } catch (\OCP\Files\NotFoundException $exception) {
+ // Nothing found
+ $this->logger->debug('deleteFromStorage: Not found exception.', array('app' => Application::APP_ID));
+
+ return true;
+ }
+ }
+
+ /**
+ * Finds a candidate to restore if a file with the specific does not exist.
+ *
+ * @param string $dir
+ * @param string $fileName
+ *
+ * @return FileInfo
+ */
+ private function findCandidateToRestore($dir, $fileName)
+ {
+ $files = array();
+ $trashBinFiles = $this->getTrashFiles($dir);
+
+ foreach ($trashBinFiles as $trashBinFile) {
+ if (strcmp($trashBinFile['name'], $fileName) === 0) {
+ $files[] = $trashBinFile;
+ }
+ }
+
+ return array_pop($files);
+ }
+
+ /**
+ * Workaround for testing.
+ *
+ * @param string $dir
+ *
+ * @return array
+ */
+ private function getTrashFiles($dir)
+ {
+ return Helper::getTrashFiles($dir, $this->userId, 'mtime', false);
+ }
+}
diff --git a/lib/Controller/RecoverController.php b/lib/Controller/RecoverController.php
index 0307460..52497df 100644
--- a/lib/Controller/RecoverController.php
+++ b/lib/Controller/RecoverController.php
@@ -57,4 +57,17 @@ class RecoverController extends Controller
{
return new TemplateResponse(Application::APP_ID, 'index');
}
+
+ /**
+ * Scan page.
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @return TemplateResponse
+ */
+ public function scan()
+ {
+ return new TemplateResponse(Application::APP_ID, 'scan');
+ }
}
diff --git a/lib/Controller/ScanController.php b/lib/Controller/ScanController.php
new file mode 100644
index 0000000..9557892
--- /dev/null
+++ b/lib/Controller/ScanController.php
@@ -0,0 +1,416 @@
+<?php
+
+/**
+ * @copyright Copyright (c) 2018 Matthias Held <matthias.held@uni-konstanz.de>
+ * @author Matthias Held <matthias.held@uni-konstanz.de>
+ * @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 <https://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\RansomwareDetection\Controller;
+
+use OCA\RansomwareDetection\Monitor;
+use OCA\RansomwareDetection\Classifier;
+use OCA\RansomwareDetection\Analyzer\SequenceAnalyzer;
+use OCA\RansomwareDetection\Analyzer\EntropyAnalyzer;
+use OCA\RansomwareDetection\Analyzer\FileCorruptionAnalyzer;
+use OCA\RansomwareDetection\Analyzer\FileNameAnalyzer;
+use OCA\RansomwareDetection\AppInfo\Application;
+use OCA\RansomwareDetection\Db\FileOperation;
+use OCA\RansomwareDetection\Service\FileOperationService;
+use OCA\RansomwareDetection\Scanner\StorageStructure;
+use OCA\Files_Trashbin\Trashbin;
+use OCA\Files_Trashbin\Helper;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\AppFramework\OCSController;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\IConfig;
+use OCP\IUserSession;
+use OCP\IRequest;
+use OCP\IDBConnection;
+use OCP\ILogger;
+
+class ScanController extends OCSController
+{
+ /** @var IConfig */
+ protected $config;
+
+ /** @var IUserSession */
+ protected $userSession;
+
+ /** @var Classifier */
+ protected $classifier;
+
+ /** @var ILogger */
+ protected $logger;
+
+ /** @var Folder */
+ protected $userFolder;
+
+ /** @var FileOperationService */
+ protected $service;
+
+ /** @var SequenceAnalyzer */
+ protected $sequenceAnalyzer;
+
+ /** @var EntropyAnalyzer */
+ protected $entropyAnalyzer;
+
+ /** @var FileCorruptionAnalyzer */
+ protected $fileCorruptionAnalyzer;
+
+ /** @var FileNameAnalyzer */
+ protected $fileNameAnalyzer;
+
+ /** @var IDBConnection */
+ protected $connection;
+
+ /** @var string */
+ protected $userId;
+
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param IUserSession $userSession
+ * @param IConfig $config
+ * @param Classifier $classifier
+ * @param ILogger $logger
+ * @param Folder $userFolder
+ * @param FileOperationService $service
+ * @param SequenceAnalyzer $sequenceAnalyzer
+ * @param EntropyAnalyzer $entropyAnalyzer
+ * @param FileCorruptionAnalyzer $fileCorruptionAnalyzer
+ * @param FileNameAnalyzer $fileNameAnalyzer
+ * @param IDBConnection $connection
+ * @param string $userId
+ */
+ public function __construct(
+ $appName,
+ IRequest $request,
+ IUserSession $userSession,
+ IConfig $config,
+ Classifier $classifier,
+ ILogger $logger,
+ Folder $userFolder,
+ FileOperationService $service,
+ SequenceAnalyzer $sequenceAnalyzer,
+ EntropyAnalyzer $entropyAnalyzer,
+ FileCorruptionAnalyzer $fileCorruptionAnalyzer,
+ FileNameAnalyzer $fileNameAnalyzer,
+ IDBConnection $connection,
+ $userId
+ ) {
+ parent::__construct($appName, $request);
+
+ $this->config = $config;
+ $this->userSession = $userSession;
+ $this->classifier = $classifier;
+ $this->userFolder = $userFolder;
+ $this->logger = $logger;
+ $this->service = $service;
+ $this->sequenceAnalyzer = $sequenceAnalyzer;
+ $this->entropyAnalyzer = $entropyAnalyzer;
+ $this->fileCorruptionAnalyzer = $fileCorruptionAnalyzer;
+ $this->fileNameAnalyzer = $fileNameAnalyzer;
+ $this->connection = $connection;
+ $this->userId = $userId;
+ }
+
+ /**
+ * Post scan recovery.
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @param integer $id
+ * @param integer $command
+ * @param string $path
+ * @param integer $timestamp
+ *
+ * @return JSONResponse
+ */
+ public function recover($id, $command, $path ,$timestamp)
+ {
+ if ($command === Monitor::WRITE) {
+ // Delete file
+ if ($this->deleteFromStorage($path)) {
+ return new JSONResponse(['status' => 'success', 'id' => $id], Http::STATUS_OK);
+ } else {
+ return new JSONResponse(['status' => 'error', 'message' => 'File cannot be deleted.'], Http::STATUS_BAD_REQUEST);
+ }
+ } else if ($command === Monitor::DELETE) {
+ // Restore file
+ $dir = '/';
+ $pathInfo = pathinfo($path);
+ $trashPath = $dir.'/'.$pathInfo['basename'];
+ if ($this->restoreFromTrashbin($trashPath, $pathInfo, $timestamp) !== false) {
+ return new JSONResponse(['status' => 'success', 'id' => $id], Http::STATUS_OK);
+ }
+
+ return new JSONResponse(['status' => 'error', 'message' => 'File does not exist.', 'path' => $trashPath, 'name' => $pathInfo['filename'], 'mtime' => $timestamp], Http::STATUS_BAD_REQUEST);
+ } else {
+ // wubalubadubdub
+ // Scan can only detect WRITE and DELETE this should never happen.
+ $this->logger->error('postRecover: RENAME or CREATE operation.', array('app' => Application::APP_ID));
+ return new JSONResponse(['status' => 'error', 'message' => 'Wrong command.'], Http::STATUS_BAD_REQUEST);
+ }
+
+ }
+
+ /**
+ * The files to scan.
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @return JSONResponse
+ */
+ public function filesToScan()
+ {
+ $storageStructure = $this->getStorageStructure($this->userFolder);
+ $trashStorageStructure = $this->getTrashStorageStructure();
+
+ $allFiles = array();
+
+ // convert file to json and merge into one array
+ $files = $storageStructure->getFiles();
+ for ($i = 0; $i < count($files); $i++) {
+ $allFiles[] = ['id' => $files[$i]->getId(), 'path' => $files[$i]->getInternalPath(), 'timestamp' => $this->getLastActivity($files[$i]->getId())['timestamp']];
+ }
+ $trashFiles = $trashStorageStructure->getFiles();
+ for ($i = 0; $i < count($trashFiles); $i++) {
+ $allFiles[] = ['id' => $trashFiles[$i]->getId(), 'path' => $trashFiles[$i]->getInternalPath(), 'timestamp' => $trashFiles[$i]->getMtime()];
+ }
+
+ // sort ASC for timestamp
+ usort($allFiles, function ($a, $b) {
+ if ($a['timestamp'] === $b['timestamp']) {
+ return 0;
+ }
+ return $a['timestamp'] - $b['timestamp'];
+ });
+
+ // build sequences
+ $sequencesArray = array();
+ $sequence = array();
+ for ($i = 0; $i < count($allFiles); $i++) {
+ if ($i === 0) {
+ $sequence = array();
+ } else {
+ if ($allFiles[$i]['timestamp'] - $allFiles[$i - 1]['timestamp'] > 180000) {
+ $sequencesArray[] = $sequence;
+ $sequence = array();
+ }
+ }
+ $sequence[] = $allFiles[$i];
+ }
+ $sequencesArray[] = $sequence;
+
+ return new JSONResponse(['status' => 'success', 'sequences' => $sequencesArray, 'number_of_files' => $storageStructure->getNumberOfFiles()], Http::STATUS_ACCEPTED);
+ }
+
+ /**
+ * Scan sequence.
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @param string $sequence
+ * @return JSONResponse
+ */
+ public function scanSequence($sequence) {
+ if (sizeof($sequence) > $this->config->getAppValue(Application::APP_ID, 'minimum_sequence_length', 0)) {
+ $sequenceResults = array();
+ foreach ($sequence as $file) {
+ $fileOperation = $this->buildFileOperation($file);
+
+ $this->classifier->classifyFile($fileOperation);
+ $sequenceResults[] = ['userId' => $fileOperation->getUserId(), 'path' => $fileOperation->getPath(), 'originalName' => preg_replace('/.d[0-9]{10}/', '', $fileOperation->getOriginalName()),
+ 'type' => $fileOperation->getType(), 'mimeType' => $fileOperation->getMimeType(), 'size' => $fileOperation->getSize(), 'corrupted' => $fileOperation->getCorrupted(), 'timestamp' => 123, 'entropy' => $fileOperation->getEntropy(),
+ 'standardDeviation' => $fileOperation->getStandardDeviation(), 'command' => $fileOperation->getCommand(), 'fileNameEntropy' => $fileOperation->getFileNameEntropy(), 'fileClass' => $fileOperation->getFileClass(), 'fileNameClass' => $fileOperation->getFileNameClass(), 'suspicionClass' => $fileOperation->getSuspicionClass()];
+ $sequenceForAnalyzer[] = $fileOperation;
+ }
+ $sequenceResult = $this->sequenceAnalyzer->analyze(0, $sequenceForAnalyzer);
+ return new JSONResponse(['status' => 'success', 'suspicion_score' => $sequenceResult->getSuspicionScore(), 'sequence' => $sequenceResults], Http::STATUS_OK);
+ } else {
+ return new JSONResponse(['status' => 'error', 'message' => 'Sequence is to short.'], Http::STATUS_BAD_REQUEST);
+ }
+ }
+
+ /**
+ * Builds a file operations from a file info array.
+ *
+ * @param array $file
+ * @return FileOperation
+ */
+ protected function buildFileOperation($file)
+ {
+ $fileOperation = new FileOperation();
+ $fileOperation->setUserId($this->userId);
+ if (strpos($file['path'], 'files_trashbin') !== false) {
+ $node = $this->userFolder->getParent()->get($file['path'] . '.d' . $file['timestamp']);
+ $fileOperation->setCommand(Monitor::DELETE);
+ $fileOperation->setTimestamp($file['timestamp']);
+ } else {
+ $node = $this->userFolder->getParent()->get($file['path']);
+ $lastActivity = $this->getLastActivity($file['id']);
+ $fileOperation->setCommand(Monitor::WRITE);
+ $fileOperation->setTimestamp($lastActivity['timestamp']);
+ }
+ $fileOperation->setOriginalName($node->getName());
+ $fileOperation->setType('file');
+ $fileOperation->setMimeType($node->getMimeType());
+ $fileOperation->setSize($node->getSize());
+ $fileOperation->setTimestamp($file['timestamp']);
+ $fileOperation->setPath($node->getPath());
+
+ // file name analysis
+ $fileNameResult = $this->fileNameAnalyzer->analyze($node->getInternalPath());
+ $fileOperation->setFileNameClass($fileNameResult->getFileNameClass());
+ $fileOperation->setFileNameEntropy($fileNameResult->getEntropyOfFileName());
+
+ $fileCorruptionResult = $this->fileCorruptionAnalyzer->analyze($node);
+ $fileOperation->setCorrupted($fileCorruptionResult->isCorrupted());
+
+ // entropy analysis
+ $entropyResult = $this->entropyAnalyzer->analyze($node);
+ $fileOperation->setEntropy($entropyResult->getEntropy());
+ $fileOperation->setStandardDeviation($entropyResult->getStandardDeviation());
+ if ($fileCorruptionResult->isCorrupted()) {
+ $fileOperation->setFileClass($entropyResult->getFileClass());
+ } else {
+ if ($fileCorruptionResult->getFileClass() !== -1) {
+ $fileOperation->setFileClass($fileCorruptionResult->getFileClass());
+ }
+ }
+
+ return $fileOperation;
+ }
+
+ /**
+ * Get last activity.
+ *
+ * @param $objectId
+ */
+ protected function getLastActivity($objectId)
+ {
+ $query = $this->connection->getQueryBuilder();
+ $query->select('*')->from('activity');
+ $query->where($query->expr()->eq('affecteduser', $query->createNamedParameter($this->userId)))
+ ->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)));
+ $result = $query->execute();
+ while ($row = $result->fetch()) {
+ $rows[] = $row;
+ }
+ $result->closeCursor();
+ return array_pop($rows);
+ }
+
+ /**
+ * Get trash storage structure.
+ *
+ * @return StorageStructure
+ */
+ protected function getTrashStorageStructure()
+ {
+ $storageStructure = new StorageStructure(0, []);
+ $nodes = Helper::getTrashFiles("/", $this->userId, 'mtime', false);
+ foreach ($nodes as $node) {
+ $storageStructure->addFile($node);
+ $storageStructure->increaseNumberOfFiles();
+ }
+ return $storageStructure;
+ }
+
+ /**
+ * Get storage structure recursively.
+ *
+ * @param INode $node
+ *
+ * @return StorageStructure
+ */
+ protected function getStorageStructure($node)
+ {
+ // set count for node to 0
+ $storageStructure = new StorageStructure(0, []);
+ if ($node instanceof Folder) {
+ // it's a folder
+ $nodes = $node->getDirectoryListing();
+ if (count($nodes) === 0) {
+ // folder is empty so nothing to do
+ return $storageStructure;
+ }
+ foreach ($nodes as $tmpNode) {
+ // analyse files in subfolder
+ $tmpStorageStructure = $this->getStorageStructure($tmpNode);
+ $storageStructure->setFiles(array_merge($storageStructure->getFiles(), $tmpStorageStructure->getFiles()));
+ $storageStructure->setNumberOfFiles($storageStructure->getNumberOfFiles() + $tmpStorageStructure->getNumberOfFiles());
+ }
+ return $storageStructure;
+ }
+ else if ($node instanceof File) {
+ // it's a file
+ $storageStructure->addFile($node);
+ $storageStructure->increaseNumberOfFiles();
+ return $storageStructure;
+ }
+ else {
+ // it's me Mario.
+ // there is nothing else than file or folder
+ $this->logger->error('getStorageStructure: Neither file nor folder.', array('app' => Application::APP_ID));
+ }
+ }
+
+ /**
+ * Deletes a file from the storage.
+ *
+ * @param string $path
+ *
+ * @return bool
+ */
+ protected function deleteFromStorage($path)
+ {
+ try {
+ $node = $this->userFolder->get($path);
+ if ($node->isDeletable()) {
+ $node->delete();
+ } else {
+ return false;
+ }
+
+ return true;
+ } catch (\OCP\Files\NotFoundException $exception) {
+ // Nothing found
+ $this->logger->debug('deleteFromStorage: Not found exception.', array('app' => Application::APP_ID));
+
+ return true;
+ }
+ }
+
+ /**
+ * Restores file from trash bin.
+ *
+ * @param string $trashPath
+ * @param array $pathInfo
+ * @param integer $timestamp
+ * @return boolean
+ */
+ protected function restoreFromTrashbin($trashPath, $pathInfo, $timestamp)
+ {
+ return Trashbin::restore($trashPath, $pathInfo['filename'], $timestamp);
+ }
+}
diff --git a/lib/Scanner/StorageStructure.php b/lib/Scanner/StorageStructure.php
new file mode 100644
index 0000000..9949504
--- /dev/null
+++ b/lib/Scanner/StorageStructure.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * @copyright Copyright (c) 2018 Matthias Held <matthias.held@uni-konstanz.de>
+ * @author Matthias Held <matthias.held@uni-konstanz.de>
+ * @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 <https://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\RansomwareDetection\Scanner;
+
+class StorageStructure {
+
+ /** @var integer */
+ protected $numberOfFiles = 0;
+
+ /** @var array */
+ protected $files = array();
+
+ /**
+ * @param integer $numberOfFiles
+ * @param array $files
+ */
+ public function __construct(
+ $numberOfFiles = 0,
+ $files = array()
+ ) {
+ $this->numberOfFiles = $numberOfFiles;
+ $this->files = $files;
+ }
+
+ /**
+ * Get number of files.
+ *
+ * @return integer
+ */
+ public function getNumberOfFiles() {
+ return $this->numberOfFiles;
+ }
+
+ /**
+ * Set number of files.
+ *
+ * @param integer $numberOfFiles
+ */
+ public function setNumberOfFiles($numberOfFiles) {
+ $this->numberOfFiles = $numberOfFiles;
+ }
+
+ /**
+ * Increase the number of files.
+ *
+ * @return integer
+ */
+ public function increaseNumberOfFiles() {
+ return $this->numberOfFiles++;
+ }
+
+ /**
+ * Get files.
+ *
+ * @return Files[]
+ */
+ public function getFiles() {
+ return $this->files;
+ }
+
+ /**
+ * Set files.
+ *
+ * @param Files[] $files
+ */
+ public function setFiles($files) {
+ $this->files = $files;
+ }
+
+ /**
+ * Add a file.
+ *
+ * @param File $file
+ */
+ public function addFile($file) {
+ array_push($this->files, $file);
+ }
+}
diff --git a/templates/index.php b/templates/index.php
index d5800e0..53bd8e9 100644
--- a/templates/index.php
+++ b/templates/index.php
@@ -23,8 +23,24 @@ script('ransomware_detection', 'vendor/font-awesome/fontawesome-all');
style('ransomware_detection', 'style');
?>
<div id="app">
+ <div id="app-navigation">
+ <ul>
+ <li class="active">
+ <a href="<?php p(\OC::$server->getURLGenerator()->linkToRoute('ransomware_detection.recover.index', [])); ?>">
+ <img alt="" src="<?php print_unescaped(\OC::$server->getURLGenerator()->imagePath('core', 'actions/history.svg')); ?>">
+ <span>Monitoring</span>
+ </a>
+ </li>
+ <li>
+ <a href="<?php p(\OC::$server->getURLGenerator()->linkToRoute('ransomware_detection.recover.scan', [])); ?>">
+ <img alt="" src="<?php print_unescaped(\OC::$server->getURLGenerator()->imagePath('core', 'actions/search.svg')); ?>">
+ <span>Scan files</span>
+ </a>
+ </li>
+ </ul>
+ </div>
<div id="app-content">
- <div id="app-content-ransomware-detection">
+ <div id="app-content-ransomware-detection-filelist">
<!-- Tables -->
</div>
</div>
diff --git a/templates/scan.php b/templates/scan.php
new file mode 100644
index 0000000..5a365a5
--- /dev/null
+++ b/templates/scan.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * @copyright Copyright (c) 2018 Matthias Held <matthias.held@uni-konstanz.de>
+ * @author Matthias Held <matthias.held@uni-konstanz.de>
+ * @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 <https://www.gnu.org/licenses/>.
+ */
+script('ransomware_detection', 'app');
+script('ransomware_detection', 'scan');
+script('ransomware_detection', 'vendor/font-awesome/fontawesome-all');
+style('ransomware_detection', 'style');
+?>
+<div id="app">
+ <div id="app-navigation">
+ <ul>
+ <li>
+ <a href="<?php p(\OC::$server->getURLGenerator()->linkToRoute('ransomware_detection.recover.index', [])); ?>">
+ <img alt="" src="<?php print_unescaped(\OC::$server->getURLGenerator()->imagePath('core', 'actions/history.svg')); ?>">
+ <span>Monitoring</span>
+ </a>
+ </li>
+ <li class="active">
+ <a href="<?php p(\OC::$server->getURLGenerator()->linkToRoute('ransomware_detection.recover.scan', [])); ?>">
+ <img alt="" src="<?php print_unescaped(\OC::$server->getURLGenerator()->imagePath('core', 'actions/search.svg')); ?>">
+ <span>Scan files</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ <div id="app-content">
+ <div id="app-content-ransomware-detection-scan">
+ <!-- Tables -->
+ <div class="section" id="section-loading">
+ <p class="text-center">
+ <div class="icon-loading-dark"></div>
+ </p>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/tests/Unit/Controller/BasicControllerTest.php b/tests/Unit/Controller/BasicControllerTest.php
new file mode 100644
index 0000000..b49592e
--- /dev/null
+++ b/tests/Unit/Controller/BasicControllerTest.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * @copyright Copyright (c) 2018 Matthias Held <matthias.held@uni-konstanz.de>
+ * @author Matthias Held <matthias.held@uni-konstanz.de>
+ * @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 <https://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\RansomwareDetection\tests\Unit\Controller;
+
+use OCA\RansomwareDetection\Controller\BasicController;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use Test\TestCase;
+
+class BasicControllerTest extends TestCase
+{
+ /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
+ protected $request;
+
+ /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */
+ protected $userSession;
+
+ /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */
+ protected $config;
+
+ /** @var string */
+ protected $userId = 'john';
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->request = $this->getMockBuilder('OCP\IRequest')
+ ->getMock();
+ $this->userSession = $this->getMockBuilder('OCP\IUserSession')
+ ->getMock();
+ $this->config = $this->getMockBuilder('OCP\IConfig')
+ ->getMock();
+ }
+
+ public function testGetDebugMode()
+ {
+ $controller = new BasicController(
+ 'ransomware_detection',
+ $this->request,
+ $this->userSession,
+ $this->config,
+ 'john'
+ );
+
+ $result = $controller->getDebugMode();
+ $this->assertTrue($result instanceof JSONResponse);
+ $this->assertEquals($result->getStatus(), Http::STATUS_ACCEPTED);
+ }
+
+ public function testGetColorMode()
+ {
+ $controller = new BasicController(
+ 'ransomware_detection',
+ $this->request,
+ $this->userSession,
+ $this->config,
+ 'john'
+ );
+
+ $result = $controller->getColorMode();
+ $this->assertTrue($result instanceof JSONResponse);
+ $this->assertEquals($result->getStatus(), Http::STATUS_ACCEPTED);
+ }
+
+ public function testChangeColorMode()
+ {
+ $controller = new BasicController(
+ 'ransomware_detection',
+ $this->request,
+ $this->userSession,
+ $this->config,
+ 'john'
+ );
+
+ $result = $controller->changeColorMode(1);
+ $this->assertTrue($result instanceof JSONResponse);
+ $this->assertEquals($result->getStatus(), Http::STATUS_ACCEPTED);
+ }
+}
diff --git a/tests/Unit/Controller/MonitoringControllerTest.php b/tests/Unit/Controller/MonitoringControllerTest.php
new file mode 100644
index 0000000..3a07604
--- /dev/null
+++ b/tests/Unit/Controller/MonitoringControllerTest.php
@@ -0,0 +1,341 @@
+<?php
+
+/**
+ * @copyright Copyright (c) 2018 Matthias Held <matthias.held@uni-konstanz.de>
+ * @author Matthias Held <matthias.held@uni-konstanz.de>
+ * @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 <https://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\RansomwareDetection\tests\Unit\Controller;
+
+use OCA\RansomwareDetection\Monitor;
+use OCA\RansomwareDetection\Analyzer\SequenceAnalyzer;
+use OCA\RansomwareDetection\Analyzer\SequenceResult;
+use OCA\RansomwareDetection\Db\FileOperation;
+use OCA\RansomwareDetection\Controller\MonitoringController;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use Test\TestCase;
+
+class MonitoringControllerTest extends TestCase
+{
+ /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
+ protected $request;
+
+ /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */
+ protected $userSession;
+
+ /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */
+ protected $config;
+
+ /** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */
+ protected $logger;
+
+ /** @var Classifier|\PHPUnit_Framework_MockObject_MockObject */
+ protected $classifier;
+
+ /** @var Folder|\PHPUnit_Framework_MockObject_MockObject */
+ protected $folder;
+
+ /** @var FileOperationService|\PHPUnit_Framework_MockObject_MockObject */
+ protected $service;
+
+ /** @var SequenceAnalyzer|\PHPUnit_Framework_MockObject_MockObject */
+ protected $sequenceAnalyzer;
+
+ /** @var string */
+ protected $userId = 'john';
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->request = $this->getMockBuilder('OCP\IRequest')
+ ->getMock();
+ $this->userSession = $this->getMockBuilder('OCP\IUserSession')
+ ->getMock();
+ $this->config = $this->getMockBuilder('OCP\IConfig')
+ ->getMock();
+ $this->logger = $this->getMockBuilder('OCP\ILogger')
+ ->getMock();
+ $this->folder = $this->getMockBuilder('OCP\Files\Folder')
+ ->getMock();
+ $connection = $this->getMockBuilder('OCP\IDBConnection')
+ ->getMock();
+ $mapper = $this->getMockBuilder('OCA\RansomwareDetection\Db\FileOperationMapper')
+ ->setConstructorArgs([$connection])
+ ->getMock();
+ $this->service = $this->getMockBuilder('OCA\RansomwareDetection\Service\FileOperationService')
+ ->setConstructorArgs([$mapper, $this->userId])
+ ->getMock();
+ $this->classifier = $this->getMockBuilder('OCA\RansomwareDetection\Classifier')
+ ->setConstructorArgs([$this->logger, $mapper, $this->service])
+ ->getMock();
+ $this->sequenceAnalyzer = $this->createMock(SequenceAnalyzer::class);
+ }
+
+ public function testListFileOperations()
+ {
+ $controller = new MonitoringController(
+ 'ransomware_detection',
+ $this->request,
+ $this->userSession,
+ $this->config,
+ $this->classifier,
+ $this->logger,
+ $this->folder,
+ $this->service,
+ $this->sequenceAnalyzer,
+ 'john'
+ );
+ $file = $this->getMockBuilder(FileOperation::class)
+ ->setMethods(['getSequence'])
+ ->getMock();
+
+ $sequenceResult = new SequenceResult(0, 0, 0, 0, 0, 0);
+
+ $file->method('getSequence')
+ ->willReturn(1);
+
+ $this->service->method('findAll')
+ ->willReturn([$file]);
+
+ $this->classifier->method('classifyFile');
+ $this->sequenceAnalyzer->method('analyze')
+ ->willReturn($sequenceResult);
+
+ $result = $controller->listFileOperations();
+ $this->assertTrue($result instanceof JSONResponse);
+ $this->assertEquals($result->getStatus(), Http::STATUS_ACCEPTED);
+ }
+
+ public function testDeleteSequence()
+ {
+ $controller = new MonitoringController(
+ 'ransomware_detection',
+ $this->request,
+ $this->userSession,
+ $this->config,
+ $this->classifier,
+ $this->logger,
+ $this->folder,
+ $this->service,
+ $this->sequenceAnalyzer,
+ 'john'
+ );
+ $this->service->method('deleteSequenceById')
+ ->with(1)
+ ->will($this->returnValue([]));
+
+ $result = $controller->deleteSequence(1);
+ $this->assertTrue($result instanceof JSONResponse);
+ $this->assertEquals($result->getStatus(), Http::STATUS_ACCEPTED);
+ }
+
+ public function dataRecover()
+ {
+ $fileOperationWrite = new FileOperation();
+ $fileOperationWrite->setCommand(Monitor::WRITE);
+ $fileOperationWrite->setPath('/admin/files');
+ $fileOperationWrite->setOriginalName('test.jpg');
+
+ $fileOperationRead = new FileOperation();
+ $fileOperationRead->setCommand(Monitor::READ);
+ $fileOperationRead->setPath('/admin/files');
+ $fileOperationRead->setOriginalName('test.jpg');
+
+ $fileOperationDelete = new FileOperation();
+ $fileOperationDelete->setCommand(Monitor::DELETE);
+ $fileOperationDelete->setPath('/admin/file');
+ $fileOperationDelete->setOriginalName('test.jpg');
+
+ $fileOperationRename = new FileOperation();
+ $fileOperationRename->setCommand(Monitor::RENAME);
+ $fileOperationRename->setPath('/admin/file');
+ $fileOperationRename->setOriginalName('test.jpg');
+
+ return [
+ ['id' => 4, 'fileOperation' => new FileOperation(), 'deleted' => false, 'response' => Http::STATUS_OK],
+ ['id' => 1, 'fileOperation' => $fileOperationRead, 'deleted' => true, 'response' => Http::STATUS_OK],
+ ['id' => 2, 'fileOperation' => $fileOperationRename, 'deleted' => true, 'response' => Http::STATUS_OK],
+ ];
+ }
+
+ /**
+ * @dataProvider dataRecover
+ *
+ * @param array $fileIds
+ * @param FileOperation $fileOperation
+ * @param bool $deleted
+ * @param HttpResponse $response
+ */
+ public function testRecover($fileIds, $fileOperation, $deleted, $response)
+ {
+ $controller = $this->getMockBuilder(MonitoringController::class)
+ ->setConstructorArgs(['ransomware_detection', $this->request, $this->userSession, $this->config, $this->classifier,
+ $this->logger, $this->folder, $this->service, $this->sequenceAnalyzer, 'john', ])
+ ->setMethods(['deleteFromStorage', 'getTrashFiles'])
+ ->getMock();
+
+ $controller->expects($this->any())
+ ->method('getTrashFiles')
+ ->willReturn([]);
+
+ $this->service->method('find')
+ ->willReturn($fileOperation);
+
+ $controller->expects($this->any())
+ ->method('deleteFromStorage')
+ ->willReturn($deleted);
+
+ $this->service->method('deleteById');
+
+ $result = $controller->recover($fileIds);
+ $this->assertTrue($result instanceof JSONResponse);
+ $this->assertEquals($result->getStatus(), $response);
+ }
+
+ public function testRecoverMultipleObjectsReturnedException()
+ {
+ $controller = $this->getMockBuilder(MonitoringController::class)
+ ->setConstructorArgs(['ransomware_detection', $this->request, $this->userSession, $this->config, $this->classifier,
+ $this->logger, $this->folder, $this->service, $this->sequenceAnalyzer, 'john', ])
+ ->setMethods(['getTrashFiles'])
+ ->getMock();
+
+ $fileOperationWrite = new FileOperation();
+ $fileOperationWrite->setCommand(Monitor::WRITE);
+ $fileOperationWrite->setPath('/admin/files');
+ $fileOperationWrite->setOriginalName('test.jpg');
+
+ $controller->expects($this->any())
+ ->method('getTrashFiles')
+ ->willReturn([]);
+
+ $this->service->method('find')
+ ->will($this->throwException(new \OCP\AppFramework\Db\MultipleObjectsReturnedException('test')));
+
+ $result = $controller->recover(1);
+ $this->assertTrue($result instanceof JSONResponse);
+ $this->assertEquals($result->getStatus(), Http::STATUS_BAD_REQUEST);
+ }
+
+ public function testDoesNotExistException()
+ {
+ $controller = new MonitoringController(
+ 'ransomware_detection',
+ $this->request,
+ $this->userSession,
+ $this->config,
+ $this->classifier,
+ $this->logger,
+ $this->folder,
+ $this->service,
+ $this->sequenceAnalyzer,
+ 'john'
+ );
+
+ $fileOperationWrite = new FileOperation();
+ $fileOperationWrite->setCommand(Monitor::WRITE);
+ $fileOperationWrite->setPath('/admin/files');
+ $fileOperationWrite->setOriginalName('test.jpg');
+
+ $this->service->method('find')
+ ->will($this->throwException(new \OCP\AppFramework\Db\DoesNotExistException('test')));
+
+ $result = $controller->recover(1);
+ $this->assertTrue($result instanceof JSONResponse);
+ $this->assertEquals($result->getStatus(), Http::STATUS_BAD_REQUEST);
+ }
+
+ public function testDeleteFromStorage()
+ {
+ $controller = new MonitoringController(
+ 'ransomware_detection',
+ $this->request,
+ $this->userSession,
+ $this->config,
+ $this->classifier,
+ $this->logger,
+ $this->folder,
+ $this->service,
+ $this->sequenceAnalyzer,
+ 'john'
+ );
+ $file = $this->createMock(File::class);
+ $file->method('isDeletable')
+ ->willReturn(true);
+
+ $file->method('delete');
+
+ $this->folder->expects($this->once())
+ ->method('get')
+ ->willReturn($file);
+ $this->assertTrue($this->invokePrivate($controller, 'deleteFromStorage', ['/admin/files/test.jpg']));
+ }
+
+ public function testDeleteFromStorageNotPossible()
+ {
+ $controller = new MonitoringController(
+ 'ransomware_detection',
+ $this->request,
+ $this->userSession,
+ $this->config,
+ $this->classifier,
+ $this->logger,
+ $this->folder,
+ $this->service,
+ $this->sequenceAnalyzer,
+ 'john'
+ );
+ $file = $this->createMock(File::class);
+
+ $file->method('isDeletable')
+ ->willReturn(false);
+
+ $file->method('delete');
+
+ $this->folder->expects($this->once())
+ ->method('get')
+ ->willReturn($file);
+ $this->assertFalse($this->invokePrivate($controller, 'deleteFromStorage', ['/admin/files/test.jpg']));
+ }
+
+ public function testDeleteFromStorageNotFoundException()
+ {
+ $controller = new MonitoringController(
+ 'ransomware_detection',
+ $this->request,
+ $this->userSession,
+ $this->config,
+ $this->classifier,
+ $this->logger,
+ $this->folder,
+ $this->service,
+ $this->sequenceAnalyzer,
+ 'john'
+ );
+ $folder = $this->createMock(Folder::class);
+
+ $this->folder->expects($this->once())
+ ->method('get')
+ ->will($this->throwException(new \OCP\Files\NotFoundException('test')));
+
+ $this->assertTrue($this->invokePrivate($controller, 'deleteFromStorage', ['/admin/files']));
+ }
+}
diff --git a/tests/Unit/Controller/ScanControllerTest.php b/tests/Unit/Controller/ScanControllerTest.php
new file mode 100644
index 0000000..28145f1
--- /dev/null
+++ b/tests/Unit/Controller/ScanControllerTest.php
@@ -0,0 +1,274 @@
+<?php
+
+/**
+ * @copyright Copyright (c) 2018 Matthias Held <matthias.held@uni-konstanz.de>
+ * @author Matthias Held <matthias.held@uni-konstanz.de>
+ * @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 <https://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\RansomwareDetection\tests\Unit\Controller;
+
+use OCA\RansomwareDetection\Monitor;
+use OCA\RansomwareDetection\Classifier;
+use OCA\RansomwareDetection\Analyzer\SequenceAnalyzer;
+use OCA\RansomwareDetection\Analyzer\SequenceResult;
+use OCA\RansomwareDetection\Analyzer\SequenceSizeAnalyzer;
+use OCA\RansomwareDetection\Analyzer\FileTypeFunnellingAnalyzer;
+use OCA\RansomwareDetection\Analyzer\EntropyFunnellingAnalyzer;
+use OCA\RansomwareDetection\Analyzer\EntropyAnalyzer;
+use OCA\RansomwareDetection\Analyzer\EntropyResult;
+use OCA\RansomwareDetection\Analyzer\FileCorruptionAnalyzer;
+use OCA\RansomwareDetection\Analyzer\FileNameAnalyzer;
+use OCA\RansomwareDetection\Analyzer\FileNameResult;
+use OCA\RansomwareDetection\AppInfo\Application;
+use OCA\RansomwareDetection\Controller\ScanController;
+use OCA\RansomwareDetection\Db\FileOperation;
+use OCA\RansomwareDetection\Service\FileOperationService;
+use OCA\RansomwareDetection\Scanner\StorageStructure;
+use OCA\RansomwareDetection\Entropy\Entropy;
+use OCP\Files\IRootFolder;
+use OCA\Files_Trashbin\Trashbin;
+use OCA\Files_Trashbin\Helper;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\AppFramework\OCSController;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\IConfig;
+use OCP\IUserSession;
+use OCP\IRequest;
+use OCP\IDBConnection;
+use OCP\ILogger;
+use Test\TestCase;
+
+class ScanControllerTest extends TestCase
+{
+ /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
+ protected $request;
+
+ /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */
+ protected $userSession;
+
+ /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */
+ protected $config;
+
+ /** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */
+ protected $logger;
+
+ /** @var Classifier|\PHPUnit_Framework_MockObject_MockObject */
+ protected $classifier;
+
+ /** @var Folder|\PHPUnit_Framework_MockObject_MockObject */
+ protected $folder;
+
+ /** @var FileOperationService|\PHPUnit_Framework_MockObject_MockObject */
+ protected $service;
+
+ /** @var SequenceAnalyzer|\PHPUnit_Framework_MockObject_MockObject */
+ protected $sequenceAnalyzer;
+
+ /** @var EntropyAnalyzer|\PHPUnit_Framework_MockObject_MockObject */
+ protected $entropyAnalyzer;
+
+ /** @var FileCorruptionAnalyzer|\PHPUnit_Framework_MockObject_MockObject */
+ protected $fileCorruptionAnalyzer;
+
+ /** @var FileNameAnalyzer|\PHPUnit_Framework_MockObject_MockObject */
+ protected $fileNameAnalyzer;
+
+ /** @var IDBConnection|\PHPUnit_Framework_MockObject_MockObject */
+ protected $connection;
+
+ /** @var string */
+ protected $userId = 'john';
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->request = $this->getMockBuilder('OCP\IRequest')
+ ->getMock();
+ $this->userSession = $this->getMockBuilder('OCP\IUserSession')
+ ->getMock();
+ $this->config = $this->getMockBuilder('OCP\IConfig')
+ ->getMock();
+ $this->logger = $this->getMockBuilder('OCP\ILogger')
+ ->getMock();
+ $this->folder = $this->getMockBuilder('OCP\Files\Folder')
+ ->getMock();
+ $this->connection = $this->getMockBuilder('OCP\IDBConnection')
+ ->getMock();
+ $mapper = $this->getMockBuilder('OCA\RansomwareDetection\Db\FileOperationMapper')
+ ->setConstructorArgs([$this->connection])
+ ->getMock();
+ $this->service = $this->getMockBuilder('OCA\RansomwareDetection\Service\FileOperationService')
+ ->setConstructorArgs([$mapper, $this->userId])
+ ->getMock();
+ $this->classifier = $this->getMockBuilder('OCA\RansomwareDetection\Classifier')
+ ->setConstructorArgs([$this->logger, $mapper, $this->service])
+ ->getMock();
+ $sequenceSizeAnalyzer = $this->getMockBuilder('OCA\RansomwareDetection\Analyzer\SequenceSizeAnalyzer')
+ ->getMock();
+ $fileTypeFunnellingAnalyzer = $this->getMockBuilder('OCA\RansomwareDetection\Analyzer\FileTypeFunnellingAnalyzer')
+ ->getMock();
+ $entropyFunnellingAnalyzer = $this->getMockBuilder('OCA\RansomwareDetection\Analyzer\EntropyFunnellingAnalyzer')
+ ->setConstructorArgs([$this->logger])
+ ->getMock();
+ $this->sequenceAnalyzer = $this->getMockBuilder('OCA\RansomwareDetection\Analyzer\SequenceAnalyzer')
+ ->setConstructorArgs([$sequenceSizeAnalyzer, $fileTypeFunnellingAnalyzer, $entropyFunnellingAnalyzer])
+ ->setMethods(['analyze'])
+ ->getMock();
+ $rootFolder = $this->createMock(IRootFolder::class);
+ $entropy = $this->createMock(Entropy::class);
+ $this->entropyAnalyzer = $this->getMockBuilder('OCA\RansomwareDetection\Analyzer\EntropyAnalyzer')
+ ->setConstructorArgs([$this->logger, $rootFolder, $entropy, $this->userId])
+ ->getMock();
+ $this->fileCorruptionAnalyzer = $this->getMockBuilder('OCA\RansomwareDetection\Analyzer\FileCorruptionAnalyzer')
+ ->setConstructorArgs([$this->logger, $rootFolder, $this->userId])
+ ->getMock();
+ $this->fileNameAnalyzer = $this->getMockBuilder('OCA\RansomwareDetection\Analyzer\FileNameAnalyzer')
+ ->setConstructorArgs([$this->logger, $entropy])
+ ->getMock();
+ }
+
+ public function dataRecover()
+ {
+ return [
+ ['id' => 4, 'command' => Monitor::DELETE, 'path' => '/test.pdf', 'timestamp' => 12345, 'restored' => false, 'response' => Http::STATUS_BAD_REQUEST],
+ ['id' => 4, 'command' => Monitor::DELETE, 'path' => '/test.pdf', 'timestamp' => 12345, 'restored' => true, 'response' => Http::STATUS_OK],
+ ['id' => 4, 'command' => Monitor::WRITE, 'path' => '/test.pdf', 'timestamp' => 12345, 'restored' => true, 'response' => Http::STATUS_OK],
+ ['id' => 4, 'command' => Monitor::WRITE, 'path' => '/test.pdf', 'timestamp' => 12345, 'restored' => false, 'response' => Http::STATUS_BAD_REQUEST],
+ ['id' => 4, 'command' => Monitor::CREATE, 'path' => '/test.pdf', 'timestamp' => 12345, 'restored' => false, 'response' => Http::STATUS_BAD_REQUEST],
+ ['id' => 4, 'command' => Monitor::RENAME, 'path' => '/test.pdf', 'timestamp' => 12345, 'restored' => false, 'response' => Http::STATUS_BAD_REQUEST],
+ ];
+ }
+
+ /**
+ * @dataProvider dataRecover
+ *
+ * @param integer $id
+ * @param integer $command
+ * @param string $path
+ * @param integer $timestamp
+ * @param boolean $restored
+ * @param HttpResponse $response
+ */
+ public function testRecover($id, $command, $path, $timestamp, $restored, $response)
+ {
+ $controller = $this->getMockBuilder(ScanController::class)
+ ->setConstructorArgs(['ransomware_detection', $this->request, $this->userSession, $this->config, $this->classifier,
+ $this->logger, $this->folder, $this->service, $this->sequenceAnalyzer, $this->entropyAnalyzer,
+ $this->fileCorruptionAnalyzer, $this->fileNameAnalyzer, $this->connection, $this->userId])
+ ->setMethods(['deleteFromStorage', 'restoreFromTrashbin'])
+ ->getMock();
+
+ $controller->expects($this->any())
+ ->method('deleteFromStorage')
+ ->willReturn($restored);
+
+ $controller->expects($this->any())
+ ->method('restoreFromTrashbin')
+ ->willReturn($restored);
+
+ $result = $controller->recover($id, $command, $path ,$timestamp);
+ $this->assertTrue($result instanceof JSONResponse);
+ $this->assertEquals($result->getStatus(), $response);
+ }
+
+ public function testFilesToScan()
+ {
+ $controller = $this->getMockBuilder(ScanController::class)
+ ->setConstructorArgs(['ransomware_detection', $this->request, $this->userSession, $this->config, $this->classifier,
+ $this->logger, $this->folder, $this->service, $this->sequenceAnalyzer, $this->entropyAnalyzer,
+ $this->fileCorruptionAnalyzer, $this->fileNameAnalyzer, $this->connection, $this->userId])
+ ->setMethods(['getStorageStructure', 'getTrashStorageStructure', 'getLastActivity'])
+ ->getMock();
+
+ $controller->expects($this->any())
+ ->method('getStorageStructure')
+ ->willReturn(new StorageStructure());
+
+ $controller->expects($this->any())
+ ->method('getTrashStorageStructure')
+ ->willReturn(new StorageStructure());
+
+ $controller->expects($this->any())
+ ->method('getLastActivity')
+ ->willReturn(123);
+
+ $result = $controller->filesToScan();
+ $this->assertTrue($result instanceof JSONResponse);
+ $this->assertEquals($result->getStatus(), Http::STATUS_ACCEPTED);
+ }
+
+ public function dataScanSequence()
+ {
+ $fileOperation1 = new FileOperation();
+ $fileOperation1->setCommand(Monitor::WRITE);
+ $fileOperation1->setOriginalName('test.csv');
+ $fileOperation1->setNewName('test.csv');
+ $fileOperation1->setPath('files/test.csv');
+ $fileOperation1->setSize(123000);
+ $fileOperation1->setType('file');
+ $fileOperation1->setMimeType('pdf');
+ $fileOperation1->setCorrupted(1);
+ $fileOperation1->setTimestamp(123);
+ $fileOperation1->setSequence(1);
+ $fileOperation1->setEntropy(7.9);
+ $fileOperation1->setStandardDeviation(0.1);
+ $fileOperation1->setFileNameEntropy(4.0);
+ $fileOperation1->setFileClass(EntropyResult::NORMAL);
+ $fileOperation1->setFileNameClass(FileNameResult::NORMAL);
+ $fileOperation1->setSuspicionClass(Classifier::HIGH_LEVEL_OF_SUSPICION);
+
+ $sequenceResult = new SequenceResult(1, 0.0, 1.1, 2.2, 4.5, []);
+
+ return [
+ ['sequence' => [], 'fileOperation' => new FileOperation(), 'sequenceResult' => $sequenceResult,'response' => Http::STATUS_BAD_REQUEST],
+ ['sequence' => [['timestamp' => 123]], 'fileOperation' => $fileOperation1, 'sequenceResult' => $sequenceResult, 'response' => Http::STATUS_OK]
+ ];
+ }
+
+ /**
+ * @dataProvider dataScanSequence
+ *
+ * @param array $sequence
+ * @param sequenceResult $sequenceResult
+ * @param FileOperation $fileOperation
+ * @param HttpResponse $response
+ */
+ public function testScanSequence($sequence, $fileOperation, $sequenceResult, $response)
+ {
+ $controller = $this->getMockBuilder(ScanController::class)
+ ->setConstructorArgs(['ransomware_detection', $this->request, $this->userSession, $this->config, $this->classifier,
+ $this->logger, $this->folder, $this->service, $this->sequenceAnalyzer, $this->entropyAnalyzer,
+ $this->fileCorruptionAnalyzer, $this->fileNameAnalyzer, $this->connection, $this->userId])
+ ->setMethods(['getLastActivity', 'buildFileOperation'])
+ ->getMock();
+
+ $controller->expects($this->any())
+ ->method('buildFileOperation')
+ ->willReturn($fileOperation);
+
+ $this->sequenceAnalyzer->expects($this->any())
+ ->method('analyze')
+ ->willReturn($sequenceResult);
+
+ $result = $controller->scanSequence($sequence);
+ $this->assertTrue($result instanceof JSONResponse);
+ $this->assertEquals($result->getStatus(), $response);
+ }
+}
diff --git a/tests/Unit/Scanner/StorageStructureTest.php b/tests/Unit/Scanner/StorageStructureTest.php
new file mode 100644
index 0000000..5d41f36
--- /dev/null
+++ b/tests/Unit/Scanner/StorageStructureTest.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @copyright Copyright (c) 2018 Matthias Held <matthias.held@uni-konstanz.de>
+ * @author Matthias Held <matthias.held@uni-konstanz.de>
+ * @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 <https://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\RansomwareDetection\tests\Unit\Scanner;
+
+use OCA\RansomwareDetection\Scanner\StorageStructure;
+use Test\TestCase;
+
+class StorageStructureTest extends TestCase
+{
+ /** @var StorageStructure */
+ protected $storageStructure;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->storageStructure = new StorageStructure();
+ }
+
+ public function testDefaultParameters() {
+ $this->assertEquals($this->storageStructure->getNumberOfFiles(), 0);
+ $this->assertEquals($this->storageStructure->getFiles(), []);
+ }
+
+ public function testGetNumberOfFiles() {
+ $this->assertEquals($this->storageStructure->getNumberOfFiles(), 0);
+ }
+
+ public function testSetNumberOfFiles() {
+ $this->storageStructure->setNumberOfFiles(10);
+ $this->assertEquals($this->storageStructure->getNumberOfFiles(), 10);
+ }
+
+ public function testIncreaseNumberOfFiles() {
+ $this->storageStructure->increaseNumberOfFiles();
+ $this->assertEquals($this->storageStructure->getNumberOfFiles(), 1);
+ }
+
+ public function testGetFiles() {
+ $this->assertEquals($this->storageStructure->getFiles(), []);
+ }
+
+ public function testSetFiles() {
+ $this->storageStructure->setFiles([10]);
+ $this->assertEquals($this->storageStructure->getFiles(), [10]);
+ }
+
+ public function testAddFiles() {
+ $this->storageStructure->addFile(11);
+ $this->storageStructure->addFile(10);
+ $this->assertEquals($this->storageStructure->getFiles(), [11, 10]);
+ }
+}