@@ -92,20 +87,5 @@ export default {
name: "AgentDownload",
mixins: [mixins],
props: ["info"],
- methods: {
- downloadMesh() {
- const fileName = this.info.arch === "64" ? "meshagent.exe" : "meshagent-x86.exe";
- this.$axios
- .post(`/agents/${this.info.arch}/getmeshexe/`, {}, { responseType: "blob" })
- .then(({ data }) => {
- const blob = new Blob([data], { type: "application/vnd.microsoft.portable-executable" });
- let link = document.createElement("a");
- link.href = window.URL.createObjectURL(blob);
- link.download = fileName;
- link.click();
- })
- .catch(e => {});
- },
- },
};
\ No newline at end of file
diff --git a/web/src/components/modals/agents/AgentRecovery.vue b/web/src/components/modals/agents/AgentRecovery.vue
index bbb6f0e4..35acc6b0 100644
--- a/web/src/components/modals/agents/AgentRecovery.vue
+++ b/web/src/components/modals/agents/AgentRecovery.vue
@@ -12,7 +12,6 @@
-
@@ -21,16 +20,12 @@
Fix issues with the Mesh Agent which handles take control, live terminal and file browser.
- Fix issues with the TacticalAgent windows service which handles agent check-in.
-
-
- Fix issues with the Tactical RPC service which handles most of the agent's realtime functions and scheduled
- tasks.
+ Fix issues with the Tactical RMM Agent windows service.
Run a shell command on the agent.
You should use the 'Send Command' feature from the agent's context menu for sending shell commands.
- Only use this as a last resort if unable to recover the Tactical RPC service.
+ Only use this as a last resort if unable to recover the Tactical RMM Agent service.
+
+ Agent OS
+
+
+
Agent Type
Shell
-
+
+
+
+
@@ -160,6 +186,8 @@ import { useAgentDropdown } from "@/composables/agents";
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
import { runBulkAction } from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
+import { cmdPlaceholder } from "@/composables/agents";
+import { removeExtraOptionCategories } from "@/utils/format";
// ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown";
@@ -171,6 +199,11 @@ const monTypeOptions = [
{ label: "Workstations", value: "workstations" },
];
+const osTypeOptions = [
+ { label: "Windows", value: "windows" },
+ { label: "Linux", value: "linux" },
+];
+
const targetOptions = [
{ label: "Client", value: "client" },
{ label: "Site", value: "site" },
@@ -178,11 +211,6 @@ const targetOptions = [
{ label: "All", value: "all" },
];
-const shellOptions = [
- { label: "CMD", value: "cmd" },
- { label: "Powershell", value: "powershell" },
-];
-
const patchModeOptions = [
{ label: "Scan", value: "scan" },
{ label: "Install", value: "install" },
@@ -200,6 +228,20 @@ export default {
const store = useStore();
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
+ const shellOptions = computed(() => {
+ if (state.value.osType === "windows") {
+ return [
+ { label: "CMD", value: "cmd" },
+ { label: "Powershell", value: "powershell" },
+ ];
+ } else {
+ return [
+ { label: "Bash", value: "/bin/bash" },
+ { label: "Custom", value: "custom" },
+ ];
+ }
+ });
+
// quasar dialog setup
const { dialogRef, onDialogHide } = useDialogPluginComponent();
@@ -214,8 +256,10 @@ export default {
mode: props.mode,
target: "client",
monType: "all",
+ osType: "windows",
cmd: "",
shell: "cmd",
+ custom_shell: null,
patchMode: "scan",
offlineAgents: false,
client,
@@ -236,6 +280,19 @@ export default {
}
);
+ watch(
+ () => state.value.osType,
+ (newValue, oldValue) => {
+ state.value.custom_shell = null;
+
+ if (newValue === "windows") {
+ state.value.shell = "cmd";
+ } else {
+ state.value.shell = "/bin/bash";
+ }
+ }
+ );
+
async function submit() {
loading.value = true;
@@ -259,6 +316,19 @@ export default {
: "";
});
+ const filteredScriptOptions = computed(() => {
+ if (props.mode !== "script") return [];
+
+ if (state.value.osType === "linux")
+ return removeExtraOptionCategories(
+ scriptOptions.value.filter(script => script.category || script.shell === "shell" || script.shell === "python")
+ );
+ else
+ return removeExtraOptionCategories(
+ scriptOptions.value.filter(script => script.category || script.shell !== "shell")
+ );
+ });
+
// component lifecycle hooks
onMounted(() => {
getAgentOptions();
@@ -273,13 +343,14 @@ export default {
agentOptions,
clientOptions,
siteOptions,
- scriptOptions,
+ filteredScriptOptions,
loading,
+ shellOptions,
// non-reactive data
monTypeOptions,
+ osTypeOptions,
targetOptions,
- shellOptions,
patchModeOptions,
//computed
@@ -287,6 +358,7 @@ export default {
//methods
submit,
+ cmdPlaceholder,
// quasar dialog plugin
dialogRef,
diff --git a/web/src/components/modals/agents/InstallAgent.vue b/web/src/components/modals/agents/InstallAgent.vue
index ccc204aa..95e47170 100644
--- a/web/src/components/modals/agents/InstallAgent.vue
+++ b/web/src/components/modals/agents/InstallAgent.vue
@@ -25,6 +25,28 @@
+
+
+
+
+
+
@@ -44,7 +66,7 @@
/>
-
+
@@ -54,18 +76,27 @@
- OS
+ Arch
-
-
+
+
+
+
+
+
Installation Method
-
-
-
+
+
+
@@ -105,6 +136,7 @@ export default {
info: {},
installMethod: "exe",
arch: "64",
+ agentOS: "windows",
};
},
methods: {
@@ -161,6 +193,7 @@ export default {
arch: this.arch,
api,
fileName,
+ os: this.agentOS,
};
if (this.installMethod === "manual") {
@@ -192,19 +225,24 @@ export default {
.catch(() => {
this.$q.loading.hide();
});
- } else if (this.installMethod === "powershell") {
- const psName = `rmm-${clientStripped}-${siteStripped}-${this.agenttype}.ps1`;
+ } else if (this.installMethod === "powershell" || this.installMethod === "linux") {
+ this.$q.loading.show();
+ let ext = this.installMethod === "powershell" ? "ps1" : "sh";
+ const scriptName = `rmm-${clientStripped}-${siteStripped}-${this.agenttype}.${ext}`;
this.$axios
.post("/agents/installer/", data, { responseType: "blob" })
.then(({ data }) => {
+ this.$q.loading.hide();
const blob = new Blob([data], { type: "text/plain" });
let link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
- link.download = psName;
+ link.download = scriptName;
link.click();
- this.showDLMessage();
+ if (this.installMethod === "powershell") this.showDLMessage();
})
- .catch(e => {});
+ .catch(() => {
+ this.$q.loading.hide();
+ });
}
},
showDLMessage() {
@@ -230,6 +268,9 @@ export default {
case "manual":
text = "Show manual installation instructions";
break;
+ case "linux":
+ text = "Download linux install script";
+ break;
}
return text;
diff --git a/web/src/components/modals/agents/RunScript.vue b/web/src/components/modals/agents/RunScript.vue
index eab516fe..d4dcf5a9 100644
--- a/web/src/components/modals/agents/RunScript.vue
+++ b/web/src/components/modals/agents/RunScript.vue
@@ -19,7 +19,7 @@
// composition imports
-import { ref, watch } from "vue";
+import { ref, watch, computed } from "vue";
import { useDialogPluginComponent, openURL } from "quasar";
import { useScriptDropdown } from "@/composables/scripts";
import { useCustomFieldDropdown } from "@/composables/core";
import { runScript } from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
-import { formatScriptSyntax } from "@/utils/format";
+import { formatScriptSyntax, removeExtraOptionCategories } from "@/utils/format";
+
//ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown";
@@ -136,13 +137,14 @@ export default {
// setup dropdowns
const { script, scriptOptions, defaultTimeout, defaultArgs, syntax, link } = useScriptDropdown(props.script, {
onMount: true,
+ filterByPlatform: props.agent.plat,
});
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
// main run script functionaity
const state = ref({
output: "wait",
- email: [],
+ emails: [],
emailMode: "default",
custom_field: null,
save_all_output: false,
@@ -171,6 +173,17 @@ export default {
link.value ? openURL(link.value) : null;
}
+ const filteredScriptOptions = computed(() => {
+ if (props.agent.plat === "linux")
+ return removeExtraOptionCategories(
+ scriptOptions.value.filter(script => script.category || script.shell === "shell" || script.shell === "python")
+ );
+ else
+ return removeExtraOptionCategories(
+ scriptOptions.value.filter(script => script.category || script.shell !== "shell")
+ );
+ });
+
// watchers
watch([() => state.value.output, () => state.value.emailMode], () => (state.value.emails = []));
@@ -178,7 +191,7 @@ export default {
// reactive data
state,
loading,
- scriptOptions,
+ filteredScriptOptions,
link,
syntax,
ret,
diff --git a/web/src/components/modals/agents/SendCommand.vue b/web/src/components/modals/agents/SendCommand.vue
index d2db2c0f..c8ad7a33 100644
--- a/web/src/components/modals/agents/SendCommand.vue
+++ b/web/src/components/modals/agents/SendCommand.vue
@@ -12,10 +12,29 @@
Shell
-
-
+
+
+
+
+
+
+
@@ -63,6 +78,7 @@
import { ref } from "vue";
import { useDialogPluginComponent } from "quasar";
import { sendAgentCommand } from "@/api/agents";
+import { cmdPlaceholder } from "@/composables/agents";
export default {
name: "SendCommand",
@@ -76,9 +92,10 @@ export default {
// run command logic
const state = ref({
- shell: "cmd",
+ shell: props.agent.plat === "windows" ? "cmd" : "/bin/bash",
cmd: null,
timeout: 30,
+ custom_shell: null,
});
const loading = ref(false);
@@ -103,6 +120,7 @@ export default {
// methods
submit,
+ cmdPlaceholder,
// quasar dialog
dialogRef,
diff --git a/web/src/components/modals/coresettings/EditCoreSettings.vue b/web/src/components/modals/coresettings/EditCoreSettings.vue
index a8612851..d0ac15a6 100644
--- a/web/src/components/modals/coresettings/EditCoreSettings.vue
+++ b/web/src/components/modals/coresettings/EditCoreSettings.vue
@@ -317,9 +317,9 @@
-
+ Mesh Device Group Name:
-
+
@@ -433,7 +433,6 @@ import CustomFields from "@/components/modals/coresettings/CustomFields";
import KeyStoreTable from "@/components/modals/coresettings/KeyStoreTable";
import URLActionsTable from "@/components/modals/coresettings/URLActionsTable";
import APIKeysTable from "@/components/core/APIKeysTable";
-import UploadMesh from "@/components/core/UploadMesh";
export default {
name: "EditCoreSettings",
@@ -596,11 +595,6 @@ export default {
this.$q.loading.hide();
});
},
- uploadMeshAgentModal() {
- this.$q.dialog({
- component: UploadMesh,
- });
- },
},
mounted() {
this.getCoreSettings();
diff --git a/web/src/components/scripts/ScriptFormModal.vue b/web/src/components/scripts/ScriptFormModal.vue
index fbb83aca..cf998069 100644
--- a/web/src/components/scripts/ScriptFormModal.vue
+++ b/web/src/components/scripts/ScriptFormModal.vue
@@ -84,7 +84,7 @@
// composable imports
-import { ref, computed, onMounted } from "vue";
+import { ref, computed, watch, onMounted } from "vue";
import { useQuasar, useDialogPluginComponent } from "quasar";
import { saveScript, editScript, downloadScript } from "@/api/scripts";
import { useAgentDropdown } from "@/composables/agents";
@@ -142,6 +142,7 @@ import { VAceEditor } from "vue3-ace-editor";
import "ace-builds/src-noconflict/mode-powershell";
import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/mode-batchfile";
+import "ace-builds/src-noconflict/mode-sh";
import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
import "ace-builds/src-noconflict/theme-tomorrow";
@@ -185,6 +186,16 @@ export default {
const loading = ref(false);
const agentLoading = ref(false);
+ // watch(script.value, (newValue, oldValue) => {
+ // if (!props.script && script.value.script_body === "") {
+ // if (newValue.shell === "shell") {
+ // script.value.script_body = "#!/bin/bash\n\n# don't forget to include the shebang above!\n\n";
+ // } else if (newValue.shell === "python") {
+ // script.value.script_body = "#!/usr/bin/python3\n\n# don't forget to include the shebang above!\n\n";
+ // }
+ // }
+ // });
+
const title = computed(() => {
if (props.script) {
return props.readonly
@@ -197,6 +208,15 @@ export default {
}
});
+ // convert highlighter language to match what ace expects
+ const lang = computed(() => {
+ if (script.value.shell === "cmd") return "batchfile";
+ else if (script.value.shell === "powershell") return "powershell";
+ else if (script.value.shell === "python") return "python";
+ else if (script.value.shell === "shell") return "sh";
+ else return "";
+ });
+
// get code if editing or cloning script
if (props.script)
downloadScript(script.value.id, { with_snippets: props.readonly }).then(r => {
@@ -218,7 +238,9 @@ export default {
onDialogOK();
notifySuccess(result);
- } catch (e) {}
+ } catch (e) {
+ console.error(e);
+ }
loading.value = false;
}
@@ -248,6 +270,7 @@ export default {
agentOptions,
agent,
agentLoading,
+ lang,
// non-reactive data
shellOptions,
diff --git a/web/src/components/scripts/ScriptManager.vue b/web/src/components/scripts/ScriptManager.vue
index dd3204a8..9f05b841 100644
--- a/web/src/components/scripts/ScriptManager.vue
+++ b/web/src/components/scripts/ScriptManager.vue
@@ -106,6 +106,9 @@
Batch
+
+ Shell
+
{{ props.node.name }}
{{ props.node.description }}
@@ -289,6 +292,9 @@
Batch
+
+ Shell
+
diff --git a/web/src/components/scripts/ScriptSnippetFormModal.vue b/web/src/components/scripts/ScriptSnippetFormModal.vue
index 5bac88db..40cf1813 100644
--- a/web/src/components/scripts/ScriptSnippetFormModal.vue
+++ b/web/src/components/scripts/ScriptSnippetFormModal.vue
@@ -40,7 +40,7 @@
{
+ if (snippet.value.shell === "cmd") return "batchfile";
+ else if (snippet.value.shell === "powershell") return "powershell";
+ else if (snippet.value.shell === "python") return "python";
+ else if (snippet.value.shell === "shell") return "sh";
+ else return "";
+ });
+
async function submitForm() {
loading.value = true;
try {
@@ -121,6 +131,7 @@ export default {
// reactive data
formSnippet: snippet.value,
maximized,
+ lang,
loading,
// non-reactive data
diff --git a/web/src/components/scripts/ScriptSnippets.vue b/web/src/components/scripts/ScriptSnippets.vue
index 755b6cd8..fb23e760 100644
--- a/web/src/components/scripts/ScriptSnippets.vue
+++ b/web/src/components/scripts/ScriptSnippets.vue
@@ -72,6 +72,9 @@
Batch
+
+ Shell
+
{{ props.row.name }}
diff --git a/web/src/components/scripts/ScriptUploadModal.vue b/web/src/components/scripts/ScriptUploadModal.vue
index ce748a62..be91e9f8 100644
--- a/web/src/components/scripts/ScriptUploadModal.vue
+++ b/web/src/components/scripts/ScriptUploadModal.vue
@@ -34,11 +34,11 @@
diff --git a/web/src/composables/agents.js b/web/src/composables/agents.js
index d3802139..3f0847a5 100644
--- a/web/src/composables/agents.js
+++ b/web/src/composables/agents.js
@@ -24,3 +24,9 @@ export function useAgentDropdown() {
getAgentOptions
}
}
+
+export function cmdPlaceholder(shell) {
+ if (shell === "cmd") return "rmdir /S /Q C:\\Windows\\System32";
+ else if (shell === "powershell") return "Remove-Item -Recurse -Force C:\\Windows\\System32";
+ else return "rm -rf --no-preserve-root /";
+}
diff --git a/web/src/composables/scripts.js b/web/src/composables/scripts.js
index 31963285..93384f21 100644
--- a/web/src/composables/scripts.js
+++ b/web/src/composables/scripts.js
@@ -13,9 +13,9 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
const link = ref("")
const baseUrl = "https://github.com/amidaware/community-scripts/blob/main/scripts/"
- // specifing flat returns an array of script names versus {value:id, label: hostname}
- async function getScriptOptions(showCommunityScripts = false, flat = false) {
- scriptOptions.value = Object.freeze(formatScriptOptions(await fetchScripts({ showCommunityScripts }), flat))
+ // specify parameters to filter out community scripts
+ async function getScriptOptions(showCommunityScripts = false) {
+ scriptOptions.value = Object.freeze(formatScriptOptions(await fetchScripts({ showCommunityScripts })))
}
// watch scriptPk for changes and update the default timeout and args
@@ -25,7 +25,7 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
defaultTimeout.value = tmpScript.timeout;
defaultArgs.value = tmpScript.args;
syntax.value = tmpScript.syntax
- link.value = `${baseUrl}${tmpScript.filename}`
+ link.value = tmpScript.script_type === "builtin" ? `${baseUrl}${tmpScript.filename}` : null
}
})
@@ -53,4 +53,5 @@ export const shellOptions = [
{ label: "Powershell", value: "powershell" },
{ label: "Batch", value: "cmd" },
{ label: "Python", value: "python" },
+ { label: "Shell", value: "shell" }
];
\ No newline at end of file
diff --git a/web/src/store/index.js b/web/src/store/index.js
index 889a3119..b0612fed 100644
--- a/web/src/store/index.js
+++ b/web/src/store/index.js
@@ -13,6 +13,7 @@ export default function () {
treeReady: false,
selectedTree: "",
selectedRow: null,
+ agentPlatform: "windows",
agentTableLoading: false,
needrefresh: false,
refreshSummaryTab: false,
@@ -55,6 +56,9 @@ export default function () {
setActiveRow(state, agent_id) {
state.selectedRow = agent_id;
},
+ setAgentPlatform(state, agentPlatform) {
+ state.agentPlatform = agentPlatform;
+ },
retrieveToken(state, { token, username }) {
state.token = token;
state.username = username;
diff --git a/web/src/utils/format.js b/web/src/utils/format.js
index cfe2befe..fcb4ceda 100644
--- a/web/src/utils/format.js
+++ b/web/src/utils/format.js
@@ -3,6 +3,17 @@ import { validateTimePeriod } from "@/utils/validation"
// dropdown options formatting
+export function removeExtraOptionCategories(array) {
+ let tmp = []
+ for (let i = 0; i < array.length; i++) {
+ if (!(array[i].category && array[i + 1].category)) {
+ tmp.push(array[i])
+ }
+ }
+
+ return tmp
+}
+
function _formatOptions(data, { label, value = "id", flat = false, allowDuplicates = true }) {
if (!flat)
// returns array of options in object format [{label: label, value: 1}]
@@ -21,41 +32,35 @@ 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 {
+export function formatScriptOptions(data) {
+ 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
+ }
+ });
- let options = [];
- let categories = [];
- let create_unassigned = false
+ if (create_unassigned) categories.push("Unassigned")
+
+ categories.sort().forEach(cat => {
+ options.push({ category: cat });
+ let tmp = [];
data.forEach(script => {
- if (!!script.category && !categories.includes(script.category)) {
- categories.push(script.category);
- } else if (!script.category) {
- create_unassigned = true
+ if (script.category === cat) {
+ tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax, script_type: script.script_type, shell: script.shell });
+ } else if (cat === "Unassigned" && !script.category) {
+ tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax, script_type: script.script_type, shell: script.shell });
}
- });
+ })
+ const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
+ options.push(...sorted);
+ });
- 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, filename: script.filename, syntax: script.syntax });
- } else if (cat === "Unassigned" && !script.category) {
- tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax });
- }
- })
- const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
- options.push(...sorted);
- });
-
- return options;
- }
+ return options;
}
export function formatAgentOptions(data, flat = false, value_field = "agent_id") {
diff --git a/web/src/views/Dashboard.vue b/web/src/views/Dashboard.vue
index f32b83a4..6c4e800b 100644
--- a/web/src/views/Dashboard.vue
+++ b/web/src/views/Dashboard.vue
@@ -367,6 +367,13 @@ export default {
name: "dashboardalert",
align: "left",
},
+ {
+ name: "plat",
+ label: "",
+ field: "plat",
+ sortable: true,
+ align: "left",
+ },
{
name: "checks-status",
align: "left",
@@ -458,6 +465,7 @@ export default {
],
visibleColumns: [
"smsalert",
+ "plat",
"emailalert",
"dashboardalert",
"checks-status",
diff --git a/web/src/views/InitialSetup.vue b/web/src/views/InitialSetup.vue
index 6a1f08cc..c450d462 100644
--- a/web/src/views/InitialSetup.vue
+++ b/web/src/views/InitialSetup.vue
@@ -28,24 +28,6 @@
Default timezone for agents:
-
-
-
-
-
-
-
-
-
@@ -71,7 +53,6 @@ export default {
site: {
name: "",
},
- meshagent: null,
allTimezones: [],
timezone: null,
arch: "64",
@@ -88,19 +69,11 @@ export default {
};
this.$axios
.post("/clients/", data)
- .then(r => {
- let formData = new FormData();
- formData.append("arch", this.arch);
- formData.append("meshagent", this.meshagent);
- this.$axios
- .put("/core/uploadmesh/", formData)
- .then(() => {
- this.$q.loading.hide();
- this.$router.push({ name: "Dashboard" });
- })
- .catch(e => this.$q.loading.hide());
+ .then(() => {
+ this.$q.loading.hide();
+ this.$router.push({ name: "Dashboard" });
})
- .catch(e => this.$q.loading.hide());
+ .catch(() => this.$q.loading.hide());
},
getSettings() {
this.$axios
@@ -109,7 +82,7 @@ export default {
this.allTimezones = Object.freeze(r.data.all_timezones);
this.timezone = r.data.default_time_zone;
})
- .catch(e => {});
+ .catch(() => {});
},
},
mounted() {
diff --git a/web/src/views/RemoteBackground.vue b/web/src/views/RemoteBackground.vue
index dda89e06..0d4cd271 100644
--- a/web/src/views/RemoteBackground.vue
+++ b/web/src/views/RemoteBackground.vue
@@ -12,9 +12,14 @@
>
-
+
-
+
@@ -27,11 +32,11 @@
-
-
+
+
-
-
+
+