run script rework start

This commit is contained in:
sadnub
2021-07-07 19:28:09 -04:00
parent f3ad5acf1c
commit 0532003c5d
7 changed files with 275 additions and 165 deletions

View File

@@ -14,4 +14,11 @@ export async function fetchAgentHistory(pk) {
const { data } = await axios.get(`${baseUrl}/history/${pk}`)
return data
} catch (e) { }
}
}
export async function runScript(payload) {
try {
const { data } = await axios.post(`${baseUrl}/runscript/`, payload)
return data
} catch (e) { }
}

10
src/api/scripts.js Normal file
View File

@@ -0,0 +1,10 @@
import axios from "axios"
const baseUrl = "/scripts"
export async function fetchScripts(params = {}) {
try {
const { data } = await axios.get(`${baseUrl}/scripts/`, { params: params })
return data
} catch (e) { }
}

View File

@@ -141,7 +141,7 @@
<q-item-section>Send Command</q-item-section>
</q-item>
<q-item clickable v-ripple v-close-popup @click="showRunScript = true">
<q-item clickable v-ripple v-close-popup @click="showRunScript(props.row)">
<q-item-section side>
<q-icon size="xs" name="fas fa-terminal" />
</q-item-section>
@@ -414,10 +414,6 @@
<q-dialog v-model="showAgentRecovery">
<AgentRecovery @close="showAgentRecovery = false" :pk="selectedAgentPk" />
</q-dialog>
<!-- run script modal -->
<q-dialog v-model="showRunScript" persistent>
<RunScript @close="showRunScript = false" :pk="selectedAgentPk" />
</q-dialog>
</div>
</template>
@@ -443,7 +439,6 @@ export default {
PendingActions,
SendCommand,
AgentRecovery,
RunScript,
},
mixins: [mixins],
data() {
@@ -457,7 +452,6 @@ export default {
showEditAgentModal: false,
showRebootLaterModal: false,
showAgentRecovery: false,
showRunScript: false,
showPendingActions: false,
pendingActionAgentPk: null,
favoriteScripts: [],
@@ -803,6 +797,14 @@ export default {
})
.catch(() => {});
},
showRunScript(agent) {
this.$q.dialog({
component: RunScript,
componentProps: {
agent,
},
});
},
},
computed: {
...mapGetters(["selectedAgentPk", "agentTableHeight"]),

View File

@@ -330,6 +330,8 @@ import ScriptOutput from "@/components/modals/checks/ScriptOutput";
import EventLogCheckOutput from "@/components/modals/checks/EventLogCheckOutput";
import CheckGraph from "@/components/graphs/CheckGraph";
import { truncateText } from "@/utils/format";
export default {
name: "ChecksTab",
emits: ["edit"],

View File

@@ -1,174 +1,184 @@
<template>
<q-card :style="{ 'min-width': width }">
<q-card-section class="row items-center">
<div class="text-h6">Run a script on {{ hostname }}</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-form @submit.prevent="send">
<q-card-section>
<q-select
:rules="[val => !!val || '*Required']"
dense
outlined
v-model="scriptPK"
:options="scriptOptions"
label="Select script"
map-options
emit-value
options-dense
@update:model-value="setScriptDefaults"
>
<template v-slot:option="scope">
<q-item v-if="!scope.opt.category" v-bind="scope.itemProps" class="q-pl-lg">
<q-item-section>
<q-item-label v-html="scope.opt.label"></q-item-label>
</q-item-section>
</q-item>
<q-item-label v-if="scope.opt.category" v-bind="scope.itemProps" header class="q-pa-sm">{{
scope.opt.category
}}</q-item-label>
</template>
</q-select>
</q-card-section>
<q-card-section>
<q-select
label="Script Arguments (press Enter after typing each argument)"
filled
v-model="args"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section>
<div class="q-gutter-sm">
<q-radio dense v-model="output" val="wait" label="Wait for Output" @update:model-value="emails = []" />
<q-radio dense v-model="output" val="forget" label="Fire and Forget" @update:model-value="emails = []" />
<q-radio dense v-model="output" val="email" label="Email results" />
</div>
</q-card-section>
<q-card-section v-if="output === 'email'">
<div class="q-gutter-sm">
<q-radio
<q-dialog ref="dialogRef" @hide="onDialogHide" :maximized="maximized">
<q-card class="dialog-plugin">
<q-bar>
Run a script on {{ agent.hostname }}
<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.prevent="sendScript">
<q-card-section>
<tactical-dropdown
:rules="[val => !!val || '*Required']"
dense
v-model="emailmode"
val="default"
label="Use email addresses from global settings"
@update:model-value="emails = []"
outlined
v-model="scriptPK"
:options="scriptOptions"
label="Select script"
mapOptions
options-dense
>
</tactical-dropdown>
</q-card-section>
<q-card-section>
<q-select
label="Script Arguments (press Enter after typing each argument)"
filled
v-model="args"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
<q-radio dense v-model="emailmode" val="custom" label="Custom emails" />
</div>
</q-card-section>
<q-card-section v-if="emailmode === 'custom' && output === 'email'">
<q-select
label="Email recipients (press Enter after typing each email)"
filled
v-model="emails"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section>
<q-input
v-model.number="timeout"
dense
outlined
type="number"
style="max-width: 150px"
label="Timeout (seconds)"
stack-label
:rules="[val => !!val || '*Required', val => val >= 5 || 'Minimum is 5 seconds']"
/>
</q-card-section>
<q-card-actions align="center">
<q-btn :loading="loading" label="Run" color="primary" class="full-width" type="submit" />
</q-card-actions>
<q-card-section v-if="ret !== null" class="q-pl-md q-pr-md q-pt-none q-ma-none scroll" style="max-height: 50vh">
<pre>{{ ret }}</pre>
</q-card-section>
</q-form>
</q-card>
</q-card-section>
<q-card-section>
<div class="q-gutter-sm">
<q-radio dense v-model="output" val="wait" label="Wait for Output" />
<q-radio dense v-model="output" val="forget" label="Fire and Forget" />
<q-radio dense v-model="output" val="email" label="Email results" />
<q-radio dense v-model="output" val="collector" label="Save results to Custom Field" />
<q-radio dense v-model="output" val="note" label="Save results to Agent Notes" />
</div>
</q-card-section>
<q-card-section v-if="output === 'email'">
<div class="q-gutter-sm">
<q-radio
dense
v-model="emailmode"
val="default"
label="Use email addresses from global settings"
@update:model-value="emails = []"
/>
<q-radio dense v-model="emailmode" val="custom" label="Custom emails" />
</div>
</q-card-section>
<q-card-section v-if="emailmode === 'custom' && output === 'email'">
<q-select
label="Email recipients (press Enter after typing each email)"
filled
v-model="emails"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section>
<q-input
v-model.number="timeout"
dense
outlined
type="number"
style="max-width: 150px"
label="Timeout (seconds)"
stack-label
:rules="[val => !!val || '*Required', val => val >= 5 || 'Minimum is 5 seconds']"
/>
</q-card-section>
<q-card-actions align="center">
<q-btn :loading="loading" label="Run" color="primary" class="full-width" type="submit" />
</q-card-actions>
<q-card-section v-if="ret !== null" class="q-pl-md q-pr-md q-pt-none q-ma-none scroll" style="max-height: 50vh">
<pre>{{ ret }}</pre>
</q-card-section>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
import mixins from "@/mixins/mixins";
import { mapState } from "vuex";
// composition imports
import { ref, watch, computed, onMounted } from "vue";
import { useStore } from "vuex";
import { useDialogPluginComponent } from "quasar";
import { useScriptDropdown } from "@/composables/scripts";
import { runScript } from "@/api/agents";
//ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown";
export default {
name: "RunScript",
emits: ["close"],
mixins: [mixins],
emits: [...useDialogPluginComponent.emits],
components: { TacticalDropdown },
props: {
pk: Number,
agent: !Object,
},
data() {
return {
scriptOptions: [],
loading: false,
scriptPK: null,
timeout: 30,
ret: null,
output: "wait",
args: [],
emails: [],
emailmode: "default",
};
},
computed: {
...mapState(["showCommunityScripts"]),
hostname() {
return this.$store.state.agentSummary.hostname;
},
width() {
return this.ret === null ? "40vw" : "70vw";
},
},
methods: {
setScriptDefaults() {
const script = this.scriptOptions.find(i => i.value === this.scriptPK);
setup(props) {
// setup vuex store
const { state } = useStore();
const showCommunityScripts = computed(() => state.showCommunityScripts);
// setup quasar dialog plugin
const { dialogRef, onDialogHide } = useDialogPluginComponent();
// setup dropdowns
const { scriptPK, scriptOptions, defaultTimeout, defaultArgs, getScriptOptions } = useScriptDropdown();
// main run script functionaity
const loading = ref(false);
const output = ref("wait");
const ret = ref(null);
const emails = ref([]);
const emailmode = ref("default");
const maximized = ref(false);
async function sendScript() {
ret.value = null;
loading.value = true;
this.timeout = script.timeout;
this.args = script.args;
},
send() {
this.ret = null;
this.loading = true;
const data = {
pk: this.pk,
timeout: this.timeout,
scriptPK: this.scriptPK,
output: this.output,
args: this.args,
emails: this.emails,
emailmode: this.emailmode,
pk: props.agent.id,
timeout: defaultTimeout.value,
scriptPK: scriptPK.value,
output: output.value,
args: defaultArgs.value,
emails: emails.value,
emailmode: emailmode.value,
};
this.$axios
.post("/agents/runscript/", data)
.then(r => {
if (this.output === "wait") {
this.loading = false;
this.ret = r.data;
} else {
this.loading = false;
this.notifySuccess(r.data);
this.$emit("close");
}
})
.catch(e => {
this.loading = false;
});
},
},
mounted() {
this.getScriptOptions(this.showCommunityScripts).then(options => (this.scriptOptions = Object.freeze(options)));
ret.value = await runScript(data);
loading.value = false;
}
// watchers
watch(output, () => (emails.value = []));
// vue component hooks
onMounted(getScriptOptions(showCommunityScripts.value));
return {
// reactive data
loading,
scriptPK,
scriptOptions,
timeout: defaultTimeout,
output,
ret,
args: defaultArgs,
emails,
emailmode,
maximized,
//methods
sendScript,
// quasar dialog plugin
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -0,0 +1,37 @@
import { ref, watch } from "vue"
import { fetchScripts } from "@/api/scripts"
import { formatScriptOptions } from "@/utils/format"
// script dropdown
export function useScriptDropdown() {
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)
}
// 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,
//methods
getScriptOptions
}
}

View File

@@ -19,6 +19,43 @@ function _formatOptions(data, { label, value = "id", flat = false, allowDuplicat
}
}
export function formatScriptOptions(data, flat = false) {
if (flat) {
// returns just script names in array
return _formatOptions(data, { label: "name", value: "pk", flat: true, allowDuplicates: false })
} else {
let options = [];
let categories = [];
let create_unassigned = false
data.forEach(script => {
if (!!script.category && !categories.includes(script.category)) {
categories.push(script.category);
} else if (!script.category) {
create_unassigned = true
}
});
if (create_unassigned) categories.push("Unassigned")
categories.sort().forEach(cat => {
options.push({ category: cat });
let tmp = [];
data.forEach(script => {
if (script.category === cat) {
tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args });
} else if (cat === "Unassigned" && !script.category) {
tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args });
}
})
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
options.push(...sorted);
});
return options;
}
}
export function formatAgentOptions(data, flat = false) {
if (flat) {
@@ -97,6 +134,7 @@ export function formatDate(date, includeSeconds = false) {
// string formatting
export function capitalize(string) {
return string[0].toUpperCase() + string.substring(1);
}
@@ -109,4 +147,8 @@ export function formatTableColumnText(text) {
words.forEach(word => string = string + " " + capitalize(word))
return string.trim()
}
export function truncateText(txt, chars) {
return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
}