rework script manager and modals to composition api. Start on script snippets

This commit is contained in:
sadnub
2021-07-29 19:41:32 -04:00
parent ae770b2b08
commit a994b08f02
18 changed files with 934 additions and 829 deletions

View File

@@ -2,9 +2,10 @@ import axios from "axios"
const baseUrl = "/scripts"
// script operations
export async function fetchScripts(params = {}) {
try {
const { data } = await axios.get(`${baseUrl}/scripts/`, { params: params })
const { data } = await axios.get(`${baseUrl}/`, { params: params })
return data
} catch (e) { }
}
@@ -14,4 +15,63 @@ export async function testScript(payload) {
const { data } = await axios.post(`${baseUrl}/testscript/`, payload)
return data
} catch (e) { }
}
export async function saveScript(payload) {
const { data } = await axios.post(`${baseUrl}/`, payload)
return data
}
export async function editScript(payload) {
const { data } = await axios.put(`${baseUrl}/${payload.id}/`, payload)
return data
}
export async function removeScript(id) {
const { data } = await axios.delete(`${baseUrl}/${id}/`)
return data
}
export async function downloadScript(id, params = {}) {
try {
const { data } = await axios.get(`${baseUrl}/download/${id}/`, { params: params })
return data
} catch (e) { }
}
// script snippet operations
export async function fetchScriptSnippets(params = {}) {
try {
const { data } = await axios.get(`${baseUrl}/snippets/`, { params: params })
return data
} catch (e) { }
}
export async function addScriptSnippet(payload) {
try {
const { data } = await axios.post(`${baseUrl}/snippets/`, payload)
return data
} catch (e) { }
}
export async function fetchScriptSnippet(id, params = {}) {
try {
const { data } = await axios.get(`${baseUrl}/snippets/${id}/`, { params: params })
return data
} catch (e) { }
}
export async function editScriptSnippet(payload) {
try {
const { data } = await axios.put(`${baseUrl}/snippets/${payload.id}/`, payload)
return data
} catch (e) { }
}
export async function deleteScriptSnippet(payload) {
try {
const { data } = await axios.delete(`${baseUrl}/snippets/${payload.id}/`)
return data
} catch (e) { }
}

View File

@@ -70,7 +70,7 @@
<q-item-section>Clients Manager</q-item-section>
</q-item>
<!-- script manager -->
<q-item clickable v-close-popup @click="showScriptManager = true">
<q-item clickable v-close-popup @click="showScriptManager">
<q-item-section>Script Manager</q-item-section>
</q-item>
<!-- automation manager -->
@@ -166,12 +166,6 @@
<UpdateAgents @close="showUpdateAgentsModal = false" @edit="edited" />
</q-dialog>
</div>
<!-- Script Manager -->
<div class="q-pa-md q-gutter-sm">
<q-dialog v-model="showScriptManager">
<ScriptManager @close="showScriptManager = false" />
</q-dialog>
</div>
<!-- Admin Manager -->
<div class="q-pa-md q-gutter-sm">
<q-dialog v-model="showAdminManager">
@@ -210,7 +204,7 @@ import ClientsManager from "@/components/ClientsManager";
import ClientsForm from "@/components/modals/clients/ClientsForm";
import SitesForm from "@/components/modals/clients/SitesForm";
import UpdateAgents from "@/components/modals/agents/UpdateAgents";
import ScriptManager from "@/components/ScriptManager";
import ScriptManager from "@/components/scripts/ScriptManager";
import EditCoreSettings from "@/components/modals/coresettings/EditCoreSettings";
import AlertsManager from "@/components/AlertsManager";
import AutomationManager from "@/components/automation/AutomationManager";
@@ -230,7 +224,6 @@ export default {
components: {
PendingActions,
UpdateAgents,
ScriptManager,
EditCoreSettings,
InstallAgent,
UploadMesh,
@@ -253,7 +246,6 @@ export default {
showPendingActions: false,
bulkMode: null,
showDeployment: false,
showScriptManager: false,
showCodeSign: false,
};
},
@@ -336,6 +328,11 @@ export default {
},
});
},
showScriptManager() {
this.$q.dialog({
component: ScriptManager,
});
},
showDebugLog() {
this.$q.dialog({
component: DialogWrapper,

View File

@@ -148,7 +148,7 @@
import { mapGetters } from "vuex";
import mixins from "@/mixins/mixins";
import PatchPolicyForm from "@/components/modals/agents/PatchPolicyForm";
import CustomField from "@/components/CustomField";
import CustomField from "@/components/ui/CustomField";
export default {
name: "EditAgent",

View File

@@ -44,7 +44,7 @@
</template>
<script>
import CustomField from "@/components/CustomField";
import CustomField from "@/components/ui/CustomField";
import mixins from "@/mixins/mixins";
export default {
name: "ClientsForm",

View File

@@ -49,7 +49,7 @@
</template>
<script>
import CustomField from "@/components/CustomField";
import CustomField from "@/components/ui/CustomField";
import mixins from "@/mixins/mixins";
export default {
name: "SitesForm",

View File

@@ -1,332 +0,0 @@
<template>
<q-dialog ref="dialog" @hide="onHide" persistent :maximized="maximized">
<q-card class="q-dialog-plugin" :style="getMaxWidth">
<q-bar>
{{ title }}
<q-space />
<q-btn dense flat icon="minimize" @click="maximized = false" :disable="!maximized">
<q-tooltip v-if="maximized" class="bg-white text-primary">Minimize</q-tooltip>
</q-btn>
<q-btn dense flat icon="crop_square" @click="maximized = true" :disable="maximized">
<q-tooltip v-if="!maximized" class="bg-white text-primary">Maximize</q-tooltip>
</q-btn>
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-form @submit="submit">
<q-card-section class="row">
<div class="q-pa-sm col-1" style="width: auto">
<q-icon
class="cursor-pointer"
:name="favoriteIcon"
size="md"
color="yellow-8"
@[clickEvent]="localScript.favorite = !localScript.favorite"
/>
</div>
<div class="q-pa-sm col-2">
<q-input
filled
dense
:readonly="readonly"
v-model="localScript.name"
label="Name"
:rules="[val => !!val || '*Required']"
/>
</div>
<div class="q-pa-sm col-2">
<q-select
:readonly="readonly"
options-dense
filled
dense
v-model="localScript.shell"
:options="shellOptions"
emit-value
map-options
label="Shell Type"
/>
</div>
<div class="q-pa-sm col-2">
<q-input
type="number"
filled
dense
:readonly="readonly"
v-model.number="localScript.default_timeout"
label="Timeout (seconds)"
:rules="[val => val >= 5 || 'Minimum is 5']"
/>
</div>
<div class="q-pa-sm col-3">
<q-select
hint="Press Enter or Tab when adding a new value"
dense
options-dense
filled
v-model="localScript.category"
:options="filterOptions"
use-input
clearable
new-value-mode="add-unique"
debounce="0"
@filter="filterFn"
label="Category"
:readonly="readonly"
/>
</div>
<div class="q-pa-sm col-2">
<q-input filled dense :readonly="readonly" v-model="localScript.description" label="Description" />
</div>
</q-card-section>
<div class="q-px-sm q-pt-none q-pb-sm q-mt-none row">
<q-select
label="Script Arguments (press Enter after typing each argument)"
class="col-12"
filled
v-model="localScript.args"
use-input
use-chips
multiple
dense
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</div>
<prism-editor
class="editor"
:readonly="readonly"
v-model="localScript.code"
:highlight="highlighter"
:style="heightVar"
line-numbers
@click="focusTextArea"
/>
<q-card-actions align="right">
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn dense flat color="primary" label="Test Script" @click="runTestScript" />
<q-btn v-if="!readonly" dense flat label="Save" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
import TestScriptModal from "@/components/modals/scripts/TestScriptModal";
import mixins from "@/mixins/mixins";
import { PrismEditor } from "vue-prism-editor";
import "vue-prism-editor/dist/prismeditor.min.css";
import { highlight, languages } from "prismjs/components/prism-core";
import "prismjs/components/prism-batch";
import "prismjs/components/prism-python";
import "prismjs/components/prism-powershell";
import "prismjs/themes/prism-tomorrow.css";
export default {
name: "ScriptFormModal",
emits: ["hide", "ok", "cancel"],
mixins: [mixins],
components: {
PrismEditor,
},
props: {
script: Object,
categories: !Array,
readonly: {
type: Boolean,
default: false,
},
clone: {
type: Boolean,
default: false,
},
},
data() {
return {
localScript: {
name: "",
code: "",
shell: "powershell",
description: "",
args: [],
category: "",
favorite: false,
script_type: "userdefined",
default_timeout: 90,
},
maximized: false,
filterOptions: [],
shellOptions: [
{ label: "Powershell", value: "powershell" },
{ label: "Batch", value: "cmd" },
{ label: "Python", value: "python" },
],
};
},
methods: {
submit() {
this.$q.loading.show();
if (this.script && !this.clone) {
this.$axios
.put(`/scripts/${this.script.id}/script/`, this.localScript)
.then(r => {
this.$q.loading.hide();
this.onOk();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
});
} else {
this.$axios
.post(`/scripts/scripts/`, this.localScript)
.then(r => {
this.$q.loading.hide();
this.onOk();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
});
}
},
getCode() {
this.$q.loading.show();
this.$axios
.get(`/scripts/${this.script.id}/download/`)
.then(r => {
this.$q.loading.hide();
this.localScript.code = r.data.code;
})
.catch(e => {
this.$q.loading.hide();
});
},
highlighter(code) {
let lang = this.localScript.shell === "cmd" ? "batch" : this.localScript.shell;
return highlight(code, languages[lang]);
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
onOk() {
this.$emit("ok");
this.hide();
},
onCancel() {
this.hide();
},
filterFn(val, update) {
update(() => {
if (val === "") {
this.filterOptions = this.categories;
} else {
const needle = val.toLowerCase();
this.filterOptions = this.categories.filter(v => v.toLowerCase().indexOf(needle) > -1);
}
});
},
focusTextArea() {
document.getElementsByClassName("prism-editor__textarea")[0].focus();
},
runTestScript() {
this.$q.dialog({
component: TestScriptModal,
componentProps: {
script: this.localScript,
},
});
},
},
computed: {
favoriteIcon() {
return this.localScript.favorite ? "star" : "star_outline";
},
title() {
if (this.script) {
return this.readonly
? `Viewing ${this.script.name}`
: this.clone
? `Copying ${this.script.name}`
: `Editing ${this.script.name}`;
} else {
return "Adding new script";
}
},
clickEvent() {
return !this.readonly ? "click" : null;
},
getMaxWidth() {
return this.maximized ? "" : "width: 70vw; max-width: 90vw";
},
heightVar() {
return this.maximized ? "--prism-height: 76vh" : "--prism-height: 70vh";
},
},
mounted() {
if (this.script) {
this.localScript.id = this.clone ? null : this.script.id;
this.localScript.name = this.clone ? "Copy of " + this.script.name : this.script.name;
this.localScript.description = this.script.description;
this.localScript.favorite = this.clone ? false : this.script.favorite;
this.localScript.shell = this.script.shell;
this.localScript.category = this.script.category;
this.localScript.script_type = this.clone ? "userdefined" : this.script.script_type;
this.localScript.default_timeout = this.script.default_timeout;
this.localScript.args = this.script.args;
this.getCode();
}
},
};
</script>
<style>
/* required class */
.editor {
/* we dont use `language-` classes anymore so thats why we need to add background and text color manually */
background: #2d2d2d;
color: #ccc;
/* you must provide font-family font-size line-height. Example: */
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
font-size: 14px;
line-height: 1.5;
padding: 5px;
height: var(--prism-height);
}
/* optional class for removing the outline */
.prism-editor__textarea:focus {
outline: none;
}
.prism-editor__textarea,
.prism-editor__container {
width: 500em !important;
-ms-overflow-style: none;
scrollbar-width: none;
}
.prism-editor__container::-webkit-scrollbar,
.prism-editor__textarea::-webkit-scrollbar {
display: none;
}
.prism-editor__editor {
white-space: pre !important;
}
.prism-editor__container {
overflow-x: auto !important;
}
</style>

View File

@@ -1,196 +0,0 @@
<template>
<q-dialog ref="dialog" @hide="onHide">
<q-card class="q-dialog-plugin" style="width: 40vw">
<q-bar>
Add Script
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-form @submit.prevent="submit">
<q-card-section class="row">
<div class="col-2">Name:</div>
<div class="col-10">
<q-input outlined dense v-model="script.name" :rules="[val => !!val || '*Required']" />
</div>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Description:</div>
<div class="col-10">
<q-input outlined dense v-model="script.description" type="textarea" />
</div>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Category:</div>
<q-select
hint="Press Enter or Tab when adding a new value"
dense
options-dense
class="col-10"
outlined
v-model="script.category"
:options="filterOptions"
use-input
clearable
new-value-mode="add-unique"
debounce="0"
@filter="filterFn"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-2">File Upload:</div>
<div class="col-10">
<q-file
v-model="script.filename"
label="Supported file types: .ps1, .bat, .py"
stack-label
filled
counter
class="full-width"
accept=".ps1, .bat, .py"
>
<template v-slot:prepend>
<q-icon name="attach_file" />
</template>
</q-file>
</div>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Type:</div>
<q-select
dense
options-dense
class="col-10"
outlined
v-model="script.shell"
:options="shellOptions"
emit-value
map-options
/>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Script Arguments:</div>
<q-select
label="(press Enter after typing each argument)"
class="col-10"
filled
v-model="script.args"
use-input
use-chips
multiple
dense
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-4">Default Timeout (seconds)</div>
<q-input
type="number"
outlined
dense
class="col-8"
v-model.number="script.default_timeout"
:rules="[val => val >= 5 || 'Minimum is 5']"
/>
</q-card-section>
<q-card-actions>
<q-space />
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn dense flat label="Add" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
import mixins from "@/mixins/mixins";
export default {
name: "ScriptModal",
emits: ["ok", "hide", "cancel"],
mixins: [mixins],
props: {
categories: !Array,
},
data() {
return {
script: {
name: "",
description: "",
shell: "powershell",
category: null,
default_timeout: 90,
args: [],
},
shellOptions: [
{ label: "Powershell", value: "powershell" },
{ label: "Batch (CMD)", value: "cmd" },
{ label: "Python", value: "python" },
],
filterOptions: [],
};
},
methods: {
submit() {
this.$q.loading.show();
let formData = new FormData();
if (!!this.script.filename) {
formData.append("filename", this.script.filename);
}
if (!!this.script.category) {
formData.append("category", this.script.category);
} else {
formData.append("category", "");
}
formData.append("file_upload", true);
formData.append("name", this.script.name);
formData.append("shell", this.script.shell);
formData.append("description", this.script.description);
formData.append("default_timeout", this.script.default_timeout);
formData.append("args", JSON.stringify(this.script.args));
this.$axios
.post("/scripts/scripts/", formData)
.then(r => {
this.$q.loading.hide();
this.onOk();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
});
},
filterFn(val, update) {
update(() => {
if (val === "") {
this.filterOptions = this.categories;
} else {
const needle = val.toLowerCase();
this.filterOptions = this.categories.filter(v => v.toLowerCase().indexOf(needle) > -1);
}
});
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
onOk() {
this.$emit("ok");
this.hide();
},
},
};
</script>

View File

@@ -0,0 +1,229 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide" persistent :maximized="maximized">
<q-card class="q-dialog-plugin" :style="maximized ? '' : 'width: 70vw; max-width: 90vw'">
<q-bar>
{{ title }}
<q-space />
<q-btn dense flat icon="minimize" @click="maximized = false" :disable="!maximized">
<q-tooltip v-if="maximized" class="bg-white text-primary">Minimize</q-tooltip>
</q-btn>
<q-btn dense flat icon="crop_square" @click="maximized = true" :disable="maximized">
<q-tooltip v-if="!maximized" class="bg-white text-primary">Maximize</q-tooltip>
</q-btn>
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-form @submit="submitForm">
<q-card-section class="row">
<div class="q-pa-sm col-1" style="width: auto">
<q-icon
class="cursor-pointer"
:name="formScript.favorite ? 'star' : 'star_outline'"
size="md"
color="yellow-8"
@[clickEvent]="formScript.favorite = !formScript.favorite"
/>
</div>
<div class="q-pa-sm col-2">
<q-input
filled
dense
:readonly="readonly"
v-model="formScript.name"
label="Name"
:rules="[val => !!val || '*Required']"
/>
</div>
<div class="q-pa-sm col-2">
<q-select
:readonly="readonly"
options-dense
filled
dense
v-model="formScript.shell"
:options="shellOptions"
emit-value
map-options
label="Shell Type"
/>
</div>
<div class="q-pa-sm col-2">
<q-input
type="number"
filled
dense
:readonly="readonly"
v-model.number="formScript.default_timeout"
label="Timeout (seconds)"
:rules="[val => val >= 5 || 'Minimum is 5']"
/>
</div>
<div class="q-pa-sm col-3">
<tactical-dropdown
hint="Press Enter or Tab when adding a new value"
filled
v-model="formScript.category"
:options="categories"
use-input
clearable
new-value-mode="add-unique"
filterable
label="Category"
:readonly="readonly"
/>
</div>
<div class="q-pa-sm col-2">
<q-input filled dense :readonly="readonly" v-model="formScript.description" label="Description" />
</div>
</q-card-section>
<div class="q-px-sm q-pt-none q-pb-sm q-mt-none row">
<tactical-dropdown
label="Script Arguments (press Enter after typing each argument)"
filled
class="col-12"
v-model="formScript.args"
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</div>
<CodeEditor
v-model="code"
:style="maximized ? '--prism-height: 76vh' : '--prism-height: 70vh'"
:readonly="readonly"
:shell="formScript.shell"
/>
<q-card-actions align="right">
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn dense flat color="primary" label="Test Script" @click="openTestScriptModal" />
<q-btn v-if="!readonly" :loading="loading" dense flat label="Save" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
// composable imports
import { ref, computed } from "vue";
import { useQuasar, useDialogPluginComponent } from "quasar";
import { saveScript, editScript, downloadScript } from "@/api/scripts";
import { notifySuccess } from "@/utils/notify";
// ui imports
import CodeEditor from "@/components/ui/CodeEditor";
import TestScriptModal from "@/components/scripts/TestScriptModal";
import TacticalDropdown from "@/components/ui/TacticalDropdown";
// static data
import { shellOptions } from "@/composables/scripts";
export default {
name: "ScriptFormModal",
emits: [...useDialogPluginComponent.emits],
components: {
CodeEditor,
TacticalDropdown,
},
props: {
script: Object,
categories: !Array,
readonly: {
type: Boolean,
default: false,
},
clone: {
type: Boolean,
default: false,
},
},
setup(props) {
// setup quasar plugins
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
const $q = useQuasar();
// script form logic
const script = props.script ? ref(Object.assign({}, props.script)) : ref({});
if (props.clone) script.value.name = `(Copy) ${script.value.name}`;
const code = ref("");
const maximized = ref(false);
const loading = ref(false);
const clickEvent = computed(() => (!props.readonly ? "click" : null));
const title = computed(() => {
if (props.script) {
return props.readonly
? `Viewing ${script.value.name}`
: props.clone
? `Copying ${script.value.name}`
: `Editing ${script.value.name}`;
} else {
return "Adding new script";
}
});
// get code if editing or cloning script
if (props.script) downloadScript(script.value.id).then(r => (code.value = r.code));
async function submitForm() {
loading.value = true;
let result = "";
try {
// base64 encode the script text
script.value.code_base64 = btoa(code.value);
// edit existing script
if (props.script && !props.clone) {
result = await editScript(script.value);
// add or save cloned script
} else {
result = await saveScript(script.value);
}
onDialogOK();
notifySuccess(result);
} catch (e) {}
loading.value = false;
}
function openTestScriptModal() {
$q.dialog({
component: TestScriptModal,
componentProps: {
script: script.value,
},
});
}
return {
// reactive data
formScript: script.value,
code,
maximized,
loading,
// non-reactive data
shellOptions,
//computed
clickEvent,
title,
//methods
submitForm,
openTestScriptModal,
// quasar dialog plugin
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div style="width: 90vw; max-width: 90vw">
<q-card>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="width: 90vw; max-width: 90vw">
<q-bar>
<q-btn @click="getScripts" class="q-mr-sm" dense flat push icon="refresh" />Script Manager
<q-space />
@@ -12,7 +12,7 @@
<div class="q-gutter-sm row">
<q-btn-dropdown icon="add" label="New" no-caps dense flat>
<q-list dense>
<q-item clickable v-close-popup @click="newScript">
<q-item clickable v-close-popup @click="newScriptModal">
<q-item-section side>
<q-icon size="xs" name="add" />
</q-item-section>
@@ -20,7 +20,7 @@
<q-item-label>New Script</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="uploadScript">
<q-item clickable v-close-popup @click="uploadScriptModal">
<q-item-section side>
<q-icon size="xs" name="cloud_upload" />
</q-item-section>
@@ -36,7 +36,7 @@
class="q-ml-sm"
:label="tableView ? 'Folder View' : 'Table View'"
:icon="tableView ? 'folder' : 'list'"
@click="setTableView(!tableView)"
@click="tableView = !tableView"
/>
<q-btn
dense
@@ -75,7 +75,12 @@
no-nodes-label="No Scripts Found"
>
<template v-slot:header-script="props">
<div>
<div
class="cursor-pointer"
@dblclick="
props.node.script_type === 'builtin' ? viewCodeModal(props.node) : editScriptModal(props.node)
"
>
<q-icon v-if="props.node.favorite" color="yellow-8" name="star" size="sm" class="q-px-sm" />
<q-icon v-else color="yellow-8" name="star_outline" size="sm" class="q-px-sm" />
@@ -96,7 +101,14 @@
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item clickable v-close-popup @click="cloneScript(props.node)">
<q-item clickable v-close-popup @click="viewCodeModal(props.node)">
<q-item-section side>
<q-icon name="remove_red_eye" />
</q-item-section>
<q-item-section>View Code</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="cloneScriptModal(props.node)">
<q-item-section side>
<q-icon name="content_copy" />
</q-item-section>
@@ -106,7 +118,7 @@
<q-item
clickable
v-close-popup
@click="editScript(props.node)"
@click="editScriptModal(props.node)"
:disable="props.node.script_type === 'builtin'"
>
<q-item-section side>
@@ -118,7 +130,7 @@
<q-item
clickable
v-close-popup
@click="deleteScript(props.node.id)"
@click="deleteScript(props.node)"
:disable="props.node.script_type === 'builtin'"
>
<q-item-section side>
@@ -127,23 +139,18 @@
<q-item-section>Delete</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup @click="favoriteScript(props.node)">
<q-item-section side>
<q-icon name="star" />
</q-item-section>
<q-item-section>{{ favoriteText(props.node.favorite) }}</q-item-section>
<q-item-section>{{
props.node.favorite ? "Remove as Favorite" : "Add as Favorite"
}}</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup @click="viewCode(props.node)">
<q-item-section side>
<q-icon name="remove_red_eye" />
</q-item-section>
<q-item-section>View Code</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="downloadScript(props.node)">
<q-item clickable v-close-popup @click="exportScript(props.node)">
<q-item-section side>
<q-icon name="cloud_download" />
</q-item-section>
@@ -166,7 +173,7 @@
class="settings-tbl-sticky"
:rows="visibleScripts"
:columns="columns"
:visible-columns="visibleColumns"
:loading="loading"
v-model:pagination="pagination"
:filter="search"
row-key="id"
@@ -188,11 +195,22 @@
<template v-slot:no-data> No Scripts Found </template>
<template v-slot:body="props">
<!-- Table View -->
<q-tr>
<q-tr
:props="props"
@dblclick="props.row.script_type === 'builtin' ? viewCodeModal(props.row) : editScriptModal(props.row)"
class="cursor-pointer"
>
<!-- Context Menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item clickable v-close-popup @click="cloneScript(props.node)">
<q-item clickable v-close-popup @click="viewCodeModal(props.row)">
<q-item-section side>
<q-icon name="remove_red_eye" />
</q-item-section>
<q-item-section>View Code</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="cloneScriptModal(props.row)">
<q-item-section side>
<q-icon name="content_copy" />
</q-item-section>
@@ -202,7 +220,7 @@
<q-item
clickable
v-close-popup
@click="editScript(props.row)"
@click="editScriptModal(props.row)"
:disable="props.row.script_type === 'builtin'"
>
<q-item-section side>
@@ -214,7 +232,7 @@
<q-item
clickable
v-close-popup
@click="deleteScript(props.row.id)"
@click="deleteScriptModal(props.row)"
:disable="props.row.script_type === 'builtin'"
>
<q-item-section side>
@@ -223,23 +241,18 @@
<q-item-section>Delete</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup @click="favoriteScript(props.row)">
<q-item-section side>
<q-icon name="star" />
</q-item-section>
<q-item-section>{{ favoriteText(props.row.favorite) }}</q-item-section>
<q-item-section>{{
props.row.favorite ? "Remove as Favorite" : "Add as Favorite"
}}</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup @click="viewCode(props.row)">
<q-item-section side>
<q-icon name="remove_red_eye" />
</q-item-section>
<q-item-section>View Code</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="downloadScript(props.row)">
<q-item clickable v-close-popup @click="exportScript(props.row)">
<q-item-section side>
<q-icon name="cloud_download" />
</q-item-section>
@@ -269,7 +282,7 @@
</q-td>
<!-- name -->
<q-td>
{{ truncateText(props.row.name) }}
{{ truncateText(props.row.name, 50) }}
<q-tooltip v-if="props.row.name.length >= 50" style="font-size: 12px">
{{ props.row.name }}
</q-tooltip>
@@ -277,8 +290,8 @@
<!-- args -->
<q-td>
<span v-if="props.row.args.length > 0">
{{ truncateText(props.row.args.toString()) }}
<q-tooltip v-if="props.row.args.toString().length >= 50" style="font-size: 12px">
{{ truncateText(props.row.args.toString(), 30) }}
<q-tooltip v-if="props.row.args.toString().length >= 30" style="font-size: 12px">
{{ props.row.args }}
</q-tooltip>
</span>
@@ -286,8 +299,8 @@
<q-td>{{ props.row.category }}</q-td>
<q-td>
{{ truncateText(props.row.description) }}
<q-tooltip v-if="props.row.description.length >= 50" style="font-size: 12px">{{
{{ truncateText(props.row.description, 30) }}
<q-tooltip v-if="props.row.description.length >= 30" style="font-size: 12px">{{
props.row.description
}}</q-tooltip>
</q-td>
@@ -298,239 +311,167 @@
</div>
</div>
</q-card>
</div>
</q-dialog>
</template>
<script>
import { mapState } from "vuex";
import mixins from "@/mixins/mixins";
import ScriptUploadModal from "@/components/modals/scripts/ScriptUploadModal";
import ScriptFormModal from "@/components/modals/scripts/ScriptFormModal";
// composition imports
import { ref, computed, watch, onMounted } from "vue";
import { useStore } from "vuex";
import { useQuasar, useDialogPluginComponent, exportFile } from "quasar";
import { fetchScripts, editScript, downloadScript, removeScript } from "@/api/scripts";
import { truncateText } from "@/utils/format";
import { notifySuccess } from "@/utils/notify";
// ui imports
import ScriptUploadModal from "@/components/scripts/ScriptUploadModal";
import ScriptFormModal from "@/components/scripts/ScriptFormModal";
// static data
const columns = [
{
name: "favorite",
label: "",
field: "favorite",
align: "left",
sortable: true,
},
{
name: "shell",
label: "Shell",
field: "shell",
align: "left",
sortable: true,
},
{
name: "name",
label: "Name",
field: "name",
align: "left",
sortable: true,
},
{
name: "args",
label: "Default Args",
field: "args",
align: "left",
sortable: true,
},
{
name: "category",
label: "Category",
field: "category",
align: "left",
sortable: true,
},
{
name: "desc",
label: "Description",
field: "description",
align: "left",
sortable: false,
},
{
name: "default_timeout",
label: "Default Timeout (seconds)",
field: "default_timeout",
align: "left",
sortable: true,
},
];
export default {
name: "ScriptManager",
mixins: [mixins],
data() {
return {
scripts: [],
search: "",
tableView: true,
expanded: [],
pagination: {
rowsPerPage: 0,
sortBy: "favorite",
descending: true,
},
columns: [
{
name: "favorite",
label: "",
field: "favorite",
align: "left",
sortable: true,
},
{
name: "shell",
label: "Shell",
field: "shell",
align: "left",
sortable: true,
},
{
name: "name",
label: "Name",
field: "name",
align: "left",
sortable: true,
},
{
name: "args",
label: "Default Args",
field: "args",
align: "left",
sortable: true,
},
{
name: "category",
label: "Category",
field: "category",
align: "left",
sortable: true,
},
{
name: "desc",
label: "Description",
field: "description",
align: "left",
sortable: false,
},
{
name: "default_timeout",
label: "Default Timeout (seconds)",
field: "default_timeout",
align: "left",
sortable: true,
},
],
visibleColumns: ["favorite", "name", "args", "category", "desc", "shell", "default_timeout"],
};
},
methods: {
getScripts() {
this.$axios
.get("/scripts/scripts/")
.then(r => {
this.scripts = r.data;
})
.catch(e => {});
},
setShowCommunityScripts(show) {
this.$store.dispatch("setShowCommunityScripts", show);
},
viewCode(script) {
this.$q.dialog({
component: ScriptFormModal,
componentProps: {
script: script,
readonly: true,
},
});
},
favoriteScript(script) {
this.$q.loading.show();
emits: [...useDialogPluginComponent.emits],
setup() {
// setup vuex store
const store = useStore();
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
// setup quasar plugins
const { dialogRef, onDialogHide } = useDialogPluginComponent();
const $q = useQuasar();
// script manager logic
const scripts = ref([]);
async function getScripts() {
loading.value = true;
scripts.value = await fetchScripts();
loading.value = false;
}
function favoriteScript(script) {
loading.value = true;
const notifyText = !script.favorite ? "Script was favorited!" : "Script was removed as a favorite!";
this.$axios
.put(`/scripts/${script.id}/script/`, { favorite: !script.favorite })
.then(() => {
this.getScripts();
this.$q.loading.hide();
this.notifySuccess(notifyText);
})
.catch(() => {
this.$q.loading.hide();
});
},
deleteScript(scriptpk) {
this.$q
.dialog({
title: "Delete script?",
cancel: true,
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
this.$axios
.delete(`/scripts/${scriptpk}/script/`)
.then(r => {
this.getScripts();
this.notifySuccess(r.data);
})
.catch(e => {});
});
},
downloadScript(script) {
this.$axios
.get(`/scripts/${script.id}/download/`)
.then(({ data }) => {
const blob = new Blob([data.code], { type: "text/plain;charset=utf-8" });
let link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = data.filename;
link.click();
})
.catch(e => {});
},
isBuiltInScript(pk) {
try {
return this.scripts.find(i => i.id === pk).script_type === "builtin" ? true : false;
} catch (e) {
return false;
}
},
favoriteText(isFavorite) {
return isFavorite ? "Remove as Favorite" : "Add as Favorite";
},
newScript() {
this.$q
.dialog({
component: ScriptFormModal,
componentProps: {
categories: this.categories,
readonly: false,
},
})
.onOk(() => {
this.getScripts();
});
},
editScript(script) {
this.$q
.dialog({
component: ScriptFormModal,
componentProps: {
script: script,
categories: this.categories,
readonly: false,
},
})
.onOk(() => {
this.getScripts();
});
},
setTableView(view) {
this.tableView = view;
this.expanded = [];
},
cloneScript(script) {
this.$q
.dialog({
component: ScriptFormModal,
componentProps: {
script: script,
categories: this.categories,
readonly: false,
clone: true,
},
})
.onOk(() => {
this.getScripts();
});
},
uploadScript() {
this.$q
.dialog({
component: ScriptUploadModal,
componentProps: {
categories: this.categories,
},
})
.onOk(() => {
this.getScripts();
});
},
},
computed: {
...mapState(["showCommunityScripts"]),
visibleScripts() {
return this.showCommunityScripts ? this.scripts : this.scripts.filter(i => i.script_type !== "builtin");
},
categories() {
editScript({ id: script.id, favorite: !script.favorite });
getScripts();
notifySuccess(notifyText);
} catch (e) {}
loading.value = false;
}
function deleteScript(script) {
$q.dialog({
title: `Delete script: ${script.name}?`,
cancel: true,
ok: { label: "Delete", color: "negative" },
}).onOk(async () => {
loading.value = true;
try {
const data = await removeScript(script.id);
notifySuccess(data);
getScripts();
} catch (e) {}
loading.value = false;
});
}
async function exportScript(script) {
loading.value = true;
const { code, filename } = await downloadScript(script.id);
exportFile(filename, new Blob([code]), { mimeType: "text/plain;charset=utf-8" });
loading.value = false;
}
// table and tree view setup
const search = ref("");
const tableView = ref(true);
const expanded = ref([]);
const loading = ref(false);
const pagination = ref({
rowsPerPage: 0,
sortBy: "favorite",
descending: true,
});
const visibleScripts = computed(() =>
showCommunityScripts.value ? scripts.value : scripts.value.filter(i => i.script_type !== "builtin")
);
const categories = computed(() => {
let list = [];
this.visibleScripts.forEach(script => {
visibleScripts.value.forEach(script => {
if (!!script.category && !list.includes(script.category)) {
list.push(script.category);
}
});
return list;
},
tree() {
if (this.tableView || this.visibleScripts.length === 0) {
});
const tree = computed(() => {
if (tableView.value || visibleScripts.value.length === 0) {
return [];
} else {
let nodes = [];
// copy scripts and categories to new array
let scriptsTemp = Object.assign([], this.visibleScripts);
let categoriesTemp = Object.assign([], this.categories);
let scriptsTemp = Object.assign([], visibleScripts.value);
let categoriesTemp = Object.assign([], categories.value);
// add Unassigned category
categoriesTemp.push("Unassigned");
@@ -561,10 +502,115 @@ export default {
return nodes;
}
},
},
mounted() {
this.getScripts();
});
watch(tableView, () => {
expanded.value = [];
});
// dialog open functions
function viewCodeModal(script) {
$q.dialog({
component: ScriptFormModal,
componentProps: {
script: script,
readonly: true,
},
});
}
function newScriptModal() {
$q.dialog({
component: ScriptFormModal,
componentProps: {
categories: categories.value,
readonly: false,
},
}).onOk(() => {
getScripts();
});
}
function editScriptModal(script) {
$q.dialog({
component: ScriptFormModal,
componentProps: {
script: script,
categories: categories.value,
readonly: false,
},
}).onOk(() => {
getScripts();
});
}
function cloneScriptModal(script) {
$q.dialog({
component: ScriptFormModal,
componentProps: {
script: script,
categories: categories.value,
readonly: false,
clone: true,
},
}).onOk(() => {
getScripts();
});
}
function uploadScriptModal() {
$q.dialog({
component: ScriptUploadModal,
componentProps: {
categories: categories.value,
},
}).onOk(() => {
getScripts();
});
}
// component life cycle hooks
onMounted(getScripts());
return {
// reactive data
search,
tableView,
expanded,
pagination,
loading,
showCommunityScripts,
// computed
visibleScripts,
// non-reactive data
columns,
// api methods
getScripts,
deleteScript,
favoriteScript,
exportScript,
// dialog methods
viewCodeModal,
newScriptModal,
editScriptModal,
cloneScriptModal,
uploadScriptModal,
// table and tree view methods
tree,
setShowCommunityScripts: show => store.dispatch("setShowCommunityScripts", show),
// helper methods
truncateText,
// quasar dialog plugin
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -0,0 +1,174 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="width: 40vw">
<q-bar>
Add Script
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-form id="scriptUploadForm" @submit="submitForm">
<q-card-section>
<q-input label="Name" outlined dense v-model="script.name" :rules="[val => !!val || '*Required']" />
</q-card-section>
<q-card-section>
<q-input label="Description" outlined dense v-model="script.description" />
</q-card-section>
<q-card-section>
<tactical-dropdown
label="Category"
hint="Press Enter or Tab when adding a new value"
outlined
v-model="script.category"
:options="categories"
filterable
clearable
new-value-mode="add-unique"
/>
</q-card-section>
<q-card-section>
<q-file
label="Script Upload"
v-model="file"
hint="Supported file types: .ps1, .bat, .py"
filled
dense
counter
accept=".ps1, .bat, .py"
>
<template v-slot:prepend>
<q-icon name="attach_file" />
</template>
</q-file>
</q-card-section>
<q-card-section>
<q-select
label="Type"
dense
options-dense
outlined
v-model="script.shell"
:options="shellOptions"
emit-value
map-options
/>
</q-card-section>
<q-card-section>
<q-select
label="Script Arguments"
placeholder="(press Enter after typing each argument)"
filled
v-model="script.args"
use-input
use-chips
multiple
dense
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section>
<q-input
label="Default Timeout"
type="number"
outlined
dense
v-model.number="script.default_timeout"
:rules="[val => val >= 5 || 'Minimum is 5']"
/>
</q-card-section>
<q-card-actions>
<q-space />
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn :loading="loading" dense flat label="Add" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
// composition imports
import { ref, watch } from "vue";
import { useDialogPluginComponent } from "quasar";
import { saveScript } from "@/api/scripts";
import { notifySuccess } from "@/utils/notify";
// ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown";
// static data
import { shellOptions } from "@/composables/scripts";
export default {
components: { TacticalDropdown },
name: "ScriptModal",
emits: [...useDialogPluginComponent.emits],
props: {
categories: !Array,
},
setup(props) {
// setup quasar plugins
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// script upload logic
const script = ref({});
const file = ref(null);
const loading = ref(false);
watch(file, (newValue, oldValue) => {
if (newValue) {
// base64 encode the script and delete file
const reader = new FileReader();
reader.onloadend = () => {
script.value.code_base64 = reader.result.replace(/^data:.+;base64,/, "");
};
reader.readAsDataURL(file.value);
} else {
script.value.code_base64 = "";
}
});
async function submitForm() {
loading.value = true;
let result = "";
try {
console.log(script.value);
result = await saveScript(script.value);
onDialogOK();
notifySuccess(result);
} catch (e) {
console.error(e);
}
loading.value = false;
}
return {
// reactive data
script,
file,
loading,
// non-reactive data
shellOptions,
// methods
submitForm,
// quasar dialog
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -15,6 +15,7 @@
label="Select Agent to run script on"
v-model="agent"
:options="agentOptions"
filterable
mapOptions
outlined
/>

View File

@@ -0,0 +1,85 @@
<template>
<prism-editor class="editor" v-model="code" :highlight="highlighter" line-numbers @click="focusTextArea" />
</template>
<script>
// prism package imports
import { PrismEditor } from "vue-prism-editor";
import "vue-prism-editor/dist/prismeditor.min.css";
import { highlight, languages } from "prismjs/components/prism-core";
import "prismjs/components/prism-batch";
import "prismjs/components/prism-python";
import "prismjs/components/prism-powershell";
import "prismjs/themes/prism-tomorrow.css";
export default {
name: "CodeEditor",
components: {
PrismEditor,
},
props: {
code: !String,
shell: !String,
},
setup(props) {
function highlighter(code) {
if (!props.shell) {
return code;
}
let lang = props.shell === "cmd" ? "batch" : props.shell;
return highlight(code, languages[lang]);
}
function focusTextArea() {
document.getElementsByClassName("prism-editor__textarea")[0].focus();
}
return {
//methods
highlighter,
focusTextArea,
};
},
};
</script>
<style>
/* required class */
.editor {
/* we dont use `language-` classes anymore so thats why we need to add background and text color manually */
background: #2d2d2d;
color: #ccc;
/* you must provide font-family font-size line-height. Example: */
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
font-size: 14px;
line-height: 1.5;
padding: 5px;
height: var(--prism-height);
}
/* optional class for removing the outline */
.prism-editor__textarea:focus {
outline: none;
}
.prism-editor__textarea,
.prism-editor__container {
width: 500em !important;
-ms-overflow-style: none;
scrollbar-width: none;
}
.prism-editor__container::-webkit-scrollbar,
.prism-editor__textarea::-webkit-scrollbar {
display: none;
}
.prism-editor__editor {
white-space: pre !important;
}
.prism-editor__container {
overflow-x: auto !important;
}
</style>

View File

@@ -4,34 +4,39 @@ import { formatScriptOptions } from "@/utils/format"
// script dropdown
export function useScriptDropdown() {
const scriptOptions = ref([])
const defaultTimeout = ref(30)
const defaultArgs = ref([])
const scriptPK = ref(null)
const scriptOptions = ref([])
const defaultTimeout = ref(30)
const defaultArgs = ref([])
const scriptPK = ref(null)
// specifing flat returns an array of script names versus {value:id, label: hostname}
async function getScriptOptions(showCommunityScripts = false, flat = false) {
scriptOptions.value = formatScriptOptions(await fetchScripts({ showCommunityScripts }), flat)
}
// specifing flat returns an array of script names versus {value:id, label: hostname}
async function getScriptOptions(showCommunityScripts = false, flat = false) {
scriptOptions.value = formatScriptOptions(await fetchScripts({ showCommunityScripts }), flat)
// watch scriptPk for changes and update the default timeout and args
watch(scriptPK, (newValue, oldValue) => {
if (newValue) {
const script = scriptOptions.value.find(i => i.value === newValue);
defaultTimeout.value = script.timeout;
defaultArgs.value = script.args;
}
})
// watch scriptPk for changes and update the default timeout and args
watch(scriptPK, (newValue, oldValue) => {
if (newValue) {
const script = scriptOptions.value.find(i => i.value === newValue);
defaultTimeout.value = script.timeout;
defaultArgs.value = script.args;
}
})
return {
//data
scriptPK,
scriptOptions,
defaultTimeout,
defaultArgs,
return {
//data
scriptPK,
scriptOptions,
defaultTimeout,
defaultArgs,
//methods
getScriptOptions
}
//methods
getScriptOptions,
}
}
export const shellOptions = [
{ label: "Powershell", value: "powershell" },
{ label: "Batch", value: "cmd" },
{ label: "Python", value: "python" },
];

View File

@@ -159,7 +159,7 @@ export default {
},
async getScriptOptions(showCommunityScripts = false) {
let options = [];
const { data } = await axios.get("/scripts/scripts/")
const { data } = await axios.get("/scripts/")
let scripts;
if (showCommunityScripts) {
scripts = data;
@@ -215,7 +215,9 @@ export default {
return `${a} at ${b}`;
},
truncateText(txt) {
return txt.length >= 60 ? txt.substring(0, 60) + "..." : txt;
if (txt)
return txt.length >= 60 ? txt.substring(0, 60) + "..." : txt;
else return ""
},
}
}

View File

@@ -175,5 +175,7 @@ export function formatTableColumnText(text) {
}
export function truncateText(txt, chars) {
if (!txt) return
return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
}

33
src/utils/notify.js Normal file
View File

@@ -0,0 +1,33 @@
import { Notify } from "quasar";
export function notifySuccess(msg, timeout = 2000) {
Notify.create({
type: "positive",
message: msg,
timeout: timeout
});
}
export function notifyError(msg, timeout = 2000) {
Notify.create({
type: "negative",
message: msg,
timeout: timeout
});
}
export function notifyWarning(msg, timeout = 2000) {
Notify.create({
type: "warning",
message: msg,
timeout: timeout
});
}
export function notifyInfo(msg, timeout = 2000) {
Notify.create({
type: "info",
message: msg,
timeout: timeout
});
}

View File

@@ -901,7 +901,6 @@ export default {
},
beforeUnmount() {
this.ws.close();
this.ws = null;
clearInterval(this.poll);
},
};