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

github.com/undo-ransomware/ransomware_detection.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMatthias <ilovemilk@wusa.io>2020-04-10 13:25:49 +0300
committerMatthias <ilovemilk@wusa.io>2020-04-10 13:25:49 +0300
commit133a820b88a78a4281e017d88d11bed169b5fdbf (patch)
tree0bd785eb085699b18a8283891248d77b194f5c12 /src
parent0a92726e3f3d082ad87101dfb9819e736bcd9df7 (diff)
add first vuejs ui version
Diffstat (limited to 'src')
-rw-r--r--src/App.vue56
-rw-r--r--src/components/Action.vue62
-rw-r--r--src/components/FileOperationsTable.vue86
-rw-r--r--src/components/Header.vue43
-rw-r--r--src/components/ProtectionStatus.vue117
-rw-r--r--src/components/RecoverAction.vue34
-rw-r--r--src/components/ServiceStatus.vue92
-rw-r--r--src/css/global.css19
-rw-r--r--src/main.js40
-rw-r--r--src/router.js31
-rw-r--r--src/views/History.vue123
-rw-r--r--src/views/Protection.vue85
-rw-r--r--src/views/Recover.vue153
-rw-r--r--src/webcomponents/ransomware-icons.js34
14 files changed, 975 insertions, 0 deletions
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..9456891
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,56 @@
+<template>
+ <Content app-name="ransomware_detection">
+ <AppNavigation>
+ <ul>
+ <AppNavigationItem v-for="item in menu" :key="item.key" :item="item" />
+ </ul>
+ </AppNavigation>
+ <router-view />
+ </Content>
+</template>
+
+<script>
+import Content from 'nextcloud-vue/dist/Components/Content'
+import AppNavigation from 'nextcloud-vue/dist/Components/AppNavigation'
+import AppNavigationItem from 'nextcloud-vue/dist/Components/AppNavigationItem'
+import AppContent from 'nextcloud-vue/dist/Components/AppContent'
+
+export default {
+ name: 'App',
+ components: {
+ Content,
+ AppNavigation,
+ AppNavigationItem,
+ AppContent,
+ },
+ computed: {
+ menu() {
+ return [
+ {
+ id: 'app-category-protection',
+ classes: [],
+ router: {name: 'protection'},
+ icon: 'icon-dashboard',
+ text: t('ransomware_detection', 'Protection'),
+ },
+ {
+ id: 'app-category-recover',
+ classes: [],
+ icon: 'icon-trash',
+ router: {name: 'recover'},
+ text: t('ransomware_detection', 'Recover'),
+ }, {
+ id: 'app-category-history',
+ classes: [],
+ icon: 'icon-hourglass',
+ router: {name: 'history'},
+ text: t('ransomware_detection', 'History'),
+ }
+ ];
+ }
+ }
+}
+</script>
+
+<style scoped>
+</style> \ No newline at end of file
diff --git a/src/components/Action.vue b/src/components/Action.vue
new file mode 100644
index 0000000..2cf8766
--- /dev/null
+++ b/src/components/Action.vue
@@ -0,0 +1,62 @@
+<template>
+ <button class="action-button pull-right"
+ :data-type="type" :data-href="link" @click="onClickActionButton">
+ <iron-icon icon="undo"></iron-icon> {{ label }}
+ </button>
+</template>
+
+<script>
+import axios from 'nextcloud-axios'
+import '@polymer/iron-icon/iron-icon.js';
+import '@polymer/iron-icons/iron-icons.js';
+
+export default {
+ name: 'Action',
+
+ props: {
+ label: {
+ type: String,
+ default: '',
+ required: true
+ },
+ link: {
+ type: String,
+ default: '',
+ required: true
+ },
+ type: {
+ type: String,
+ default: '',
+ required: true
+ },
+ data: {
+ type: Object,
+ default: {},
+ required: false
+ }
+ },
+
+ methods: {
+ onClickActionButton: function() {
+ axios({
+ method: this.type || 'GET',
+ url: this.link
+ })
+ .then(() => {
+ this.$parent._$el.fadeOut(OC.menuSpeed)
+ this.$parent.$emit('remove')
+ $('body').trigger(new $.Event('OCA.Notification.Action', {
+ notification: this.$parent,
+ action: {
+ url: this.link,
+ type: this.type || 'GET'
+ }
+ }))
+ })
+ .catch(() => {
+ OC.Notification.showTemporary(t('notifications', 'Failed to perform action'))
+ });
+ }
+ }
+}
+</script> \ No newline at end of file
diff --git a/src/components/FileOperationsTable.vue b/src/components/FileOperationsTable.vue
new file mode 100644
index 0000000..c5cc5b4
--- /dev/null
+++ b/src/components/FileOperationsTable.vue
@@ -0,0 +1,86 @@
+<template>
+ <vaadin-grid theme="row-dividers" column-reordering-allowed multi-sort :items.prop="fileOperations">
+ <vaadin-grid-selection-column auto-select frozen></vaadin-grid-selection-column>
+ <vaadin-grid-column width="5em" flex-grow="0" id="status" header="Status"></vaadin-grid-column>
+ <vaadin-grid-sort-column width="9em" path="originalName" header="Name"></vaadin-grid-sort-column>
+ <vaadin-grid-sort-column width="9em" path="timestamp" id="time" header="GeƤndert"></vaadin-grid-sort-column>
+ </vaadin-grid>
+</template>
+
+<script>
+import '@vaadin/vaadin-grid/vaadin-grid.js';
+import '@vaadin/vaadin-grid/vaadin-grid-selection-column.js';
+import '@vaadin/vaadin-grid/vaadin-grid-sort-column.js';
+import '@vaadin/vaadin-grid/vaadin-grid-column.js';
+import '@polymer/iron-icon/iron-icon.js';
+import '@polymer/iron-icons/iron-icons.js';
+import '../webcomponents/ransomware-icons'
+import 'time-elements/dist/time-elements';
+
+export default {
+ name: 'FileOperationsTable',
+ data() {
+ return {
+ fileOperations: this.items
+ }
+ },
+ props: {
+ data: {
+ type: Array,
+ required: true
+ }
+ },
+ watch: {
+ data: {
+ immediate: true,
+ handler (newVal, oldVal) {
+ this.fileOperations = newVal;
+ this.$emit('table-state-changed');
+ if (oldVal !== undefined) {
+ document.querySelector('vaadin-grid').clearCache();
+ document.querySelector('vaadin-grid vaadin-grid-selection-column').selectAll = false;
+ }
+ }
+ }
+ },
+ mounted () {
+ document.querySelector('#status').renderer = (root, grid, rowData) => {
+ const icon = document.createElement('iron-icon');
+ switch (rowData.item.status) {
+ case 0:
+ icon.setAttribute('icon', 'ransomware:timelapse');
+ icon.style = "color: blue;";
+ break;
+ case 1:
+ icon.setAttribute('icon', 'verified-user');
+ icon.style = "color: green;";
+ break;
+ case 2:
+ icon.setAttribute('icon', 'error');
+ icon.style = "color: red;";
+ break;
+ default:
+ icon.setAttribute('icon', 'ransomware:timelapse');
+ icon.style = "color: blue;";
+ break;
+ }
+ root.innerHTML = '';
+ root.appendChild(icon);
+ }
+
+ document.querySelector('#time').renderer = (root, grid, rowData) => {
+ const localTime = document.createElement('local-time');
+ localTime.setAttribute('datetime', moment.unix(rowData.item.timestamp).format("YYYY-MM-DDTHH:mm:ss.SSS"));
+ localTime.textContent = moment.unix(rowData.item.timestamp).format('dddd, MMMM Do YYYY, HH:mm:ss');
+ root.innerHTML = '';
+ root.appendChild(localTime);
+ }
+ }
+}
+</script>
+
+<style scoped>
+ vaadin-grid {
+ border: none;
+ }
+</style> \ No newline at end of file
diff --git a/src/components/Header.vue b/src/components/Header.vue
new file mode 100644
index 0000000..456d065
--- /dev/null
+++ b/src/components/Header.vue
@@ -0,0 +1,43 @@
+<template>
+ <div class="header">
+ <h2>
+ {{this.header}}
+ </h2>
+ <div class="actions">
+ <slot/>
+ </div>
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'Header',
+ props: {
+ header: {
+ type: String,
+ default: '',
+ required: true
+ }
+ }
+}
+</script>
+
+<style scoped>
+ h2 {
+ height: 100%;
+ margin: 0px;
+ padding: 10px;
+ }
+ .actions {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ padding: 0px 10px 0px 10px;
+ }
+ .header {
+ height: 50px;
+ display: flex;
+ justify-content: space-between;
+ align-items: stretch;
+ }
+</style> \ No newline at end of file
diff --git a/src/components/ProtectionStatus.vue b/src/components/ProtectionStatus.vue
new file mode 100644
index 0000000..62e4805
--- /dev/null
+++ b/src/components/ProtectionStatus.vue
@@ -0,0 +1,117 @@
+<template>
+ <div class="container" v-bind:class="[protection && !detection? 'good' : 'bad']">
+ <h1>
+ <span v-if="protection && !detection"><iron-icon icon="ransomware:shield"></iron-icon> Your files are protected against destruction by ransomware.</span>
+ <span v-if="!protection"><iron-icon icon="error"></iron-icon> Your files are not protected. One service is not working properly.</span>
+ <span v-if="protection && detection"><iron-icon icon="ransomware:locked"></iron-icon> Ransomware attack detected.</span>
+ </h1>
+ <paper-button class="recover-button" @click="$router.push('recover')" v-if="protection && detection"><iron-icon icon="undo"></iron-icon>Recover</paper-button>
+ </div>
+</template>
+
+<script>
+import '@polymer/paper-button/paper-button.js';
+import '@polymer/iron-icon/iron-icon.js';
+import '@polymer/iron-icons/iron-icons.js';
+import '../webcomponents/ransomware-icons'
+import axios from 'nextcloud-axios'
+
+export default {
+ name: 'ProtectionStatus',
+ props: {
+ protectionLink: {
+ type: String,
+ default: '',
+ required: true
+ },
+ detectionLink: {
+ type: String,
+ default: '',
+ required: true
+ }
+ },
+ created() {
+ this.fetchServicesStatus();
+ this.fetchDetectionStatus();
+ },
+ data() {
+ return {
+ detection: 0,
+ protection: 0
+ };
+ },
+ methods: {
+ fetchServicesStatus() {
+ axios({
+ method: 'GET',
+ url: this.protectionLink
+ })
+ .then(json => {
+ this.protection = 1;
+ for (i = 0; i < json.data.length; i++) {
+ if (json.data[i].status == 0) {
+ this.protection = 0;
+ }
+ }
+ this.$emit('protection-state-changed');
+ })
+ .catch( error => { console.error(error); });
+ },
+ fetchDetectionStatus() {
+ axios({
+ method: 'GET',
+ url: this.detectionLink
+ })
+ .then(json => {
+ this.detection = 0;
+ if (json.data.length > 0) {
+ this.detection = 1;
+ }
+ })
+ .catch( error => { console.error(error); });
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+ .container {
+ h1 {
+ height: calc(100% - 52px);
+ color: #fff;
+ line-height: 48px;
+ display: flex;
+ align-items: center;
+ font-size: 32px;
+ iron-icon {
+ width: 48px;
+ height: 48px;
+ }
+ span {
+ vertical-align: middle;
+ }
+ }
+
+ width: 100%;
+ height: 100%;
+ box-shadow: none;
+ color: #fff;
+ padding: 0px 10px 0px 10px;
+ &.good {
+ background-color: #18b977;
+ }
+ &.bad {
+ background-color: #e2523d;
+ }
+ }
+
+ .recover-button {
+ display: flex;
+ border: 1px solid #fff;
+ }
+
+ .recover-button:hover {
+ background-color: #fff;
+ color: #c00;
+ }
+</style> \ No newline at end of file
diff --git a/src/components/RecoverAction.vue b/src/components/RecoverAction.vue
new file mode 100644
index 0000000..515b83b
--- /dev/null
+++ b/src/components/RecoverAction.vue
@@ -0,0 +1,34 @@
+<template>
+ <button class="action-button pull-right" @click="onClickActionButton">
+ <iron-icon icon="ransomware:trash"></iron-icon> {{ label }}
+ </button>
+</template>
+
+<script>
+import '@polymer/iron-icon/iron-icon.js';
+import '@polymer/iron-icons/iron-icons.js';
+import '../webcomponents/ransomware-icons'
+
+export default {
+ name: 'RecoverAction',
+
+ props: {
+ label: {
+ type: String,
+ default: '',
+ required: true
+ }
+ },
+ methods: {
+ onClickActionButton() {
+ this.$emit("recover");
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+ button {
+ --border-radius-pill: 0px;
+ }
+</style> \ No newline at end of file
diff --git a/src/components/ServiceStatus.vue b/src/components/ServiceStatus.vue
new file mode 100644
index 0000000..9c005b5
--- /dev/null
+++ b/src/components/ServiceStatus.vue
@@ -0,0 +1,92 @@
+<template>
+ <div>
+ <h2 class="container">
+ <div class="item name">{{ serviceName }}</div>
+ <div v-if="serviceStatus" class="item status active">Active</div>
+ <div v-if="!serviceStatus" class="item status offline">Offline</div>
+ </h2>
+ <div v-if="!serviceStatus" class="description">{{description}}</div>
+ </div>
+</template>
+
+<script>
+import '@polymer/paper-card/paper-card.js';
+import '@polymer/paper-button/paper-button.js';
+import '@polymer/iron-icon/iron-icon.js';
+import '@polymer/iron-icons/iron-icons.js';
+import axios from 'nextcloud-axios'
+
+export default {
+ name: 'ServiceStatus',
+ props: {
+ link: {
+ type: String,
+ default: '',
+ required: true
+ },
+ description: {
+ type: String,
+ default: '',
+ required: true
+ }
+ },
+ data() {
+ return {
+ serviceName: "Not available.",
+ serviceStatus: 0
+ };
+ },
+ created () {
+ this.fetchServiceName();
+ this.fetchServiceStatus();
+ },
+ methods: {
+ fetchServiceName: function() {
+ axios({
+ method: 'GET',
+ url: this.link
+ })
+ .then(json => {
+ this.serviceName = json.data.name;
+ })
+ .catch( error => { console.error(error); });
+ },
+ fetchServiceStatus() {
+ axios({
+ method: 'GET',
+ url: this.link
+ })
+ .then(json => {
+ this.serviceStatus = json.data.status;
+ this.$emit('service-state-changed');
+ })
+ .catch( error => { console.error(error); });
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+ .container {
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ }
+ h2 {
+ margin: 0px;
+ }
+ .item {
+ padding: 5px 10px 5px 10px;
+ }
+ .description {
+ color: #9b9b9b;
+ padding: 0px 10px 0px 10px;
+ }
+ .active {
+ color: #18b977;
+ }
+ .offline {
+ color: #e2523d;
+ }
+
+</style> \ No newline at end of file
diff --git a/src/css/global.css b/src/css/global.css
new file mode 100644
index 0000000..8ef68df
--- /dev/null
+++ b/src/css/global.css
@@ -0,0 +1,19 @@
+.icon-shield {
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgd2lkdGg9IjI0IgogICBoZWlnaHQ9IjI0IgogICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2ZzYiCiAgIHNvZGlwb2RpOmRvY25hbWU9ImxvY2stc2hpZWxkLnN2ZyIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC45Mi4zICgyNDA1NTQ2LCAyMDE4LTAzLTExKSI+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhMTIiPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgICAgPGRjOnRpdGxlIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxkZWZzCiAgICAgaWQ9ImRlZnMxMCIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgb2JqZWN0dG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBndWlkZXRvbGVyYW5jZT0iMTAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjY0MCIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI0ODAiCiAgICAgaWQ9Im5hbWVkdmlldzgiCiAgICAgc2hvd2dyaWQ9ImZhbHNlIgogICAgIGlua3NjYXBlOnpvb209IjE1LjczMzEyNiIKICAgICBpbmtzY2FwZTpjeD0iMjguNDM2MzgyIgogICAgIGlua3NjYXBlOmN5PSIzNS4wNzIzNzIiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnNiIgLz4KICA8cGF0aAogICAgIGQ9Ik0wIDBoMjR2MjRIMHoiCiAgICAgZmlsbD0ibm9uZSIKICAgICBpZD0icGF0aDIiIC8+CiAgPHBhdGgKICAgICBkPSJNIDEyIDEgTCAzIDUgTCAzIDExIEMgMyAxNi41NSA2Ljg0IDIxLjc0IDEyIDIzIEMgMTcuMTYgMjEuNzQgMjEgMTYuNTUgMjEgMTEgTCAyMSA1IEwgMTIgMSB6IE0gMTIgMy45NjA5Mzc1IEMgMTMuMTY1NDk1IDMuOTYwOTM3NSAxNC4xNjk1NTcgNC4zNzYwMjU4IDE0Ljc4NTE1NiA1LjE2MDE1NjIgQyAxNS40MDA3NTUgNS45NDQyODY2IDE1LjYxNzE4OCA2Ljk3NjE2NzkgMTUuNjE3MTg4IDguMTQyNTc4MSBMIDE1LjYxNzE4OCA5LjU1NDY4NzUgTCAxNy4zNTE1NjIgOS41NTQ2ODc1IEMgMTcuNTE0MDk2IDkuNTU0Njg3NSAxNy42NDQ1MzEgOS42ODUxMjMgMTcuNjQ0NTMxIDkuODQ3NjU2MiBMIDE3LjY0NDUzMSAxNi4zMTY0MDYgQyAxNy42NDQ1MzEgMTYuNDc4OTM5IDE3LjUxNDA5NiAxNi42MTEzMjggMTcuMzUxNTYyIDE2LjYxMTMyOCBMIDYuNjQ4NDM3NSAxNi42MTEzMjggQyA2LjQ4NTkwNDMgMTYuNjExMzI4IDYuMzU1NDY4OCAxNi40Nzg5MzkgNi4zNTU0Njg4IDE2LjMxNjQwNiBMIDYuMzU1NDY4OCA5Ljg0NzY1NjIgQyA2LjM1NTQ2ODcgOS42ODUxMjMgNi40ODU5MDQzIDkuNTU0Njg3NSA2LjY0ODQzNzUgOS41NTQ2ODc1IEwgOC4zODI4MTI1IDkuNTU0Njg3NSBMIDguMzgyODEyNSA4LjE0MjU3ODEgQyA4LjM4MjgxMjUgNi45NzYxNjc5IDguNTk5MjQ1IDUuOTQ0Mjg2NiA5LjIxNDg0MzggNS4xNjAxNTYyIEMgOS44MzA0NDI1IDQuMzc2MDI1OCAxMC44MzQ1MDYgMy45NjA5Mzc1IDEyIDMuOTYwOTM3NSB6IE0gMTIgNS41NDg4MjgxIEMgMTEuMTY4MzUxIDUuNTQ4ODI4MSAxMC43NjEzIDUuNzYzMDA4OCAxMC40NjQ4NDQgNi4xNDA2MjUgQyAxMC4xNjgzODcgNi41MTgyNDIzIDkuOTcwNzAzMSA3LjE4MTU0MDMgOS45NzA3MDMxIDguMTQyNTc4MSBMIDkuOTcwNzAzMSA5LjU1NDY4NzUgTCAxNC4wMjkyOTcgOS41NTQ2ODc1IEwgMTQuMDI5Mjk3IDguMTQyNTc4MSBDIDE0LjAyOTI5NyA3LjE4MTU0MDMgMTMuODMxNjEzIDYuNTE4MjQyNyAxMy41MzUxNTYgNi4xNDA2MjUgQyAxMy4yMzg3IDUuNzYzMDA5MSAxMi44MzE2NSA1LjU0ODgyODEgMTIgNS41NDg4MjgxIHogTSAxMS42MTcxODggMTAuMTYwMTU2IEwgMTEuNjE3MTg4IDEwLjg3MzA0NyBDIDExLjQyMzQyNCAxMC45MDgyNzYgMTEuMjU2MTUzIDEwLjk2NTY1NSAxMS4xMTUyMzQgMTEuMDQ0OTIyIEMgMTAuOTc0MzE1IDExLjExOTc4NSAxMC44NTk1NTkgMTEuMjEyMTczIDEwLjc3MTQ4NCAxMS4zMjIyNjYgQyAxMC42ODM0MSAxMS40MzIzNTkgMTAuNjE2MzAzIDExLjU1NDg5MSAxMC41NzIyNjYgMTEuNjkxNDA2IEMgMTAuNTMyNjMyIDExLjgyMzUxOCAxMC41MTM2NzIgMTEuOTYyMDk5IDEwLjUxMzY3MiAxMi4xMDc0MjIgQyAxMC41MTM2NzIgMTIuMjk2NzgyIDEwLjU0NzIyNiAxMi40NjExNDEgMTAuNjEzMjgxIDEyLjU5NzY1NiBDIDEwLjY4Mzc0MSAxMi43Mjk3NjggMTAuNzcyNzE5IDEyLjg0MTExNiAxMC44ODI4MTIgMTIuOTMzNTk0IEMgMTAuOTk3MzA5IDEzLjAyNjA3MiAxMS4xMjMyNSAxMy4xMDU4MTkgMTEuMjU5NzY2IDEzLjE3MTg3NSBDIDExLjQwMDY4NSAxMy4yMzM1MjcgMTEuNTQ0MTMgMTMuMjkwOTA1IDExLjY4OTQ1MyAxMy4zNDM3NSBDIDExLjgzOTE3OSAxMy4zOTY1OTQgMTEuOTcxNDQxIDEzLjQ0NTcgMTIuMDg1OTM4IDEzLjQ5NDE0MSBDIDEyLjIwMDQzNCAxMy41MzgxNzggMTIuMjk0Mjc3IDEzLjU4NjMyNSAxMi4zNjkxNDEgMTMuNjM0NzY2IEMgMTIuNDQ0MDA0IDEzLjY4MzIwNiAxMi41MDEzODIgMTMuNzM3MTc1IDEyLjU0MTAxNiAxMy43OTg4MjggQyAxMi41ODA2NDkgMTMuODU2MDc3IDEyLjYwMTU2MiAxMy45MjYwOTUgMTIuNjAxNTYyIDE0LjAwOTc2NiBDIDEyLjYwMTU2MiAxNC4xNDE4NzcgMTIuNTUwNTA0IDE0LjI0NTQ0OSAxMi40NDkyMTkgMTQuMzIwMzEyIEMgMTIuMzUyMzM3IDE0LjM5MDc3MiAxMi4xNjkwMTggMTQuNDI1NzgxIDExLjkwMDM5MSAxNC40MjU3ODEgQyAxMS42NjI1OSAxNC40MjU3ODEgMTEuNDM2NDg2IDE0LjM5NTYzNyAxMS4yMjA3MDMgMTQuMzMzOTg0IEMgMTEuMDA5MzI0IDE0LjI3MjMzMiAxMC44MjYwMDUgMTQuMjAzNzY5IDEwLjY3MTg3NSAxNC4xMjg5MDYgTCAxMC4zODg2NzIgMTQuODg4NjcyIEMgMTAuNDg1NTU0IDE0LjkzNzExNCAxMC42MzE5MTEgMTQuOTk5MzU2IDEwLjgzMDA3OCAxNS4wNzQyMTkgQyAxMS4wMzI2NDkgMTUuMTQ0Njc4IDExLjI5NTcxNiAxNS4xOTIzMjggMTEuNjE3MTg4IDE1LjIxODc1IEwgMTEuNjE3MTg4IDE2LjAwNTg1OSBMIDEyLjQ0OTIxOSAxNi4wMDU4NTkgTCAxMi40NDkyMTkgMTUuMTk5MjE5IEMgMTIuNjY1MDAxIDE1LjE2Mzk4OSAxMi44NDQ5MTEgMTUuMTA2NjExIDEyLjk5MDIzNCAxNS4wMjczNDQgQyAxMy4xMzk5NjEgMTQuOTQ4MDc2IDEzLjI2MTAzOCAxNC44NTQyMzMgMTMuMzUzNTE2IDE0Ljc0NDE0MSBDIDEzLjQ0NTk5NCAxNC42MzQwNDggMTMuNTEzMTAxIDE0LjUxNDQyNyAxMy41NTI3MzQgMTQuMzg2NzE5IEMgMTMuNTkyMzY4IDE0LjI1NDYwNyAxMy42MTEzMjggMTQuMTIyMzQ2IDEzLjYxMTMyOCAxMy45OTAyMzQgQyAxMy42MTEzMjggMTMuNzk2NDcgMTMuNTc2MzE5IDEzLjYyNzc0NCAxMy41MDU4NTkgMTMuNDgyNDIyIEMgMTMuNDM5ODA0IDEzLjMzNzA5OSAxMy4zNDU5NjEgMTMuMjExMTU4IDEzLjIyMjY1NiAxMy4xMDU0NjkgQyAxMy4xMDM3NTYgMTIuOTk1Mzc1IDEyLjk2MDMxIDEyLjg5ODEyNCAxMi43OTI5NjkgMTIuODE0NDUzIEMgMTIuNjMwMDMxIDEyLjczMDc4MyAxMi40NTE1NzYgMTIuNjU0NDQ0IDEyLjI1NzgxMiAxMi41ODM5ODQgQyAxMi4xMzAxMDQgMTIuNTM5OTQ3IDEyLjAyMDIxMiAxMi41MDAwNzMgMTEuOTI3NzM0IDEyLjQ2NDg0NCBDIDExLjgzOTY2IDEyLjQyNTIxIDExLjc2NDc3NyAxMi4zODUzMzcgMTEuNzAzMTI1IDEyLjM0NTcwMyBDIDExLjY0MTQ3MyAxMi4zMDYwNyAxMS41OTUyNzkgMTIuMjYxMzMxIDExLjU2NDQ1MyAxMi4yMTI4OTEgQyAxMS41MzgwMzEgMTIuMTYwMDQ2IDExLjUyNTM5MSAxMi4wOTk3NTYgMTEuNTI1MzkxIDEyLjAyOTI5NyBDIDExLjUyNTM5MSAxMS45MTQ4IDExLjU3MTU4NCAxMS44MjI0MTMgMTEuNjY0MDYyIDExLjc1MTk1MyBDIDExLjc1NjU0MSAxMS42ODE0OTQgMTEuOTA3NzYyIDExLjY0NjQ4NCAxMi4xMTkxNDEgMTEuNjQ2NDg0IEMgMTIuMzMwNTE5IDExLjY0NjQ4NCAxMi41MzEzNDMgMTEuNjY2OSAxMi43MjA3MDMgMTEuNzEwOTM4IEMgMTIuOTE0NDY3IDExLjc1NDk3NSAxMy4wODMxOTMgMTEuODA5NDQyIDEzLjIyODUxNiAxMS44NzEwOTQgTCAxMy40MzM1OTQgMTEuMDc4MTI1IEMgMTMuMzM2NzEyIDExLjAzNDA4OSAxMy4yMDQ0NTEgMTAuOTg5MzUgMTMuMDM3MTA5IDEwLjk0NTMxMiBDIDEyLjg3NDE3MiAxMC45MDEyNzUgMTIuNjc4MjEyIDEwLjg2NzcyMiAxMi40NDkyMTkgMTAuODQ1NzAzIEwgMTIuNDQ5MjE5IDEwLjE2MDE1NiBMIDExLjYxNzE4OCAxMC4xNjAxNTYgeiAiCiAgICAgaWQ9InBhdGg0IiAvPgogIDxnCiAgICAgaWQ9Imc2IgogICAgIHRyYW5zZm9ybT0ibWF0cml4KDAuMTMzMzU1MDYsMCwwLDAuMTMzMzU1MDYsMC40MzE0NTQ3LC04Ljk2ODAwNDEpIj4KICAgIDxnCiAgICAgICBpZD0idGV4dDgyOCIKICAgICAgIHN0eWxlPSJmb250LXN0eWxlOm5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zaXplOjQ5LjUzMzc5NDRweDtsaW5lLWhlaWdodDoxMjUlO2ZvbnQtZmFtaWx5OlNhbnM7dGV4dC1hbGlnbjplbmQ7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7dGV4dC1hbmNob3I6ZW5kO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MTEuMzIyMDA5MDlweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgYXJpYS1sYWJlbD0iJCIgLz4KICA8L2c+Cjwvc3ZnPgo=)
+}
+
+.icon-undo {
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+DQo8c3ZnPg0KPGcgaWQ9InVuZG8iPjxwYXRoIGQ9Ik0xMi41IDhjLTIuNjUgMC01LjA1Ljk5LTYuOSAyLjZMMiA3djloOWwtMy42Mi0zLjYyYzEuMzktMS4xNiAzLjE2LTEuODggNS4xMi0xLjg4IDMuNTQgMCA2LjU1IDIuMzEgNy42IDUuNWwyLjM3LS43OEMyMS4wOCAxMS4wMyAxNy4xNSA4IDEyLjUgOHoiPjwvcGF0aD48L2c+DQo8L3N2Zz4=)
+}
+
+.icon-hourglass {
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSIxNzkyIiB2aWV3Qm94PSIwIDAgMTc5MiAxNzkyIiB3aWR0aD0iMTc5MiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTUzNiAxMjhxMCAyNjEtMTA2LjUgNDYxLjV0LTI2Ni41IDMwNi41cTE2MCAxMDYgMjY2LjUgMzA2LjV0MTA2LjUgNDYxLjVoOTZxMTQgMCAyMyA5dDkgMjN2NjRxMCAxNC05IDIzdC0yMyA5aC0xNDcycS0xNCAwLTIzLTl0LTktMjN2LTY0cTAtMTQgOS0yM3QyMy05aDk2cTAtMjYxIDEwNi41LTQ2MS41dDI2Ni41LTMwNi41cS0xNjAtMTA2LTI2Ni41LTMwNi41dC0xMDYuNS00NjEuNWgtOTZxLTE0IDAtMjMtOXQtOS0yM3YtNjRxMC0xNCA5LTIzdDIzLTloMTQ3MnExNCAwIDIzIDl0OSAyM3Y2NHEwIDE0LTkgMjN0LTIzIDloLTk2em0tMTI4IDBoLTEwMjRxMCA2NiA5IDEyOGgxMDA2cTktNjEgOS0xMjh6bTAgMTUzNnEwLTEzMC0zNC0yNDkuNXQtOTAuNS0yMDgtMTI2LjUtMTUyLTE0Ni05NC41aC0yMzBxLTc2IDMxLTE0NiA5NC41dC0xMjYuNSAxNTItOTAuNSAyMDgtMzQgMjQ5LjVoMTAyNHoiLz48L3N2Zz4=)
+}
+
+.icon-dashboard {
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkJz48c3ZnIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEyOCAxMjgiIGhlaWdodD0iMTI4cHgiIGlkPSJMYXllcl8xIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB3aWR0aD0iMTI4cHgiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxnPjxnPjxwYXRoIGQ9Ik0yNCw4MEg4LjV2OEgyNGMyLjIxMSwwLDQtMS43ODksNC00UzI2LjIxMSw4MCwyNCw4MHogTTI3LjIzLDQxLjU3NGwtNi42MTcsMy45MSAgICBjMC4xOCwwLjYzNywwLjQ2MSwxLjI0NiwwLjk2MSwxLjc0NmwxMS4zMTMsMTEuMzEzYzEuNTYzLDEuNTY2LDQuMDk0LDEuNTY2LDUuNjU2LDBjMS41NjYtMS41NjMsMS41NjYtNC4wOTQsMC01LjY1NiAgICBMMjcuMjMsNDEuNTc0eiBNNjAsMjguNTMxVjQ0YzAsMi4yMTEsMS43ODksNCw0LDRzNC0xLjc4OSw0LTRWMjhoLTRMNjAsMjguNTMxeiBNMTAwLjc3LDQxLjU3NEw4OS40NTcsNTIuODg3ICAgIGMtMS41NjYsMS41NjMtMS41NjYsNC4wOTQsMCw1LjY1NmMxLjU2MywxLjU2Niw0LjA5NCwxLjU2Niw1LjY1NiwwbDEwLjY5OS0xMC42OTlMMTAwLjc3LDQxLjU3NHogTTEwNCw4MGMtMi4yMTEsMC00LDEuNzg5LTQsNCAgICBzMS43ODksNCw0LDRoMTMuNXYtOEgxMDR6Ii8+PC9nPjwvZz48cGF0aCBkPSJNNjQsMjRDMzAuODYzLDI0LDQsNTAuODYzLDQsODRjMCw3LjAyMywxLjI3LDEzLjczNCwzLjQ4OCwyMGgxMTMuMDIzQzEyMi43Myw5Ny43MzQsMTI0LDkxLjAyMywxMjQsODQgIEMxMjQsNTAuODYzLDk3LjEzNywyNCw2NCwyNHogTTUyLjkzLDk2YzEuNjU2LTQuNjIxLDUuODc1LTgsMTEuMDctOHM5LjQxNCwzLjM3OSwxMS4wNyw4SDUyLjkzeiBNODIuNTEyLDkyLjVsMjMuNDMtMzMuNzE1ICBMNzIuNTcsODIuMDA4YzAuMDk4LDAuMDQ3LDAuMTgsMC4xMTcsMC4yNzMsMC4xNjRDNzAuMTcyLDgwLjgyOCw2Ny4xOTUsODAsNjQsODBjLTkuNjQxLDAtMTcuNTk0LDYuODk4LTE5LjQ1MywxNkgxMy40NDUgIEMxMi40ODQsOTIuMDMxLDEyLDg4LjAxNiwxMiw4NGMwLTI4LjY3MiwyMy4zMjgtNTIsNTItNTJzNTIsMjMuMzI4LDUyLDUyYzAsNC4wMTYtMC40ODQsOC4wMzEtMS40NDUsMTJIODMuNDUzIi8+PC9zdmc+);
+}
+
+.icon-trash {
+ background-image: url(data:img/png;base64,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)
+} \ No newline at end of file
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..e2242b0
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,40 @@
+import "core-js/stable";
+import "regenerator-runtime/runtime";
+
+import Vue from 'vue'
+import App from './App'
+import router from './router'
+import VueMoment from 'vue-moment'
+import AsyncComputed from 'vue-async-computed'
+import {sync} from 'vuex-router-sync'
+import axios from "axios";
+
+
+// CSP config for webpack dynamic chunk loading
+// eslint-disable-next-line
+__webpack_nonce__ = btoa(OC.requestToken)
+
+// Correct the root of the app for chunk loading
+// OC.linkTo matches the apps folders
+// eslint-disable-next-line
+__webpack_public_path__ = OC.linkTo('ransomware_detection', 'js/')
+
+import "./css/global.css"
+
+Vue.prototype.t = t
+Vue.prototype.n = n
+Vue.prototype.OC = OC
+Vue.prototype.OCA = OCA
+Vue.prototype.$axios = axios
+
+Vue.use(VueMoment);
+Vue.use(AsyncComputed);
+
+Vue.config.devtools = true
+
+/* eslint-disable-next-line no-new */
+new Vue({
+ el: '#content',
+ router,
+ render: h => h(App)
+}) \ No newline at end of file
diff --git a/src/router.js b/src/router.js
new file mode 100644
index 0000000..c93277b
--- /dev/null
+++ b/src/router.js
@@ -0,0 +1,31 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+import {generateUrl} from 'nextcloud-server/dist/router'
+
+const Protection = () => import('./views/Protection')
+const Recover = () => import('./views/Recover')
+const History = () => import('./views/History')
+
+Vue.use(Router)
+
+export default new Router({
+ base: generateUrl('/apps/ransowmare_detection/'),
+ linkActiveClass: 'active',
+ routes: [
+ {
+ path: '/',
+ name: 'protection',
+ component: Protection,
+ },
+ {
+ path: '/recover',
+ name: 'recover',
+ component: Recover,
+ },
+ {
+ path: '/history',
+ name: 'history',
+ component: History,
+ },
+ ],
+}) \ No newline at end of file
diff --git a/src/views/History.vue b/src/views/History.vue
new file mode 100644
index 0000000..18bd197
--- /dev/null
+++ b/src/views/History.vue
@@ -0,0 +1,123 @@
+<template>
+ <AppContent>
+ <iron-pages :selected="page">
+ <div id="loading" class="page">
+ <paper-spinner active></paper-spinner>
+ </div>
+ <div class="page">
+ <Header header="History">
+ <RecoverAction id="recover" label="Recover selected files" v-on:recover="onRecover" primary></RecoverAction>
+ </Header>
+ <FileOperationsTable id="ransomware-table" :data="fileOperations" v-on:table-state-changed="tableStateChanged"></FileOperationsTable>
+ </div>
+ </iron-pages>
+ </AppContent>
+</template>
+
+<script>
+import '@polymer/paper-spinner/paper-spinner.js';
+import '@polymer/iron-pages/iron-pages.js';
+import FileOperationsTable from '../components/FileOperationsTable'
+import Header from '../components/Header'
+import RecoverAction from '../components/RecoverAction'
+import AppContent from 'nextcloud-vue/dist/Components/AppContent'
+
+export default {
+ name: 'History',
+ components: {
+ AppContent,
+ FileOperationsTable,
+ Header,
+ RecoverAction
+ },
+ data() {
+ return {
+ fileOperations: [],
+ page: 0
+ };
+ },
+ mounted() {
+ this.page = 0;
+ this.fetchData();
+ setInterval(() => this.fetchData(), 3000);
+ },
+ computed: {
+ recoverUrl() {
+ return OC.generateUrl('/apps/ransomware_detection/api/v1/file-operation')
+ },
+ fileOperationsUrl() {
+ return OC.generateUrl('/apps/ransomware_detection/api/v1/file-operation')
+ }
+ },
+ methods: {
+ tableStateChanged() {
+ this.page = 1;
+ },
+ fetchData() {
+ this.$axios({
+ method: 'GET',
+ url: this.fileOperationsUrl
+ })
+ .then(json => {
+ this.fileOperations = json.data;
+ })
+ .catch( error => { console.error(error); });
+ },
+ onRecover() {
+ const items = document.querySelector('#ransomware-table').items;
+ const selected = document.querySelector('#ransomware-table').selectedItems;
+ for (var i = 0; i < selected.length; i++) {
+ this.recover(selected[i].id);
+ }
+ },
+ remove(id) {
+ for (var i = 0; i < this.fileOperations.length; i++) {
+ if (this.fileOperations[i].id === id) {
+ this.fileOperations.splice(i, 1);
+ }
+ }
+ },
+ async recover(id) {
+ await this.$axios({
+ method: 'PUT',
+ url: this.recoverUrl + '/' + id + '/recover'
+ })
+ .then(response => {
+ switch(response.status) {
+ case 204:
+ this.remove(id);
+ break;
+ default:
+ console.log(response);
+ break;
+ }
+ })
+ .catch(error => {
+ console.error(error);
+ });
+ }
+ }
+}
+</script>
+
+<style scoped>
+ #ransomware-table {
+ height: calc(100% - 50px);
+ }
+ #recover {
+ background-color: grey;
+ color: #fff;
+ }
+ iron-pages {
+ height: 100%;
+ }
+ .page {
+ height: 100%;
+ }
+ #loading {
+ display: flex;
+ align-items: center;
+ height: 90vh;
+ justify-content: center;
+ }
+</style> \ No newline at end of file
diff --git a/src/views/Protection.vue b/src/views/Protection.vue
new file mode 100644
index 0000000..3ce0848
--- /dev/null
+++ b/src/views/Protection.vue
@@ -0,0 +1,85 @@
+<template>
+ <AppContent>
+ <iron-pages selected="0">
+ <div id="loading">
+ <paper-spinner active></paper-spinner>
+ </div>
+ <div>
+ <ProtectionStatus :detection-link="detectionUrl" :protection-link="servicesUrl" id="protection-status" v-on:protection-state-changed="protectionStateChanged"></ProtectionStatus>
+ <div id="services">
+ <ServiceStatus :link="detectionServiceUrl" description="Your files currently cannot be analyzed for ransomware. To enable ransomware detection, contact your system administator." v-on:service-state-changed="detectionStateChanged" class="service"></ServiceStatus>
+ <ServiceStatus :link="monitorServiceUrl" description="There may be a problem with your Nextcloud installation. Please contact your system administator." v-on:service-state-changed="monitorStateChanged" class="service"></ServiceStatus>
+ </div>
+ </div>
+ </iron-pages>
+ </AppContent>
+</template>
+
+<script>
+import '@polymer/paper-spinner/paper-spinner.js';
+import '@polymer/iron-pages/iron-pages.js';
+import AppContent from 'nextcloud-vue/dist/Components/AppContent'
+import ProtectionStatus from '../components/ProtectionStatus'
+import ServiceStatus from '../components/ServiceStatus'
+
+export default {
+ name: 'Protection',
+ components: {
+ AppContent,
+ ProtectionStatus,
+ ServiceStatus
+ },
+ data() {
+ return {
+ protectionReady: false,
+ detectionReady: false,
+ monitorReady: false
+ };
+ },
+ computed: {
+ detectionUrl() {
+ return OC.generateUrl('/apps/ransomware_detection/api/v1/detection');
+ },
+ servicesUrl() {
+ return OC.generateUrl('/apps/ransomware_detection/api/v1/service');
+ },
+ detectionServiceUrl() {
+ return OC.generateUrl('/apps/ransomware_detection/api/v1/service/0');
+ },
+ monitorServiceUrl() {
+ return OC.generateUrl('/apps/ransomware_detection/api/v1/service/1');
+ }
+ },
+ methods: {
+ protectionStateChanged() {
+ this.protectionReady = true;
+ this.hideSpinner();
+ },
+ monitorStateChanged() {
+ this.monitorReady = true;
+ this.hideSpinner();
+ },
+ detectionStateChanged() {
+ this.detectionReady = true;
+ this.hideSpinner();
+ },
+ hideSpinner() {
+ if (this.protectionReady && this.monitorReady && this.detectionReady) {
+ document.querySelector('iron-pages').selectIndex(1);
+ }
+ }
+ }
+}
+</script>
+
+<style scoped>
+ #protection-status {
+ height: 40vh;
+ }
+ #loading {
+ display: flex;
+ align-items: center;
+ height: 90vh;
+ justify-content: center;
+ }
+</style> \ No newline at end of file
diff --git a/src/views/Recover.vue b/src/views/Recover.vue
new file mode 100644
index 0000000..51aa6ca
--- /dev/null
+++ b/src/views/Recover.vue
@@ -0,0 +1,153 @@
+<template>
+ <AppContent>
+ <iron-pages :selected="page">
+ <div id="loading" class="page">
+ <paper-spinner active></paper-spinner>
+ </div>
+ <div class="page">
+ <Header header="Recover">
+ <RecoverAction v-if="detected" id="recover" label="Recover" v-on:recover="onRecover" primary></RecoverAction>
+ </Header>
+ <FileOperationsTable v-if="detected" id="ransomware-table" :data="fileOperations" v-on:table-state-changed="tableStateChanged"></FileOperationsTable>
+ <span id="message" v-if="!detected">
+ <iron-icon icon="verified-user"></iron-icon>
+ Nothing found. You are safe.
+ </span>
+ </div>
+ </iron-pages>
+ </AppContent>
+</template>
+
+<script>
+import '@polymer/paper-spinner/paper-spinner.js';
+import '@polymer/iron-pages/iron-pages.js';
+import '@polymer/iron-icon/iron-icon.js';
+import '@polymer/iron-icons/iron-icons.js';
+import FileOperationsTable from '../components/FileOperationsTable'
+import Header from '../components/Header'
+import RecoverAction from '../components/RecoverAction'
+import AppContent from 'nextcloud-vue/dist/Components/AppContent'
+
+export default {
+ name: 'Recover',
+ components: {
+ AppContent,
+ FileOperationsTable,
+ Header,
+ RecoverAction
+ },
+ data() {
+ return {
+ detected: 0,
+ fileOperations: [],
+ page: 0
+ };
+ },
+ mounted() {
+ this.page = 0;
+ this.fetchDetectionStatus();
+ this.fetchData();
+ },
+ methods: {
+ fetchDetectionStatus() {
+ this.$axios({
+ method: 'GET',
+ url: this.detectionsUrl
+ })
+ .then(json => {
+ if (json.data.length > 0) {
+ this.detected = 1;
+ } else {
+ this.page = 1;
+ }
+ })
+ .catch( error => { console.error(error); });
+ },
+ tableStateChanged() {
+ this.page = 1;
+ },
+ fetchData() {
+ this.$axios({
+ method: 'GET',
+ url: this.fileOperationsUrl
+ })
+ .then(json => {
+ this.fileOperations = json.data;
+ })
+ .catch( error => { console.error(error); });
+ },
+ onRecover() {
+ const items = document.querySelector('#ransomware-table').items;
+ const selected = document.querySelector('#ransomware-table').selectedItems;
+ for (var i = 0; i < selected.length; i++) {
+ this.recover(selected[i].id);
+ }
+ },
+ remove(id) {
+ for (var i = 0; i < this.fileOperations.length; i++) {
+ if (this.fileOperations[i].id === id) {
+ this.fileOperations.splice(i, 1);
+ }
+ }
+ },
+ async recover(id) {
+ await this.$axios({
+ method: 'PUT',
+ url: this.recoverUrl + '/' + id + '/recover'
+ })
+ .then(response => {
+ switch(response.status) {
+ case 204:
+ this.remove(id);
+ break;
+ default:
+ console.log(response);
+ break;
+ }
+ })
+ .catch(error => {
+ console.error(error);
+ });
+ }
+ },
+ computed: {
+ detectionsUrl() {
+ return OC.generateUrl('/apps/ransomware_detection/api/v1/detection');
+ },
+ recoverUrl() {
+ return OC.generateUrl('/apps/ransomware_detection/api/v1/file-operation')
+ },
+ fileOperationsUrl() {
+ return OC.generateUrl('/apps/ransomware_detection/api/v1/file-operation')
+ }
+ }
+}
+</script>
+
+<style scoped>
+ #ransomware-table {
+ height: calc(100% - 50px);
+ }
+ #recover {
+ background-color: green;
+ color: #fff;
+ }
+ #message {
+ display: flex;
+ justify-content: center;
+ font-size: 1.5em;
+ font-weight: bold;
+ }
+ iron-pages {
+ height: 100%;
+ }
+ .page {
+ height: 100%;
+ }
+ #loading {
+ display: flex;
+ align-items: center;
+ height: 90vh;
+ justify-content: center;
+ }
+</style> \ No newline at end of file
diff --git a/src/webcomponents/ransomware-icons.js b/src/webcomponents/ransomware-icons.js
new file mode 100644
index 0000000..9d23d52
--- /dev/null
+++ b/src/webcomponents/ransomware-icons.js
@@ -0,0 +1,34 @@
+/**
+@license
+Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '@polymer/iron-icon/iron-icon.js';
+import '@polymer/iron-iconset-svg/iron-iconset-svg.js';
+
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const template = html`<iron-iconset-svg name="ransomware" size="24">
+<svg><defs>
+<g id="timelapse"><path d="M16.24 7.76C15.07 6.59 13.54 6 12 6v6l-4.24 4.24c2.34 2.34 6.14 2.34 8.49 0 2.34-2.34 2.34-6.14-.01-8.48zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"></path></g>
+<g id="shield"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.96c1.165 0 2.17.416 2.785 1.2.616.784.832 1.816.832 2.983v1.412h1.735c.162 0 .293.13.293.293v6.468c0 .163-.13.295-.293.295H6.648a.294.294 0 0 1-.293-.295V9.848c0-.163.13-.293.293-.293h1.735V8.143c0-1.167.216-2.199.832-2.983.615-.784 1.62-1.2 2.785-1.2zm0 1.589c-.832 0-1.239.214-1.535.592-.297.377-.494 1.04-.494 2.002v1.412h4.058V8.143c0-.961-.197-1.625-.494-2.002-.296-.378-.703-.592-1.535-.592zm-.383 4.611v.713a1.603 1.603 0 0 0-.502.172 1.083 1.083 0 0 0-.543.646 1.44 1.44 0 0 0-.058.416c0 .19.033.354.1.49.07.133.159.244.269.337.114.092.24.172.377.238.14.062.284.119.43.172.15.053.281.102.396.15.114.044.208.092.283.14a.56.56 0 0 1 .172.165c.04.057.06.127.06.21a.365.365 0 0 1-.152.311c-.097.07-.28.106-.549.106a2.46 2.46 0 0 1-.68-.092 3.522 3.522 0 0 1-.548-.205l-.283.76a5 5 0 0 0 .441.185c.203.07.466.118.787.145v.787h.832v-.807a1.7 1.7 0 0 0 .541-.172c.15-.079.271-.173.364-.283.092-.11.16-.23.199-.357.04-.132.058-.265.058-.397a1.15 1.15 0 0 0-.105-.508 1.078 1.078 0 0 0-.283-.377 1.837 1.837 0 0 0-.43-.29 4.653 4.653 0 0 0-.535-.231 11.1 11.1 0 0 1-.33-.12 1.734 1.734 0 0 1-.225-.118.447.447 0 0 1-.139-.133.403.403 0 0 1-.039-.184c0-.114.047-.207.14-.277.092-.07.243-.106.454-.106.212 0 .412.02.602.065.193.044.362.098.508.16l.205-.793a2.571 2.571 0 0 0-.397-.133 3.604 3.604 0 0 0-.588-.1v-.685h-.832z"/></g>
+<g id="locked"><path d="M12 1a5 5 0 0 0-5 5v2H6a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2h-1V6a5 5 0 0 0-5-5zm0 1.9A3.1 3.1 0 0 1 15.1 6v2H8.9V6A3.1 3.1 0 0 1 12 2.9zm-.83 6.83h1.76V11a2.33 2.33 0 0 1 1.88 2.23h-1.3c-.03-.73-.41-1.23-1.46-1.23-.99 0-1.58.45-1.58 1.09 0 .55.43.91 1.76 1.26 1.33.34 2.75.91 2.75 2.57 0 1.2-.9 1.86-2.05 2.08v1.26h-1.76V19c-1.12-.24-2.08-.96-2.15-2.24h1.29c.06.69.54 1.23 1.74 1.23 1.3 0 1.58-.65 1.58-1.05 0-.55-.29-1.06-1.75-1.41-1.64-.4-2.76-1.07-2.76-2.42 0-1.13.92-1.87 2.05-2.12V9.73z"/></g>
+<g id="hourglass"><path d="M1536 128q0 261-106.5 461.5t-266.5 306.5q160 106 266.5 306.5t106.5 461.5h96q14 0 23 9t9 23v64q0 14-9 23t-23 9h-1472q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h96q0-261 106.5-461.5t266.5-306.5q-160-106-266.5-306.5t-106.5-461.5h-96q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h1472q14 0 23 9t9 23v64q0 14-9 23t-23 9h-96zm-128 0h-1024q0 66 9 128h1006q9-61 9-128zm0 1536q0-130-34-249.5t-90.5-208-126.5-152-146-94.5h-230q-76 31-146 94.5t-126.5 152-90.5 208-34 249.5h1024z"/></g>
+<g id="dashboard"><path d="M64,24C30.863,24,4,50.863,4,84c0,7.023,1.27,13.734,3.488,20h113.023C122.73,97.734,124,91.023,124,84 C124,50.863,97.137,24,64,24z M52.93,96c1.656-4.621,5.875-8,11.07-8s9.414,3.379,11.07,8H52.93z M82.512,92.5l23.43-33.715 L72.57,82.008c0.098,0.047,0.18,0.117,0.273,0.164C70.172,80.828,67.195,80,64,80c-9.641,0-17.594,6.898-19.453,16H13.445 C12.484,92.031,12,88.016,12,84c0-28.672,23.328-52,52-52s52,23.328,52,52c0,4.016-0.484,8.031-1.445,12H83.453"/></g>
+<g id="file"><path d="M1152 512v-472q22 14 36 28l408 408q14 14 28 36h-472zm-128 32q0 40 28 68t68 28h544v1056q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h800v544z"/></g>
+<g id="trash" >
+ <path d="M 9.066406 1.101562 L 7.507812 2.199219 L 6.957031 1.667969 C 6.585938 1.316406 6.320312 1.183594 6.167969 1.265625 C 5.347656 1.765625 2.816406 3.734375 2.730469 3.933594 C 2.679688 4.082031 2.800781 4.398438 3 4.667969 L 3.351562 5.148438 L 1.710938 6.449219 C 0.636719 7.316406 0.0664062 7.898438 0.0351562 8.148438 C -0.0351562 8.535156 0.167969 8.851562 1.726562 10.964844 L 2.378906 11.867188 L 2.78125 11.484375 L 3.183594 11.117188 L 3.183594 18.984375 L 4.074219 19.25 C 5.398438 19.648438 7.960938 20 9.554688 20 C 11.09375 20 13.476562 19.667969 14.882812 19.25 L 15.753906 18.984375 L 15.753906 8.332031 L 11.632812 8.300781 L 7.507812 8.25 L 10.878906 5.867188 L 14.261719 3.484375 L 12.957031 1.785156 C 11.261719 -0.417969 11.210938 -0.417969 9.066406 1.101562 Z M 11.800781 2.234375 L 12.570312 3.148438 L 8.179688 6.265625 C 5.75 7.984375 3.535156 9.566406 3.234375 9.800781 L 2.699219 10.199219 L 2.09375 9.351562 C 1.761719 8.898438 1.476562 8.449219 1.476562 8.382812 C 1.457031 8.300781 10.425781 1.683594 10.996094 1.351562 C 11.011719 1.332031 11.363281 1.734375 11.800781 2.234375 Z M 14.414062 13.898438 L 14.414062 18.300781 L 13.542969 18.5 C 11.546875 18.933594 7.003906 18.867188 5.078125 18.382812 L 4.691406 18.300781 L 4.691406 14.285156 C 4.691406 10.398438 4.710938 10.265625 5.0625 9.882812 C 5.414062 9.5 5.429688 9.5 9.921875 9.5 L 14.414062 9.5 Z M 14.414062 13.898438 "/>
+ <path d="M 6.199219 14.25 L 6.199219 17.167969 L 7.375 17.167969 L 7.375 11.332031 L 6.199219 11.332031 Z M 6.199219 14.25 "/>
+ <path d="M 8.882812 14.25 L 8.882812 17.167969 L 10.054688 17.167969 L 10.054688 11.332031 L 8.882812 11.332031 Z M 8.882812 14.25 "/>
+ <path d="M 11.730469 14.25 L 11.730469 17.167969 L 12.90625 17.167969 L 12.90625 11.332031 L 11.730469 11.332031 Z M 11.730469 14.25 "/>
+ <path d="M 14.496094 5.382812 C 14.128906 5.515625 13.609375 5.714844 13.339844 5.867188 C 12.871094 6.117188 12.90625 6.117188 14.09375 6.050781 C 15.082031 5.984375 15.519531 6.035156 16.289062 6.316406 C 17.847656 6.917969 19.054688 8.367188 19.359375 10.035156 L 19.476562 10.667969 L 17.597656 10.667969 L 19.121094 12.582031 C 19.960938 13.632812 20.699219 14.5 20.78125 14.5 C 20.867188 14.5 21.601562 13.632812 22.441406 12.582031 L 23.964844 10.667969 L 22.492188 10.667969 L 22.289062 9.867188 C 21.839844 8.035156 20.546875 6.5 18.804688 5.699219 C 17.664062 5.183594 15.585938 5.035156 14.496094 5.382812 Z M 14.496094 5.382812 "/>
+</g>
+</defs></svg>
+</iron-iconset-svg>`;
+
+document.head.appendChild(template.content);