Ports the corporate-GitHub wilm branch onto main, keeping the two branches
separate. Thirteen upstream commits arrive with their authorship intact; the
review fixes and the portability work below are on top.
What arrives:
endpoints plugin - a ShopDB agent reporting endpoint state, telemetry,
services and interfaces, with a command channel the agent
long-polls. Ingest stays on the generic collector route
(ADR-006); the agent key is a UI-managed single-scope
token whose revocation is an instant kill switch.
computers policies - Configuration and Business Policies (RMM-style), scope
trees validated at write time, actions checked against
the endpoints command allowlist. Feature-gated: every
route 404s while the setting is off, so enabling it can
never half-expose the surface.
asset CSV import - templates, a collector script carrying the site path, and
a validate-then-commit import. A plugin can own a virtual
column plus a post-write row hook, which is how a PC's
machine number becomes a controls link without a
hand-built relationships sheet.
agent/ - the Windows agent and its installer.
Contract 0.21.0 -> 0.24.0, additive throughout: register_import_hook (0.22),
mint/list/revoke_service_token (0.23), collector_authorized (0.24). The stability
table had no rows for any of them and now does.
MySQL 5.6 portability. The branch stored three policy columns as sa.JSON, the
only JSON columns in the product, and 5.6 has no JSON type: the migration failed
with a bare syntax error near the word JSON. They are TEXT now, through a
JSONText type decorator, so all seventeen call sites still read and write dicts.
The convention elsewhere is already TEXT (guides.blocksjson,
geenforce.manifestjson).
Review fixes carried on top:
- six policy GETs were jwt_required(optional=True), which authenticates a
caller without checking anything; they require computers.view now, and six
redundant jwt_required decorators are gone
- the agent installer took a default BaseUrl and CollectorKey, so a mistyped
run silently pointed an endpoint at the wrong instance; both are mandatory
and a non-https BaseUrl throws unless -AllowInsecureHttp is passed
- a committing CSV import wrote no audit row
Documentation: the 34 new routes are described in docs/api-inventory.json, which
is hand-maintained on purpose, and the generated spec now covers 331 paths and
477 operations across 22 surfaces. DEVELOPMENT-SETUP's 1071 troubleshooting row
said "use MySQL 8 for dev" and now gives the actual fix, including the trap that
5.6 downgrades ROW_FORMAT=DYNAMIC to Compact with a warning instead of an error.
Full suite 2123 passed, vitest 260 passed, all seventeen plugin chains migrate
clean on MySQL 5.6 and the naming, docs, parity and read-surface gates are green.
294 lines
10 KiB
Vue
294 lines
10 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>Endpoints</h2>
|
|
</div>
|
|
|
|
<p class="setting-description">
|
|
Manage how shop-floor endpoint agents authenticate to this server, build
|
|
the agent for this site, and set what it reports and how often.
|
|
</p>
|
|
|
|
<!-- ====================================================================
|
|
Key management
|
|
================================================================= -->
|
|
<section class="settings-section">
|
|
<h3>Key management</h3>
|
|
<p class="field-hint">
|
|
The endpoint agent reports with a single managed key scoped to endpoint
|
|
telemetry only. It is separate from every other collector, so rotating
|
|
or revoking it stops all endpoint-agent traffic immediately without
|
|
touching the PC or GE-Enforce collectors.
|
|
</p>
|
|
|
|
<div class="card form-card">
|
|
<div v-if="message" class="settings-success">{{ message }}</div>
|
|
<div v-if="error" class="error-message">{{ error }}</div>
|
|
|
|
<div v-if="loading" class="field-hint">Loading...</div>
|
|
|
|
<template v-else>
|
|
<div class="form-group">
|
|
<label>Status</label>
|
|
<p v-if="key" class="field-hint">
|
|
Active key <code class="mono">{{ key.tokenprefix }}...</code>,
|
|
created {{ formatDate(key.createddate) }}.
|
|
<template v-if="key.lastusedat">
|
|
Last used {{ formatDate(key.lastusedat) }}.
|
|
</template>
|
|
<template v-else>Not used yet.</template>
|
|
</p>
|
|
<p v-else class="field-hint">
|
|
No endpoint agent key is active. Generate one to let agents report
|
|
with a managed credential.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Bootstrap .env fallback</label>
|
|
<p class="field-hint">
|
|
<template v-if="envkeyfallback">
|
|
<strong>On.</strong> The bootstrap COLLECTOR_API_KEY still
|
|
authorizes endpoint reports. Generating a key turns this off so
|
|
the managed key becomes the only credential.
|
|
</template>
|
|
<template v-else>
|
|
<strong>Off.</strong> Only the managed key above authorizes
|
|
endpoint reports, so revoking it is an instant kill switch.
|
|
</template>
|
|
</p>
|
|
</div>
|
|
|
|
<div class="button-row">
|
|
<button class="btn btn-primary" @click="regenerate"
|
|
:disabled="busy">
|
|
{{ busy ? 'Working...' : (key ? 'Regenerate key' : 'Generate key') }}
|
|
</button>
|
|
<button v-if="key" class="btn btn-danger" @click="revoke"
|
|
:disabled="busy">
|
|
Revoke key
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="secret" class="card form-card secret-card">
|
|
<h4>Save this key now</h4>
|
|
<p class="field-hint">
|
|
It will not be shown again. Configure it as the agent
|
|
<code>X-API-Key</code> header value.
|
|
</p>
|
|
<p class="mono secret-value">{{ secret }}</p>
|
|
<button class="btn btn-secondary" @click="copySecret">
|
|
{{ copied ? 'Copied' : 'Copy to clipboard' }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="card form-card">
|
|
<h4>How it works</h4>
|
|
<ul class="field-hint info-list">
|
|
<li>
|
|
<strong>Generate</strong> mints a fresh key and, on the first
|
|
generation, turns off the bootstrap <code>.env</code> fallback so
|
|
the managed key becomes the only accepted credential. The full
|
|
secret is shown once - store it in the agent config as the
|
|
<code>X-API-Key</code> header.
|
|
</li>
|
|
<li>
|
|
<strong>Regenerate</strong> is a hard cutover: the previous key is
|
|
revoked in the same moment the new one is minted. Every deployed
|
|
agent must be reconfigured with the new key before it can report
|
|
again.
|
|
</li>
|
|
<li>
|
|
<strong>Revoke</strong> deactivates the key and leaves it off. All
|
|
endpoint-agent reporting stops until you generate a new key. This is
|
|
the kill switch.
|
|
</li>
|
|
</ul>
|
|
|
|
<h4>When to rotate</h4>
|
|
<ul class="field-hint info-list">
|
|
<li>
|
|
<strong>An installer or agent left the secure environment.</strong>
|
|
If a build carrying the key is copied off the shop floor - onto an
|
|
engineer's laptop, a USB stick, or a machine that leaves the site -
|
|
treat the key as compromised and regenerate immediately.
|
|
</li>
|
|
<li>
|
|
<strong>A device is lost, stolen, or decommissioned</strong> while
|
|
still holding the key, and you cannot confirm the key was wiped.
|
|
</li>
|
|
<li>
|
|
<strong>Staff offboarding.</strong> Someone who could read the key
|
|
from a config or build pipeline is leaving; rotate as part of the
|
|
offboarding checklist.
|
|
</li>
|
|
<li>
|
|
<strong>Scheduled hygiene.</strong> Rotate on a routine cadence
|
|
(for example quarterly) so a silently leaked key has a bounded
|
|
lifetime.
|
|
</li>
|
|
<li>
|
|
<strong>Suspicious traffic.</strong> The Fleet view shows reports
|
|
from a host you do not recognize - revoke now, investigate, then
|
|
regenerate and redeploy to known-good endpoints.
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ====================================================================
|
|
Build site agent (placeholder)
|
|
================================================================= -->
|
|
<section class="settings-section">
|
|
<h3>Build site agent</h3>
|
|
<div class="card form-card">
|
|
<p class="field-hint">
|
|
Produce an instance-locked agent executable stamped with this server's
|
|
URL, the endpoint agent key, and the reporting cadence, so it deploys
|
|
with no manual configuration on the client.
|
|
</p>
|
|
<p class="field-hint">
|
|
<strong>Coming soon.</strong> The build pipeline is not wired up yet -
|
|
the C# agent runtime is still under development. Generate a key above
|
|
and configure it manually as the agent <code>X-API-Key</code> for now.
|
|
</p>
|
|
<button class="btn btn-secondary" type="button" disabled>
|
|
Build agent (coming soon)
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ====================================================================
|
|
Agent configuration (placeholder)
|
|
================================================================= -->
|
|
<section class="settings-section">
|
|
<h3>Agent configuration</h3>
|
|
<div class="card form-card">
|
|
<p class="field-hint">
|
|
Define what the agent collects and how often: reporting cadence,
|
|
monitored processes and services, the curated Windows event list, and
|
|
whether unknown hosts self-adopt on first contact. These settings are
|
|
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.
|
|
</p>
|
|
<button class="btn btn-secondary" type="button" disabled>
|
|
Edit configuration (coming soon)
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import api from '@/api'
|
|
|
|
const loading = ref(true)
|
|
const busy = ref(false)
|
|
const key = ref(null)
|
|
const envkeyfallback = ref(true)
|
|
const secret = ref('')
|
|
const copied = ref(false)
|
|
const message = ref('')
|
|
const error = ref('')
|
|
|
|
function formatDate(value) {
|
|
if (!value) return ''
|
|
const parsed = new Date(value)
|
|
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString()
|
|
}
|
|
|
|
function applyStatus(data) {
|
|
key.value = data.key || null
|
|
envkeyfallback.value = !!data.envkeyfallback
|
|
}
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const response = await api.get('/endpoints/agentkey')
|
|
applyStatus(response.data.data || {})
|
|
} catch (loadError) {
|
|
error.value = loadError.response?.status === 403
|
|
? 'Only an administrator can manage the endpoint agent key'
|
|
: 'Could not load the endpoint agent key status'
|
|
console.error(loadError)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
|
|
async function regenerate() {
|
|
if (key.value &&
|
|
!window.confirm('Regenerate the key? The current key stops working '
|
|
+ 'immediately and every agent must be reconfigured.')) {
|
|
return
|
|
}
|
|
busy.value = true
|
|
message.value = ''
|
|
error.value = ''
|
|
secret.value = ''
|
|
copied.value = false
|
|
try {
|
|
const response = await api.post('/endpoints/agentkey/regenerate')
|
|
const data = response.data.data || {}
|
|
applyStatus(data)
|
|
secret.value = data.secret || ''
|
|
message.value = 'Endpoint agent key generated.'
|
|
} catch (regenError) {
|
|
error.value = regenError.response?.data?.data?.error?.message
|
|
|| 'Could not generate the endpoint agent key'
|
|
} finally {
|
|
busy.value = false
|
|
}
|
|
}
|
|
|
|
async function revoke() {
|
|
if (!window.confirm('Revoke the key? All endpoint-agent reporting stops '
|
|
+ 'until a new key is generated.')) {
|
|
return
|
|
}
|
|
busy.value = true
|
|
message.value = ''
|
|
error.value = ''
|
|
secret.value = ''
|
|
try {
|
|
const response = await api.delete('/endpoints/agentkey')
|
|
applyStatus(response.data.data || {})
|
|
message.value = 'Endpoint agent key revoked.'
|
|
} catch (revokeError) {
|
|
error.value = revokeError.response?.data?.data?.error?.message
|
|
|| 'Could not revoke the endpoint agent key'
|
|
} finally {
|
|
busy.value = false
|
|
}
|
|
}
|
|
|
|
async function copySecret() {
|
|
try {
|
|
await navigator.clipboard.writeText(secret.value)
|
|
copied.value = true
|
|
} catch (copyError) {
|
|
console.error(copyError)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.mono { font-family: monospace; word-break: break-all; }
|
|
.form-card h3 { margin-top: 0; }
|
|
.form-card h4 { margin-top: 1rem; margin-bottom: 0.25rem; }
|
|
.form-card h4:first-child { margin-top: 0; }
|
|
.button-row { display: flex; gap: 0.5rem; }
|
|
.secret-card { border: 1px solid var(--color-warning, #b8860b); }
|
|
.secret-value { padding: 0.5rem; background: var(--color-surface-2, #1e1e1e); }
|
|
.settings-section { margin-bottom: 2rem; }
|
|
.settings-section > h3 { margin-bottom: 0.25rem; }
|
|
.info-list { margin: 0.25rem 0 0; padding-left: 1.25rem; }
|
|
.info-list li { margin-bottom: 0.5rem; }
|
|
</style>
|