From bd4e9e0fb268438e8f21369ad971613823e41879 Mon Sep 17 00:00:00 2001 From: Matthias Held Date: Tue, 24 Jul 2018 17:21:56 +0200 Subject: Add storage scanner --- js/app.js | 33 +++- js/filelist.js | 2 - js/scan.js | 522 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 549 insertions(+), 8 deletions(-) create mode 100644 js/scan.js (limited to 'js') 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/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 = $('
'); - } else { - header = $('
') } 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 + * + * @author Matthias Held + * + * @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 . + */ + +(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. + */ + _selectedFiles: {}, + + /** + * Map of files in the current folder. + * The entries are of file data. + * + * @type 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 = $(''); + } else { + header = $('
Start scan
Sequences scanned: 0/' + numberOfSequences + '
') + } + 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 = $('
'); + return section; + }, + + /** + * Creates the section. + */ + _createSection: function(sequence) { + var section = $('
'); + return section; + }, + + /** + * All files recovered text. + */ + _createAllFilesRecovered: function() { + var text = $('

' + t('ransomware_detection', 'All files successfully recovered.') + '

'); + return text; + }, + + /** + * No suspicious files found text. + */ + _createNoSuspiciousFilesFound: function() { + var text = $('

' + t('ransomware_detection', 'No suspicious files found.') + '

'); + 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 = + $(''); + return table; + }, + + /** + * Creates a new row in the table. + */ + _createFileRow: function(fileData, sequence) { + var self = this; + var td, 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 = $('').attr({ "class": "selection"}); + td.append('' + + ''); + 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 = $('').addClass('name-text'); + var innernameSpan = $('').addClass('inner-name-text').text(basename); + + nameSpan.append(innernameSpan); + + if (extension) { + nameSpan.append($('').addClass('extension').text(extension)); + } + } else { + nameSpan = $('').addClass('name-text'); + innernameSpan = $('').addClass('inner-name-text').text(t('ransomware_detection', 'Not found.')); + + nameSpan.append(innernameSpan); + } + + td = $('').attr({ "class": "file-name"}); + td.append(nameSpan); + tr.append(td); + + if (fileData.command === 1) { + // delete + td = $('').append($('

').attr({"title": "DELETE"}).tooltip({placement: 'top'}).prepend('')); + } else if (fileData.command === 2) { + // rename + td = $('').append($('

').attr({"title": "RENAME"}).tooltip({placement: 'top'}).prepend('')); + } else if (fileData.command === 3) { + // write + td = $('').append($('

').attr({"title": "WRITE"}).tooltip({placement: 'top'}).prepend('')); + } else if (fileData.command === 4) { + // read + td = $('').append($('

').attr({"title": "READ"}).tooltip({placement: 'top'}).prepend('')); + } else if (fileData.command === 5) { + // create + td = $('').append($('

').attr({"title": "CREATE"}).tooltip({placement: 'top'}).prepend('')); + } else { + // error + td = $('').append($('

').attr({"title": "ERROR"}).tooltip({placement: 'top'}).prepend('')); + } + 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 = $('').append($('

').attr({ + "class": "filesize" + }).text(simpleSize)); + tr.append(td); + + if (fileData.fileClass === 1) { + // encrypted + td = $('').append($('

').attr({"title": "ENCRYPTED"}).tooltip({placement: 'top'}).prepend('')); + } else if (fileData.fileClass === 2) { + // compressed + td = $('').append($('

').attr({"title": "COMPRESSED"}).tooltip({placement: 'top'}).prepend('')); + } else if (fileData.fileClass === 3) { + // normal + td = $('').append($('

').attr({"title": "NORMAL"}).tooltip({placement: 'top'}).prepend('')); + } else { + // error + td = $('').append($('

').attr({"title": "ERROR"}).tooltip({placement: 'top'}).prepend('')); + } + tr.append(td); + + if (fileData.fileNameClass === 0) { + // normal + td = $('').append($('

').attr({"title": "NORMAL"}).tooltip({placement: 'top'}).prepend('')); + } else if (fileData.fileNameClass === 1) { + // suspicious + td = $('').append($('

').attr({"title": "SUSPICIOUS FILE EXTENSION"}).tooltip({placement: 'top'}).prepend('')); + } else if (fileData.fileNameClass === 2) { + // suspicious + td = $('').append($('

').attr({"title": "SUSPICIOUS FILE NAME"}).tooltip({placement: 'top'}).prepend('')); + } else if (fileData.fileNameClass === 3) { + // suspicious + td = $('').append($('

').attr({"title": "SUSPICIOUS"}).tooltip({placement: 'top'}).prepend('')); + } else { + // error + td = $('').append($('

').attr({"title": "ERROR"}).tooltip({placement: 'top'}).prepend('')); + } + 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 = $('').attr({ "class": "date" }); + td.append($('').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() {}); -- cgit v1.2.3