diff options
author | Matthias Held <ilovemilk@wusa.io> | 2018-07-24 18:21:56 +0300 |
---|---|---|
committer | Matthias Held <ilovemilk@wusa.io> | 2018-07-24 18:21:56 +0300 |
commit | bd4e9e0fb268438e8f21369ad971613823e41879 (patch) | |
tree | 7be80df8138e4050f579e99a1032afc648eda87f | |
parent | 3fbaae5aa2026ee893f7b4a9b9fc53ca9ff35b76 (diff) |
Add storage scanner
-rw-r--r-- | CHANGELOG.md | 12 | ||||
-rw-r--r-- | appinfo/routes.php | 22 | ||||
-rw-r--r-- | css/style.scss | 12 | ||||
-rw-r--r-- | js/app.js | 33 | ||||
-rw-r--r-- | js/filelist.js | 2 | ||||
-rw-r--r-- | js/scan.js | 522 | ||||
-rw-r--r-- | lib/Analyzer/EntropyAnalyzer.php | 1 | ||||
-rw-r--r-- | lib/AppInfo/Application.php | 17 | ||||
-rw-r--r-- | lib/Controller/BasicController.php | 107 | ||||
-rw-r--r-- | lib/Controller/MonitoringController.php | 324 | ||||
-rw-r--r-- | lib/Controller/RecoverController.php | 13 | ||||
-rw-r--r-- | lib/Controller/ScanController.php | 416 | ||||
-rw-r--r-- | lib/Scanner/StorageStructure.php | 97 | ||||
-rw-r--r-- | templates/index.php | 18 | ||||
-rw-r--r-- | templates/scan.php | 52 | ||||
-rw-r--r-- | tests/Unit/Controller/BasicControllerTest.php | 101 | ||||
-rw-r--r-- | tests/Unit/Controller/MonitoringControllerTest.php | 341 | ||||
-rw-r--r-- | tests/Unit/Controller/ScanControllerTest.php | 274 | ||||
-rw-r--r-- | tests/Unit/Scanner/StorageStructureTest.php | 72 |
19 files changed, 2405 insertions, 31 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; +} @@ -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/filelist.js b/js/filelist.js index d63cf6c..c7389dc 100644 --- a/js/filelist.js +++ b/js/filelist.js @@ -250,8 +250,6 @@ _createAppHeader: function() { 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"></div>') } return header; }, 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]); + } +} |