Add endpoint monitor catalog and warn-only drift reconciler
Operators need to see when a machine's running services or processes fall outside the desired state defined by policy. This adds a reusable monitor catalog (shipped read-only rows plus user-built local rows), computes drift server-side on agent check-in, and surfaces a warning on the endpoint panels. Remediation stays manual for now; automation can follow once demand is proven. Includes migrations 0006-0008 and the monitors API client.
This commit is contained in:
@@ -18,6 +18,12 @@ export default [
|
||||
component: () => import('./views/AgentKeySettings.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'endpoints' }
|
||||
},
|
||||
{
|
||||
path: 'settings/endpoints/monitors',
|
||||
name: 'endpoints-monitors-settings',
|
||||
component: () => import('./views/MonitorsSettings.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'endpoints' }
|
||||
},
|
||||
// TEMPORARY / DEV ONLY. A manual bench to exercise the update_app and
|
||||
// run_script delivery path end to end while Cameron builds the real UI.
|
||||
// Gated on endpoints.deploy inside the view; remove once the production
|
||||
|
||||
@@ -307,7 +307,7 @@
|
||||
</section>
|
||||
|
||||
<!-- ====================================================================
|
||||
Agent configuration (placeholder)
|
||||
Agent configuration
|
||||
================================================================= -->
|
||||
<section class="settings-section">
|
||||
<h3>Agent configuration</h3>
|
||||
@@ -319,11 +319,16 @@
|
||||
stamped into the build above so a deployed agent needs no local edits.
|
||||
</p>
|
||||
<p class="field-hint">
|
||||
<strong>Coming soon.</strong> These controls arrive with the C# agent
|
||||
runtime. Until then the agent uses its built-in defaults.
|
||||
Monitored processes and services are defined as reusable monitors and
|
||||
attached to PC types through site policies, so a deployed agent fetches
|
||||
the right set for its type.
|
||||
</p>
|
||||
<button class="btn btn-secondary" type="button" disabled>
|
||||
Edit configuration (coming soon)
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
type="button"
|
||||
@click="editConfiguration"
|
||||
>
|
||||
Edit configuration
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
@@ -332,8 +337,10 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import api from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(true)
|
||||
const busy = ref(false)
|
||||
const key = ref(null)
|
||||
@@ -540,6 +547,10 @@ async function buildSiteAgent() {
|
||||
agentbusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function editConfiguration() {
|
||||
router.push({ name: 'endpoints-monitors-settings' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
232
plugins/endpoints/frontend/views/MonitorsSettings.vue
Normal file
232
plugins/endpoints/frontend/views/MonitorsSettings.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Monitor Definitions</h2>
|
||||
<button class="btn btn-primary" @click="openModal()">+ Add Monitor</button>
|
||||
</div>
|
||||
|
||||
<p class="setting-description">
|
||||
A monitor definition is a reusable process or service the agent can watch,
|
||||
with a bespoke display label separate from the raw OS name. Shipped monitors
|
||||
are product-owned and read-only. Attach monitors to PC types through site
|
||||
policies so a deployed agent fetches the right set for its type.
|
||||
</p>
|
||||
|
||||
<div class="card">
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
<template v-else>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Display name</th>
|
||||
<th>Kind</th>
|
||||
<th>OS identity</th>
|
||||
<th>Launch path</th>
|
||||
<th>Active</th>
|
||||
<th>Source</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in monitors" :key="m.endpointmonitorid">
|
||||
<td>{{ m.displayname }}</td>
|
||||
<td>{{ m.kind }}</td>
|
||||
<td class="cell-truncate" :title="osidentity(m)">{{ osidentity(m) || '-' }}</td>
|
||||
<td class="cell-truncate" :title="m.launchpath">{{ m.launchpath || '-' }}</td>
|
||||
<td>{{ m.isactive ? 'Yes' : 'No' }}</td>
|
||||
<td>
|
||||
<span class="badge" :class="m.source === 'local' ? 'badge-secondary' : 'badge-warning'">
|
||||
{{ m.source }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<button class="btn btn-secondary btn-sm" :disabled="m.source !== 'local'" @click="openModal(m)">Edit</button>
|
||||
<button class="btn btn-danger btn-sm" :disabled="m.source !== 'local'" @click="remove(m)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="monitors.length === 0">
|
||||
<td colspan="7" style="text-align:center; color: var(--text-light);">No monitor definitions</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="showModal" class="modal-overlay">
|
||||
<div class="modal modal-lg">
|
||||
<div class="modal-header">
|
||||
<h3>{{ editing ? 'Edit Monitor' : 'Add Monitor' }}</h3>
|
||||
</div>
|
||||
<form @submit.prevent="save">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Display name *</label>
|
||||
<input v-model="form.displayname" type="text" class="form-control" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Kind *</label>
|
||||
<select v-model="form.kind" class="form-control">
|
||||
<option value="process">process</option>
|
||||
<option value="service">service</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="form.kind === 'process'" class="form-group">
|
||||
<label>Process name * <span class="hint">(executable the agent matches, e.g. agent.exe)</span></label>
|
||||
<input v-model="form.processname" type="text" class="form-control" />
|
||||
</div>
|
||||
<div v-else class="form-group">
|
||||
<label>Service name * <span class="hint">(Windows service name)</span></label>
|
||||
<input v-model="form.servicename" type="text" class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Launch path <span class="hint">(optional; starts the app)</span></label>
|
||||
<input v-model="form.launchpath" type="text" class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Data collector path <span class="hint">(optional)</span></label>
|
||||
<input v-model="form.datacollectorpath" type="text" class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Log collector path <span class="hint">(optional)</span></label>
|
||||
<input v-model="form.logcollectorpath" type="text" class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input v-model="form.isactive" type="checkbox" />
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="closeModal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="saving">{{ saving ? 'Saving...' : 'Save' }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { endpointsApi } from '@/api'
|
||||
import { useToast } from '@/composables/toast'
|
||||
import { apiError } from '@/utils/apiError'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const monitors = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
const showModal = ref(false)
|
||||
const editing = ref(null)
|
||||
const saving = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const form = ref(emptyForm())
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
kind: 'process',
|
||||
displayname: '',
|
||||
processname: '',
|
||||
servicename: '',
|
||||
launchpath: '',
|
||||
datacollectorpath: '',
|
||||
logcollectorpath: '',
|
||||
isactive: true
|
||||
}
|
||||
}
|
||||
|
||||
function osidentity(m) {
|
||||
return m.kind === 'service' ? m.servicename : m.processname
|
||||
}
|
||||
|
||||
onMounted(() => loadData())
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await endpointsApi.monitors.list()
|
||||
monitors.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading monitor definitions:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openModal(item = null) {
|
||||
editing.value = item
|
||||
error.value = ''
|
||||
if (item) {
|
||||
form.value = {
|
||||
kind: item.kind || 'process',
|
||||
displayname: item.displayname || '',
|
||||
processname: item.processname || '',
|
||||
servicename: item.servicename || '',
|
||||
launchpath: item.launchpath || '',
|
||||
datacollectorpath: item.datacollectorpath || '',
|
||||
logcollectorpath: item.logcollectorpath || '',
|
||||
isactive: item.isactive !== false
|
||||
}
|
||||
} else {
|
||||
form.value = emptyForm()
|
||||
}
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function closeModal() { showModal.value = false; editing.value = null }
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const payload = {
|
||||
kind: form.value.kind,
|
||||
displayname: form.value.displayname,
|
||||
processname: form.value.processname,
|
||||
servicename: form.value.servicename,
|
||||
launchpath: form.value.launchpath,
|
||||
datacollectorpath: form.value.datacollectorpath,
|
||||
logcollectorpath: form.value.logcollectorpath,
|
||||
isactive: form.value.isactive
|
||||
}
|
||||
if (editing.value) {
|
||||
await endpointsApi.monitors.update(editing.value.endpointmonitorid, payload)
|
||||
} else {
|
||||
await endpointsApi.monitors.create(payload)
|
||||
}
|
||||
closeModal()
|
||||
loadData()
|
||||
} catch (err) {
|
||||
error.value = apiError(err, 'Failed to save monitor')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(m) {
|
||||
if (!confirm(`Delete monitor "${m.displayname}"?`)) return
|
||||
try {
|
||||
await endpointsApi.monitors.remove(m.endpointmonitorid)
|
||||
loadData()
|
||||
} catch (err) {
|
||||
toast.error(apiError(err, 'Failed to delete'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cell-truncate {
|
||||
max-width: 240px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.hint { color: var(--text-light); font-weight: normal; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user