debug log rework

This commit is contained in:
sadnub
2021-06-21 12:20:30 -04:00
parent f32d8b49d1
commit c4dacad41c
24 changed files with 900 additions and 627 deletions

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

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

View File

@@ -3,7 +3,8 @@ import axios from "axios"
const baseUrl = "/agents"
export async function fetchAgents() {
const { data } = await axios.get(`${baseUrl}/listagentsnodetail/`)
return data
try {
const { data } = await axios.get(`${baseUrl}/listagentsnodetail/`)
return data
} catch (e) { }
}

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

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

View File

@@ -1,9 +1,17 @@
import axios from "axios"
const baseUrl = "/logs/debuglog/"
const baseUrl = "/logs"
export async function fetchDebugLog(payload) {
const { data } = await axios.patch(`${baseUrl}`, payload)
try {
const { data } = await axios.patch(`${baseUrl}/debuglog/`, payload)
return data
} catch (e) { }
}
return data
}
export async function fetchAuditLog(payload) {
try {
const { data } = await axios.patch(`${baseUrl}/auditlogs/`, payload)
return data
} catch (e) { }
}

View File

@@ -11,116 +11,49 @@
<div class="text-h6 q-pl-sm q-pt-sm">Filter</div>
<div class="row">
<div class="q-pa-sm col-1">
<q-option-group v-model="filterType" :options="filterTypeOptions" color="primary" @update:model-value="clear" />
<q-option-group v-model="filterType" :options="filterTypeOptions" color="primary" />
</div>
<div class="q-pa-sm col-2" v-if="filterType === 'agents'">
<q-select
new-value-mode="add"
multiple
filled
dense
v-model="agentFilter"
use-input
use-chips
fill-input
input-debounce="3"
label="Agent"
emit-value
:options="agentOptions"
@filter="getAgentOptions"
hint="Start typing the agents name"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">No results</q-item-section>
</q-item>
</template>
<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>
<tactical-dropdown v-model="agentFilter" :options="agentOptions" label="Agent" clearable multiple filled />
</div>
<div class="q-pa-sm col-2" v-if="filterType === 'clients'">
<q-select
clearable
multiple
filled
dense
<tactical-dropdown
v-model="clientFilter"
fill-input
:options="clientOptions"
label="Clients"
map-options
emit-value
:options="clientsOptions"
clearable
multiple
filled
mapOptions
/>
</div>
<div class="q-pa-sm col-2">
<q-select
new-value-mode="add"
multiple
filled
dense
v-model="userFilter"
use-input
use-chips
fill-input
input-debounce="3"
label="User"
emit-value
:options="userOptions"
@filter="getUserOptions"
hint="Start typing the username"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">No results</q-item-section>
</q-item>
</template>
</q-select>
<tactical-dropdown v-model="userFilter" :options="userOptions" label="Users" clearable filled multiple />
</div>
<div class="q-pa-sm col-2">
<q-select
clearable
filled
multiple
use-chips
dense
<tactical-dropdown
v-model="actionFilter"
label="Action"
emit-value
map-options
:options="actionOptions"
/>
</div>
<div class="q-pa-sm col-2">
<q-select
label="Action"
clearable
filled
multiple
use-chips
dense
v-model="objectFilter"
label="Object"
emit-value
map-options
:options="objectOptions"
mapOptions
/>
</div>
<div class="q-pa-sm col-2">
<q-select filled dense v-model="timeFilter" label="Time" emit-value map-options :options="timeOptions">
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">No results</q-item-section>
</q-item>
</template>
</q-select>
<tactical-dropdown
v-model="objectFilter"
:options="objectOptions"
label="Object"
clearable
filled
multiple
mapOptions
/>
</div>
<div class="q-pa-sm col-2">
<tactical-dropdown v-model="timeFilter" :options="timeOptions" label="Time" filled mapOptions />
</div>
<div class="q-pa-sm col-1">
<q-btn color="primary" label="Search" @click="search" />
@@ -130,287 +63,98 @@
<q-card-section>
<q-table
@request="onRequest"
dense
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
class="audit-mgr-tbl-sticky"
binary-state-sort
title="Audit Logs"
:rows="auditLogs"
:columns="columns"
row-key="id"
dense
binary-state-sort
v-model:pagination="pagination"
:rows-per-page-options="[25, 50, 100, 500, 1000]"
:no-data-label="noDataText"
@row-click="showDetails"
virtual-scroll
:no-data-label="tableNoDataText"
@row-click="openAuditDetail"
:loading="loading"
>
<template v-slot:top-right>
<q-btn color="primary" icon-right="archive" label="Export to csv" no-caps @click="exportLog" />
<q-btn dense color="primary" icon-right="archive" @click="exportLog" />
</template>
<template v-slot:body-cell-action="props">
<q-td :props="props">
<div>
<q-badge :color="actionColor(props.value)" :label="actionText(props.value)" />
<q-badge :color="formatActionColor(props.value)" :label="props.value" />
</div>
</q-td>
</template>
</q-table>
</q-card-section>
<div class="q-pa-md q-gutter-sm">
<q-dialog v-model="showLogDetails" @hide="closeDetails">
<AuditLogDetail :log="logDetails" />
</q-dialog>
</div>
</q-card>
</template>
<script>
import AuditLogDetail from "@/components/modals/logs/AuditLogDetail";
import mixins from "@/mixins/mixins";
import { formatAgentOptions } from "@/utils/format";
// composition imports
import { onMounted } from "vue";
import { useAuditLog } from "@/composables/logs";
import { useClientDropdown } from "@/composables/clients";
import { useAgentDropdown } from "@/composables/agents";
import { useUserDropdown } from "@/composables/accounts";
import { exportTableToCSV } from "@/utils/csv";
// ui imported
import TacticalDropdown from "@/components/ui/TacticalDropdown";
export default {
name: "AuditManager",
mixins: [mixins],
components: { AuditLogDetail },
data() {
components: { TacticalDropdown },
setup() {
// setup dropdowns
const { clientOptions, getClientOptions } = useClientDropdown();
const { agentOptions, getAgentOptions } = useAgentDropdown();
const { userOptions, userDropdownLoading, getUserOptions } = useUserDropdown();
onMounted(() => {
getClientOptions();
getAgentOptions(true);
getUserOptions(true);
});
const AuditLog = useAuditLog();
return {
showLogDetails: false,
logDetails: null,
searched: false,
auditLogs: [],
userOptions: [],
agentOptions: [],
agentFilter: null,
userFilter: [],
actionFilter: null,
clientsOptions: [],
clientFilter: null,
objectFilter: null,
timeFilter: 7,
filterType: "clients",
filterTypeOptions: [
{
label: "Clients",
value: "clients",
},
{
label: "Agents",
value: "agents",
},
],
columns: [
{
name: "entry_time",
label: "Time",
field: "entry_time",
align: "left",
sortable: true,
format: (val, row) => this.formatDate(val, true),
},
{ name: "username", label: "Username", field: "username", align: "left", sortable: true },
{ name: "agent", label: "Agent", field: "agent", align: "left", sortable: true },
{ name: "action", label: "Action", field: "action", align: "left", sortable: true },
{
name: "object_type",
label: "Object",
field: "object_type",
align: "left",
sortable: true,
format: (val, row) => this.formatObject(val),
},
{ name: "message", label: "Message", field: "message", align: "left", sortable: true },
],
actionOptions: [
{ value: "agent_install", label: "Agent Installs" },
{ value: "add", label: "Add Object" },
{ value: "bulk_action", label: "Bulk Actions" },
{ value: "check_run", label: "Check Run Results" },
{ value: "execute_command", label: "Execute Command" },
{ value: "execute_script", label: "Execute Script" },
{ value: "delete", label: "Delete Object" },
{ value: "failed_login", label: "Failed User login" },
{ value: "login", label: "User Login" },
{ value: "modify", label: "Modify Object" },
{ value: "remote_session", label: "Remote Session" },
{ value: "task_run", label: "Task Run Results" },
],
timeOptions: [
{ value: 1, label: "1 Day Ago" },
{ value: 7, label: "1 Week Ago" },
{ value: 30, label: "30 Days Ago" },
{ value: 90, label: "3 Months Ago" },
{ value: 180, label: "6 Months Ago" },
{ value: 365, label: "1 Year Ago" },
{ value: 0, label: "Everything" },
],
objectOptions: [
{ value: "agent", label: "Agent" },
{ value: "automatedtask", label: "Automated Task" },
{ value: "bulk", label: "Bulk Actions" },
{ value: "coresettings", label: "Core Settings" },
{ value: "check", label: "Check" },
{ value: "client", label: "Client" },
{ value: "policy", label: "Policy" },
{ value: "site", label: "Site" },
{ value: "script", label: "Script" },
{ value: "user", label: "User" },
{ value: "winupdatepolicy", label: "Patch Policy" },
],
pagination: {
rowsPerPage: 25,
rowsNumber: null,
sortBy: "entry_time",
descending: true,
page: 1,
// data
auditLogs: AuditLog.auditLogs,
agentFilter: AuditLog.agentFilter,
userFilter: AuditLog.userFilter,
actionFilter: AuditLog.actionFilter,
clientFilter: AuditLog.clientFilter,
objectFilter: AuditLog.objectFilter,
timeFilter: AuditLog.timeFilter,
filterType: AuditLog.filterType,
loading: AuditLog.loading,
pagination: AuditLog.pagination,
userOptions,
userDropdownLoading,
// non-reactive data
clientOptions,
agentOptions,
columns: useAuditLog.tableColumns,
actionOptions: useAuditLog.actionOptions,
objectOptions: useAuditLog.objectOptions,
timeOptions: useAuditLog.timeOptions,
filterTypeOptions: useAuditLog.filterTypeOptions,
//computed
tableNoDataText: AuditLog.noDataText,
// methods
search: AuditLog.search,
onRequest: AuditLog.onRequest,
openAuditDetail: AuditLog.openAuditDetail,
formatActionColor: AuditLog.formatActionColor,
exportLog: () => {
exportTableToCSV(auditLogs, useAuditLog.tableColumns);
},
};
},
methods: {
getClients() {
this.$axios
.get("/clients/clients/")
.then(r => {
this.clientsOptions = Object.freeze(r.data.map(client => ({ label: client.name, value: client.id })));
})
.catch(e => {});
},
getUserOptions(val, update, abort) {
if (val.length < 2) {
abort();
return;
}
update(() => {
this.$q.loading.show();
const needle = val.toLowerCase();
let data = {
type: "user",
pattern: needle,
};
this.$axios
.post(`logs/auditlogs/optionsfilter/`, data)
.then(r => {
this.userOptions = Object.freeze(r.data.map(user => user.username));
this.$q.loading.hide();
})
.catch(e => {
this.$q.loading.hide();
});
});
},
getAgentOptions(val, update, abort) {
if (val.length < 2) {
abort();
return;
}
update(() => {
this.$q.loading.show();
const needle = val.toLowerCase();
let data = {
type: "agent",
pattern: needle,
};
this.$axios
.post(`logs/auditlogs/optionsfilter/`, data)
.then(r => {
this.agentOptions = Object.freeze(formatAgentOptions(r.data));
this.$q.loading.hide();
})
.catch(e => {
this.$q.loading.hide();
});
});
},
exportLog() {
exportTableToCSV(this.auditLogs, this.columns);
},
onRequest(props) {
// needed to update external pagination object
const { page, rowsPerPage, sortBy, descending } = props.pagination;
this.pagination.page = page;
this.pagination.rowsPerPage = rowsPerPage;
this.pagination.sortBy = sortBy;
this.pagination.descending = descending;
this.search();
},
search() {
this.$q.loading.show();
this.searched = true;
let data = {
pagination: this.pagination,
};
if (!!this.agentFilter && this.agentFilter.length > 0) data["agentFilter"] = this.agentFilter;
else if (!!this.clientFilter && this.clientFilter.length > 0) data["clientFilter"] = this.clientFilter;
if (!!this.userFilter && this.userFilter.length > 0) data["userFilter"] = this.userFilter;
if (!!this.timeFilter) data["timeFilter"] = this.timeFilter;
if (!!this.actionFilter && this.actionFilter.length > 0) data["actionFilter"] = this.actionFilter;
if (!!this.objectFilter && this.objectFilter.length > 0) data["objectFilter"] = this.objectFilter;
this.$axios
.patch("/logs/auditlogs/", data)
.then(r => {
this.$q.loading.hide();
this.auditLogs = Object.freeze(r.data.audit_logs);
this.pagination.rowsNumber = r.data.total;
})
.catch(e => {
this.$q.loading.hide();
});
},
showDetails(evt, row, index) {
this.logDetails = row;
this.showLogDetails = true;
},
closeDetails() {
this.logDetails = null;
this.showLogDetails = false;
},
actionColor(action) {
if (action === "add") return "success";
else if (action === "agent_install") return "success";
else if (action === "modify") return "warning";
else if (action === "delete") return "negative";
else if (action === "failed_login") return "negative";
else return "primary";
},
actionText(action) {
if (action.includes("_")) {
let text = action.split("_");
return this.capitalize(text[0]) + " " + this.capitalize(text[1]);
} else {
return this.capitalize(action);
}
},
formatObject(text) {
if (text === "winupdatepolicy") return "Patch Policy";
else if (text === "automatedtask") return "Automated Task";
else if (text === "coresettings") return "Core Settings";
else return this.capitalize(text);
},
clear() {
this.clientFilter = null;
this.agentFilter = null;
},
},
computed: {
noDataText() {
return this.searched ? "No data found. Try to refine you search" : "Click search to find audit logs";
},
},
mounted() {
this.getClients();
},
};
</script>

View File

@@ -1,84 +0,0 @@
<template>
<div v-if="!selectedAgent">No agent selected</div>
<div v-else class="bg-grey-10 text-white">
<div class="q-pa-md row">
<tactical-dropdown
class="col-2 q-pr-sm"
v-model="logTypeFilter"
label="Log Type Filter"
:options="logTypeOptions"
dark
clearable
/>
<q-radio dark v-model="logLevelFilter" color="cyan" val="info" label="Info" />
<q-radio dark v-model="logLevelFilter" color="red" val="critical" label="Critical" />
<q-radio dark v-model="logLevelFilter" color="red" val="error" label="Error" />
<q-radio dark v-model="logLevelFilter" color="yellow" val="warning" label="Warning" />
</div>
<q-separator />
<q-table
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
:rows="debugLog"
:columns="columns"
dense
title="Debug Logs"
:pagination="{ sortBy: 'entry_time', descending: true }"
>
<template v-slot:top-right>
<q-btn color="primary" icon-right="archive" label="Export to csv" no-caps @click="exportDebugLog" />
</template>
</q-table>
</div>
</template>
<script>
// composition imports
import { watch, computed } from "vue";
import { useStore } from "vuex";
import { useDebugLog } from "@/composables/logs";
import { exportTableToCSV } from "@/utils/csv";
// ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown";
export default {
name: "DebugTab",
components: {
TacticalDropdown,
},
setup() {
const store = useStore();
const selectedAgent = computed(() => store.state.selectedRow);
const { debugLog, logLevelFilter, logTypeFilter, agentFilter, getDebugLog } = useDebugLog();
watch([logLevelFilter, logTypeFilter], () => getDebugLog());
watch(selectedAgent, (newValue, oldValue) => {
if (newValue) {
agentFilter.value = selectedAgent.value;
getDebugLog();
}
});
return {
// data
debugLog,
logLevelFilter,
logTypeFilter,
// non-reactive data
columns: useDebugLog.debugLogTableColumns,
logTypeOptions: useDebugLog.logTypeOptions,
// computed
selectedAgent,
// methods
getDebugLog,
exportDebugLog: () => {
exportTableToCSV(debugLog.value, useDebugLog.debugLogTableColumns);
},
};
},
};
</script>

View File

@@ -209,7 +209,8 @@
</template>
<script>
import DebugLogModal from "@/components/modals/logs/DebugLogModal";
import DialogWrapper from "@/components/ui/DialogWrapper";
import DebugLog from "@/components/logs/DebugLog";
import PendingActions from "@/components/modals/logs/PendingActions";
import ClientsManager from "@/components/ClientsManager";
import ClientsForm from "@/components/modals/clients/ClientsForm";
@@ -328,7 +329,19 @@ export default {
},
showDebugLog() {
this.$q.dialog({
component: DebugLogModal,
component: DialogWrapper,
componentProps: {
vuecomponent: DebugLog,
noCard: true,
componentProps: {
modal: true,
},
dialogProps: {
maximized: true,
["transition-show"]: "slide-up",
["transition-hide"]: "slide-down",
},
},
});
},
edited() {

View File

@@ -48,10 +48,10 @@
<q-tab-panel name="assets" class="q-pb-xs q-pt-none">
<AssetsTab />
</q-tab-panel>
<q-tab-panel name="debug" class="q-pb-xs q-pt-none">
<q-tab-panel name="debug" class="q-pa-none">
<DebugTab />
</q-tab-panel>
<q-tab-panel name="audit" class="q-pb-xs q-pt-none">
<q-tab-panel name="audit" class="q-pa-none">
<AuditTab />
</q-tab-panel>
</q-tab-panels>
@@ -64,9 +64,9 @@ import ChecksTab from "@/components/ChecksTab";
import AutomatedTasksTab from "@/components/AutomatedTasksTab";
import WindowsUpdates from "@/components/WindowsUpdates";
import SoftwareTab from "@/components/SoftwareTab";
import HistoryTab from "@/components/HistoryTab";
import AuditTab from "@/components/AuditTab";
import DebugTab from "@/components/DebugTab";
import HistoryTab from "@/components/agents/HistoryTab";
import AuditTab from "@/components/agents/AuditTab";
import DebugTab from "@/components/agents/DebugTab";
import AssetsTab from "@/components/AssetsTab";
import NotesTab from "@/components/NotesTab";

View File

@@ -0,0 +1,31 @@
<template>
<div v-if="!selectedAgent">No agent selected</div>
<div v-else>
<DebugLog :agentpk="selectedAgent" />
</div>
</template>
<script>
// composition imports
import { computed } from "vue";
import { useStore } from "vuex";
// ui imports
import DebugLog from "@/components/logs/DebugLog";
export default {
name: "DebugTab",
components: {
DebugLog,
},
setup() {
const store = useStore();
const selectedAgent = computed(() => store.state.selectedRow);
return {
// computed
selectedAgent,
};
},
};
</script>

View File

@@ -0,0 +1,41 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide" maximized transition-show="slide-up" transition-hide="slide-down">
<q-card class="q-dialog-plugin" style="width: 70vw; max-width: 90vw">
<q-bar>
<span class="text-caption">{{ log.message }}</span>
<q-space />
<q-btn dense flat icon="close" v-close-popup />
</q-bar>
<q-card-section class="row scroll" style="max-height: 65vh">
<div class="col-6" v-if="log.before_value !== null">
<div class="text-h6">Before</div>
<pre>{{ JSON.stringify(log.before_value, null, 4) }}</pre>
</div>
<div class="col-6">
<div class="text-h6">After</div>
<pre>{{ JSON.stringify(log.after_value, null, 4) }}</pre>
</div>
</q-card-section>
</q-card>
</q-dialog>
</template>
<script>
import { useDialogPluginComponent } from "quasar";
export default {
name: "AuditLogDetail",
emits: [...useDialogPluginComponent.emits],
props: {
log: !Object,
},
setup() {
const { dialogRef, onDialogHide } = useDialogPluginComponent();
return {
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -0,0 +1,166 @@
<template>
<q-card>
<q-bar>
<q-btn @click="search" class="q-mr-sm" dense flat push icon="refresh" />
<q-space />Audit Manager
<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>
<div class="text-h6 q-pl-sm q-pt-sm">Filter</div>
<div class="row">
<div class="q-pa-sm col-1">
<q-option-group v-model="filterType" :options="filterTypeOptions" color="primary" />
</div>
<div class="q-pa-sm col-2" v-if="filterType === 'agents'">
<tactical-dropdown v-model="agentFilter" :options="agentOptions" label="Agent" clearable multiple filled />
</div>
<div class="q-pa-sm col-2" v-if="filterType === 'clients'">
<tactical-dropdown
v-model="clientFilter"
:options="clientOptions"
label="Clients"
clearable
multiple
filled
mapOptions
/>
</div>
<div class="q-pa-sm col-2">
<tactical-dropdown v-model="userFilter" :options="userOptions" label="Users" clearable filled multiple />
</div>
<div class="q-pa-sm col-2">
<tactical-dropdown
v-model="actionFilter"
:options="actionOptions"
label="Action"
clearable
filled
multiple
mapOptions
/>
</div>
<div class="q-pa-sm col-2">
<tactical-dropdown
v-model="objectFilter"
:options="objectOptions"
label="Object"
clearable
filled
multiple
mapOptions
/>
</div>
<div class="q-pa-sm col-2">
<tactical-dropdown v-model="timeFilter" :options="timeOptions" label="Time" filled mapOptions />
</div>
<div class="q-pa-sm col-1">
<q-btn color="primary" label="Search" @click="search" />
</div>
</div>
<q-separator />
<q-card-section>
<q-table
@request="onRequest"
:title="modal ? 'Audit Logs' : ''"
:rows="auditLogs"
:columns="columns"
row-key="id"
dense
binary-state-sort
v-model:pagination="pagination"
:rows-per-page-options="[25, 50, 100, 500, 1000]"
:no-data-label="tableNoDataText"
@row-click="openAuditDetail"
:loading="loading"
>
<template v-slot:top-right>
<export-table-btn :data="auditLogs" :columns="columns" />
</template>
<template v-slot:body-cell-action="props">
<q-td :props="props">
<div>
<q-badge :color="formatActionColor(props.value)" :label="props.value" />
</div>
</q-td>
</template>
</q-table>
</q-card-section>
</q-card>
</template>
<script>
// composition imports
import { onMounted } from "vue";
import { useAuditLog } from "@/composables/logs";
import { useClientDropdown } from "@/composables/clients";
import { useAgentDropdown } from "@/composables/agents";
import { useUserDropdown } from "@/composables/accounts";
// ui imported
import ExportTableBtn from "@/components/ui/ExportTableBtn"
import TacticalDropdown from "@/components/ui/TacticalDropdown";
import ExportTableBtn from '../ui/ExportTableBtn.vue';
export default {
name: "AuditManager",
components: { TacticalDropdown, ExportTableBtn },
props: {
agentpk:
ExportTableBtn Number,
modal: {
type: Boolean,
default: false
}
},
setup() {
// setup dropdowns
const { clientOptions, getClientOptions } = useClientDropdown();
const { agentOptions, getAgentOptions } = useAgentDropdown();
const { userOptions, userDropdownLoading, getUserOptions } = useUserDropdown();
onMounted(() => {
getClientOptions();
getAgentOptions(true);
getUserOptions(true);
});
const AuditLog = useAuditLog();
return {
// data
auditLogs: AuditLog.auditLogs,
agentFilter: AuditLog.agentFilter,
userFilter: AuditLog.userFilter,
actionFilter: AuditLog.actionFilter,
clientFilter: AuditLog.clientFilter,
objectFilter: AuditLog.objectFilter,
timeFilter: AuditLog.timeFilter,
filterType: AuditLog.filterType,
loading: AuditLog.loading,
pagination: AuditLog.pagination,
userOptions,
userDropdownLoading,
// non-reactive data
clientOptions,
agentOptions,
columns: useAuditLog.tableColumns,
actionOptions: useAuditLog.actionOptions,
objectOptions: useAuditLog.objectOptions,
timeOptions: useAuditLog.timeOptions,
filterTypeOptions: useAuditLog.filterTypeOptions,
//computed
tableNoDataText: AuditLog.noDataText,
// methods
search: AuditLog.search,
onRequest: AuditLog.onRequest,
openAuditDetail: AuditLog.openAuditDetail,
formatActionColor: AuditLog.formatActionColor,
};
},
};
</script>

View File

@@ -0,0 +1,111 @@
<template>
<q-card>
<q-bar v-if="modal">
<q-btn @click="getDebugLog" class="q-mr-sm" dense flat push icon="refresh" />Debug Log
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip content-class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-card-section class="row">
<tactical-dropdown
v-if="!agentpk"
class="col-2 q-pr-sm"
v-model="agentFilter"
label="Agents Filter"
:options="agentOptions"
mapOptions
outlined
clearable
/>
<tactical-dropdown
class="col-2 q-pr-sm"
v-model="logTypeFilter"
label="Log Type Filter"
:options="logTypeOptions"
mapOptions
outlined
clearable
/>
<q-radio v-model="logLevelFilter" color="cyan" val="info" label="Info" />
<q-radio v-model="logLevelFilter" color="red" val="critical" label="Critical" />
<q-radio v-model="logLevelFilter" color="red" val="error" label="Error" />
<q-radio v-model="logLevelFilter" color="yellow" val="warning" label="Warning" />
<q-space />
<export-table-btn v-if="!modal" :data="debugLog" :columns="columns" />
</q-card-section>
<q-separator />
<q-card-section>
<q-table
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
:rows="debugLog"
:columns="columns"
:title="modal ? 'Debug Logs' : ''"
:pagination="{ sortBy: 'entry_time', descending: true }"
dense
binary-state-sort
>
<template v-slot:top-right>
<export-table-btn v-if="modal" :data="debugLog" :columns="columns" />
</template>
</q-table>
</q-card-section>
</q-card>
</template>
<script>
// composition api
import { watch, onMounted } from "vue";
import { useDebugLog } from "@/composables/logs";
import { useAgentDropdown } from "@/composables/agents";
// ui components
import TacticalDropdown from "@/components/ui/TacticalDropdown";
import ExportTableBtn from "@/components/ui/ExportTableBtn.vue";
export default {
name: "LogModal",
components: {
TacticalDropdown,
ExportTableBtn,
},
props: {
agentpk: Number,
modal: {
type: Boolean,
default: false,
},
},
setup(props) {
const { debugLog, logLevelFilter, logTypeFilter, agentFilter, getDebugLog } = useDebugLog();
const { agentOptions, getAgentOptions } = useAgentDropdown();
if (props.agentpk) {
agentFilter.value = props.agentpk;
}
watch([logLevelFilter, agentFilter, logTypeFilter], getDebugLog);
onMounted(() => {
if (!props.agentpk) getAgentOptions();
getDebugLog();
});
return {
// data
debugLog,
logLevelFilter,
logTypeFilter,
agentFilter,
agentOptions,
// non-reactive data
columns: useDebugLog.tableColumns,
logTypeOptions: useDebugLog.logTypeOptions,
// methods
getDebugLog,
};
},
};
</script>

View File

@@ -1,31 +0,0 @@
<template>
<q-card style="width: 70vw; max-width: 90vw">
<q-bar>
<span class="text-caption">{{ log.message }}</span>
<q-space />
<q-btn dense flat icon="close" v-close-popup />
</q-bar>
<q-card-section class="row scroll" style="max-height: 65vh">
<div class="col-6" v-if="log.before_value !== null">
<div class="text-h6">Before</div>
<pre>{{ JSON.stringify(log.before_value, null, 4) }}</pre>
</div>
<div class="col-6">
<div class="text-h6">After</div>
<pre>{{ JSON.stringify(log.after_value, null, 4) }}</pre>
</div>
</q-card-section>
</q-card>
</template>
<script>
export default {
name: "AuditLogDetail",
props: {
log: Object,
},
data() {
return {};
},
};
</script>

View File

@@ -1,105 +0,0 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide" maximized transition-show="slide-up" transition-hide="slide-down">
<q-card class="q-dialog-plugin bg-grey-10 text-white">
<q-bar>
<q-btn @click="getDebugLog" class="q-mr-sm" dense flat push icon="refresh" />Debug Log
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip content-class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-card-section class="row">
<tactical-dropdown
class="col-2 q-pr-sm"
v-model="agentFilter"
label="Agents Filter"
:options="agentOptions"
dark
clearable
/>
<tactical-dropdown
class="col-2 q-pr-sm"
v-model="logTypeFilter"
label="Log Type Filter"
:options="logTypeOptions"
dark
clearable
/>
<q-radio dark v-model="logLevelFilter" color="cyan" val="info" label="Info" />
<q-radio dark v-model="logLevelFilter" color="red" val="critical" label="Critical" />
<q-radio dark v-model="logLevelFilter" color="red" val="error" label="Error" />
<q-radio dark v-model="logLevelFilter" color="yellow" val="warning" label="Warning" />
</q-card-section>
<q-separator />
<q-card-section>
<q-table
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
:rows="debugLog"
:columns="columns"
dense
title="Debug Logs"
:pagination="{ sortBy: 'entry_time', descending: true }"
>
<template v-slot:top-right>
<q-btn color="primary" icon-right="archive" label="Export to csv" no-caps @click="exportDebugLog" />
</template>
</q-table>
</q-card-section>
</q-card>
</q-dialog>
</template>
<script>
// composition api
import { watch, onMounted } from "vue";
import { useDialogPluginComponent } from "quasar";
import { useDebugLog } from "@/composables/logs";
import { useAgentDropdown } from "@/composables/agents";
import { exportTableToCSV } from "@/utils/csv";
// ui components
import TacticalDropdown from "@/components/ui/TacticalDropdown";
export default {
name: "LogModal",
components: {
TacticalDropdown,
},
emits: [...useDialogPluginComponent.emits],
setup() {
const { debugLog, logLevelFilter, logTypeFilter, agentFilter, getDebugLog } = useDebugLog();
const { dialogRef, onDialogHide } = useDialogPluginComponent();
const { agentOptions, getAgentOptions } = useAgentDropdown();
watch([logLevelFilter, agentFilter, logTypeFilter], getDebugLog);
onMounted(() => {
getAgentOptions();
getDebugLog();
});
return {
// data
debugLog,
logLevelFilter,
logTypeFilter,
agentFilter,
agentOptions,
// non-reactive data
columns: useDebugLog.debugLogTableColumns,
logTypeOptions: useDebugLog.logTypeOptions,
// methods
getDebugLog,
exportDebugLog: () => {
exportTableToCSV(debugLog.value, useDebugLog.debugLogTableColumns);
},
// quasar dialog plugin
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -1,6 +1,6 @@
<template>
<q-dialog ref="dialog" @hide="onHide" v-bind="dialogProps">
<q-card class="q-dialog-plugin" :style="`min-width: ${width}vw`">
<q-dialog ref="dialogRef" @hide="onDialogHide" v-bind="dialogProps">
<q-card v-if="!noCard" class="q-dialog-plugin" :style="`min-width: ${width}vw`">
<q-bar>
{{ title }}
<q-space />
@@ -9,16 +9,24 @@
</q-btn>
</q-bar>
<div class="scroll" :style="`height: ${height}vh`">
<component :is="vuecomponent" v-bind="{ ...$attrs, ...componentProps }" @close="onOk" @hide="hide" />
<component
:is="vuecomponent"
v-bind="{ ...$attrs, ...componentProps }"
@close="onDialogOK"
@hide="onDialogHide"
/>
</div>
</q-card>
<component v-else class="q-dialog-plugin" :is="vuecomponent" v-bind="{ ...$attrs, ...componentProps }" />
</q-dialog>
</template>
<script>
import { useDialogPluginComponent } from "quasar";
export default {
name: "DialogWrapper",
emits: ["hide", "ok", "cancel"],
emits: [...useDialogPluginComponent.emits],
props: {
vuecomponent: {},
title: String,
@@ -30,24 +38,24 @@ export default {
type: String,
default: "50",
},
noCard: {
type: Boolean,
default: false,
},
componentProps: Object,
dialogProps: Object,
},
inheritAttrs: false,
methods: {
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
onOk() {
this.$emit("ok");
this.hide();
},
setup() {
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
return {
// quasar dialog plugin
dialogRef,
onDialogHide,
onDialogOK,
onDialogCancel,
};
},
};
</script>

View File

@@ -0,0 +1,22 @@
<template>
<q-btn dense color="primary" icon-right="archive" @click="export">
<q-tooltip>Export table as CSV</q-tooltip>
</q-btn>
</template>
<script>
import { exportTableToCSV } from "@/utils/csv";
export default {
name: "export-table-btn",
props: {
columns: !Array,
data: !Array,
},
setup(props) {
return {
export: () => exportTableToCSV(props.data, props.columns),
};
},
};
</script>

View File

@@ -2,16 +2,17 @@
<q-select
dense
options-dense
outlined
@update:model-value="value => $emit('update:modelValue', value)"
:model-value="modelValue"
map-options
emit-value
:map-options="mapOptions"
:emit-value="mapOptions"
:multiple="multiple"
:use-chips="multiple"
>
<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-label v-html="mapOptions ? scope.opt.label : scope.opt"></q-item-label>
</q-item-section>
</q-item>
<q-item-label v-if="scope.opt.category" header class="q-pa-sm">{{ scope.opt.category }}</q-item-label>
@@ -23,6 +24,14 @@ export default {
name: "tactical-dropdown",
props: {
modelValue: !String,
mapOptions: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
default: false,
},
},
};
</script>

View File

@@ -0,0 +1,44 @@
import { ref } from "vue"
import { fetchUsers } from "@/api/accounts"
import { formatUserOptions } from "@/utils/format"
export function useUserDropdown() {
const userOptions = ref([])
const userDropdownLoading = ref(false)
async function getUserOptions(flat = false) {
userOptions.value = formatUserOptions(await fetchUsers(), flat)
}
function getDynamicUserOptions(val, update, abort) {
if (!val || val.length < 2) {
abort()
return
}
update(async () => {
userDropdownLoading.value = true
const params = {
search: val.toLowerCase()
}
const options = await fetchUsers(params)
userOptions.value = options.map(user => user.username)
userDropdownLoading.value = false
})
}
return {
//data
userOptions,
userDropdownLoading,
//methods
getUserOptions,
getDynamicUserOptions
}
}

View File

@@ -7,8 +7,8 @@ export function useAgentDropdown() {
const agentOptions = ref([])
const getAgentOptions = async () => {
agentOptions.value = formatAgentOptions(await fetchAgents())
async function getAgentOptions(flat = false) {
agentOptions.value = formatAgentOptions(await fetchAgents(), flat)
}
return {

View File

@@ -0,0 +1,37 @@
import { ref } from "vue"
import { fetchClients } from "@/api/clients"
import { formatClientOptions, formatSiteOptions } from "@/utils/format"
export function useClientDropdown() {
const clientOptions = ref([])
async function getClientOptions(flat = false) {
clientOptions.value = formatClientOptions(await fetchClients(), flat)
}
return {
//data
clientOptions,
//methods
getClientOptions
}
}
export function useSiteDropdown() {
const siteOptions = ref([])
async function getSiteOptions() {
siteOptions.value = formatSiteOptions(await fetchSites())
}
return {
//data
siteOptions,
//methods
getSiteOptions
}
}

View File

@@ -1,6 +1,8 @@
import { ref } from "vue"
import { fetchDebugLog } from "@/api/logs"
import { ref, computed, watch } from "vue"
import { useQuasar } from "quasar"
import { fetchDebugLog, fetchAuditLog } from "@/api/logs"
import { formatDate, formatTableColumnText } from "@/utils/format"
import AuditLogDetailModal from "@/components/logs/AuditLogDetailModal";
// debug log
export function useDebugLog() {
@@ -10,7 +12,7 @@ export function useDebugLog() {
const logLevelFilter = ref("info")
const logTypeFilter = ref(null)
const getDebugLog = async () => {
async function getDebugLog() {
const data = {
logLevelFilter: logLevelFilter.value
}
@@ -33,14 +35,14 @@ export function useDebugLog() {
}
useDebugLog.logTypeOptions = [
{ label: "Agent Update", value: "agent_value" },
{ label: "Agent Update", value: "agent_update" },
{ label: "Agent Issues", value: "agent_issues" },
{ label: "Windows Updates", value: "windows_updates" },
{ label: "System Issues", value: "system_issues" },
{ label: "Scripting", value: "scripting" }
]
useDebugLog.debugLogTableColumns = [
useDebugLog.tableColumns = [
{
name: "entry_time",
label: "Time",
@@ -60,4 +62,189 @@ useDebugLog.debugLogTableColumns = [
format: (val, row) => formatTableColumnText(val),
},
{ name: "message", label: "Message", field: "message", align: "left", sortable: true },
]
// audit Log
export function useAuditLog() {
const auditLogs = ref([])
const agentFilter = ref(null)
const userFilter = ref(null)
const actionFilter = ref(null)
const clientFilter = ref(null)
const objectFilter = ref(null)
const timeFilter = ref(7)
const filterType = ref("clients")
const loading = ref(false)
const searched = ref(false)
const pagination = ref({
rowsPerPage: 25,
rowsNumber: null,
sortBy: "entry_time",
descending: true,
page: 1,
})
async function search() {
loading.value = true
searched.value = true;
const data = {
pagination: pagination.value
};
if (!!agentFilter.value && agentFilter.value.length > 0) data["agentFilter"] = agentFilter.value;
else if (!!clientFilter.value && clientFilter.value.length > 0) data["clientFilter"] = clientFilter.value;
if (!!userFilter.value && userFilter.value.length > 0) data["userFilter"] = userFilter.value;
if (!!timeFilter.value) data["timeFilter"] = timeFilter.value;
if (!!actionFilter.value && actionFilter.value.length > 0) data["actionFilter"] = actionFilter.value;
if (!!objectFilter.value && objectFilter.value.length > 0) data["objectFilter"] = objectFilter.value;
const { audit_logs, total } = await fetchAuditLog(data)
auditLogs.value = audit_logs
pagination.value.rowsNumber = total
loading.value = false
}
function onRequest(props) {
const { page, rowsPerPage, sortBy, descending } = props.pagination;
pagination.value.page = page;
pagination.value.rowsPerPage = rowsPerPage;
pagination.value.sortBy = sortBy;
pagination.value.descending = descending;
search();
}
const { dialog } = useQuasar()
function openAuditDetail(evt, log) {
dialog({
component: AuditLogDetailModal,
componentProps: {
log
}
})
}
function formatActionColor(action) {
if (action === "add") return "success";
else if (action === "agent_install") return "success";
else if (action === "modify") return "warning";
else if (action === "delete") return "negative";
else if (action === "failed_login") return "negative";
else return "primary";
}
watch(filterType, () => {
agentFilter.value = null
clientFilter.value = null
})
const noDataText = computed(() => searched ? "No data found. Try to refine you search" : "Click search to find audit logs")
return {
// data
auditLogs,
agentFilter,
userFilter,
actionFilter,
clientFilter,
objectFilter,
timeFilter,
filterType,
loading,
searched,
pagination,
//computed
noDataText,
// methods
search,
onRequest,
openAuditDetail,
formatActionColor
}
}
useAuditLog.tableColumns = [
{
name: "entry_time",
label: "Time",
field: "entry_time",
align: "left",
sortable: true,
format: (val, row) => formatDate(val, true),
},
{ name: "username", label: "Username", field: "username", align: "left", sortable: true },
{ name: "agent", label: "Agent", field: "agent", align: "left", sortable: true },
{
name: "action",
label: "Action",
field: "action",
align: "left",
sortable: true,
format: (val, row) => formatTableColumnText(val)
},
{
name: "object_type",
label: "Object",
field: "object_type",
align: "left",
sortable: true,
format: (val, row) => formatTableColumnText(val),
},
{ name: "message", label: "Message", field: "message", align: "left", sortable: true },
]
useAuditLog.actionOptions = [
{ value: "agent_install", label: "Agent Installs" },
{ value: "add", label: "Add Object" },
{ value: "bulk_action", label: "Bulk Actions" },
{ value: "check_run", label: "Check Run Results" },
{ value: "execute_command", label: "Execute Command" },
{ value: "execute_script", label: "Execute Script" },
{ value: "delete", label: "Delete Object" },
{ value: "failed_login", label: "Failed User login" },
{ value: "login", label: "User Login" },
{ value: "modify", label: "Modify Object" },
{ value: "remote_session", label: "Remote Session" },
{ value: "task_run", label: "Task Run Results" },
]
useAuditLog.objectOptions = [
{ value: "agent", label: "Agent" },
{ value: "automatedtask", label: "Automated Task" },
{ value: "bulk", label: "Bulk Actions" },
{ value: "coresettings", label: "Core Settings" },
{ value: "check", label: "Check" },
{ value: "client", label: "Client" },
{ value: "policy", label: "Policy" },
{ value: "site", label: "Site" },
{ value: "script", label: "Script" },
{ value: "user", label: "User" },
{ value: "winupdatepolicy", label: "Patch Policy" },
]
useAuditLog.timeOptions = [
{ value: 1, label: "1 Day Ago" },
{ value: 7, label: "1 Week Ago" },
{ value: 30, label: "30 Days Ago" },
{ value: 90, label: "3 Months Ago" },
{ value: 180, label: "6 Months Ago" },
{ value: 365, label: "1 Year Ago" },
{ value: 0, label: "Everything" },
]
useAuditLog.filterTypeOptions = [
{
label: "Clients",
value: "clients",
},
{
label: "Agents",
value: "agents",
},
]

View File

@@ -1,35 +1,84 @@
export function formatAgentOptions(data) {
let options = []
const agents = data.map(agent => ({
label: agent.hostname,
value: agent.pk,
cat: `${agent.client} > ${agent.site}`,
}));
// dropdown options formatting
let categories = [];
agents.forEach(option => {
if (!categories.includes(option.cat)) {
categories.push(option.cat);
function _formatOptions(data, { label, value = "id", flat = false, allowDuplicates = true }) {
if (!flat)
// returns array of options in object format [{label: label, value: 1}]
return data.map(i => ({ label: i[label], value: i[value] }));
else
// returns options as an array of strings ["label", "label1"]
if (!allowDuplicates)
return data.map(i => i[label]);
else {
const options = []
data.forEach(i => {
if (!options.includes(i[label]))
options.push(i[label])
});
return options
}
});
}
categories.sort().forEach(cat => {
options.push({ category: cat });
let tmp = []
agents.forEach(agent => {
if (agent.cat === cat) {
tmp.push(agent);
export function formatAgentOptions(data, flat = false) {
if (flat) {
// returns just agent hostnames in array
return _formatOptions(data, { label: "hostname", value: "pk", flat: true, allowDuplicates: false })
} else {
// returns options with categories in object format
let options = []
const agents = data.map(agent => ({
label: agent.hostname,
value: agent.pk,
cat: `${agent.client} > ${agent.site}`,
}));
let categories = [];
agents.forEach(option => {
if (!categories.includes(option.cat)) {
categories.push(option.cat);
}
});
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
options.push(...sorted);
categories.sort().forEach(cat => {
options.push({ category: cat });
let tmp = []
agents.forEach(agent => {
if (agent.cat === cat) {
tmp.push(agent);
}
});
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
options.push(...sorted);
});
return options
}
}
export function formatClientOptions(data, flat = false) {
return _formatOptions(data, { label: "name", flat: flat })
}
export function formatSiteOptions(data, flat = false) {
const options = []
data.forEach(client => {
options.push({ category: client.name });
options.push(..._formatOptions(client.sites, { label: "name", flat: flat }))
});
return options
}
export function formatUserOptions(data, flat = false) {
return _formatOptions(data, { label: "username", flat: flat })
}
// date formatting
function _appendLeadingZeroes(n) {
if (n <= 9) {
return "0" + n;
@@ -46,6 +95,8 @@ export function formatDate(date, includeSeconds = false) {
return includeSeconds ? formatted + ":" + _appendLeadingZeroes(dt.getSeconds()) : formatted
}
// string formatting
export function capitalize(string) {
return string[0].toUpperCase() + string.substring(1);
}