<#849 Enrollment &amp; config parity: per-OS flags...
# osctrl
g
#849 Enrollment &amp; config parity: per-OS flags, cert upload, assembled config + SPA tabs/sliders/forms/docs links Pull request opened by alvarofraguas Summary This PR closes the parity gap between the legacy admin's environment-management surface (
/conf/{env_uuid}
,
/enroll/{env}
) and the new React SPA at
frontend/
. The work covered 10 items — 3 API extensions + 7 SPA additions — and along the way picked up a handful of UX-consistency and security improvements surfaced by an in-house audit pass. What's in here Backend (3 new endpoints): 1.
GET /api/v1/environments/{env}/enroll/flags{Linux,Mac,Windows,FreeBSD}
— per-OS flags with
__SECRET_FILE__
/
__CERT_FILE__
placeholders substituted for the platform's canonical install path. Same substitution legacy admin's download path performs via
generateFlags
. 2.
POST /api/v1/environments/{env}/enroll/cert
— base64-PEM upload with strict PEM + x509 validation server-side. Capped at 64 KiB. Audit-logged. 3.
GET /api/v1/environments/{env}/configuration/assembled
— returns the cached assembled osquery configuration JSON (composed by
RefreshConfiguration
, which already fires from every parts mutation). Pure read, no side effect. SPA (env configuration page reorg): • Tabbed editor on `/_app/env/{env}/config`:
Settings
(intervals sliders + expiration) followed by one tab per JSON section (
Options
,
Schedule
,
Packs
,
Decorators
,
ATC
,
Flags
). • IntervalsCard uses three range sliders with the exact bounds from legacy admin's
conf.html
(Configuration / Logging 10–600 step 10, Query 10–300 step 1) plus a numeric readout. • AddOptionForm + AddScheduledQueryForm — inline forms above the Monaco editor on Options + Schedule respectively. Parse the current draft JSON, append, re-serialize, push back to the editor. • DocsLink atom (new) — small
? docs
chip in every section header, opens the matching osquery read-the-docs anchor in a new tab. Surfaces in EnvConfig and Enroll tabs. SPA (enrollment page reorg): • Page split into three tabs:
Install
(default — scripts + collapsible Package URLs),
Configuration
(FlagsCard with per-OS download + AssembledConfigCard Monaco viewer),
Lifecycle
(Enroll/Remove secret cards + full CertificateCard with paste-or-upload replace). • CertificateCard surfaces the env cert preview (first PEM line + last 60 chars) with copy/download/replace. • FlagsCard with per-OS tabs consumes the four new flag endpoints. SPA (multi-select consistency across list pages): • Tags, Operators (Users), and Environments pages gained the same multi-select dock as Carves/Queries/Nodes. Per-row Edit/Delete preserved; bulk delete via Promise.allSettled with partial-success reporting. • Operators page's bulk-select correctly excludes the current operator's own row. SPA (other polish): • NodesTablePage filter chip pad restyled to match Queries' StatusTabs format; old duplicate inline status pad removed. • Profile page reorganized from a single tall Security mega-card to a 2×2 grid of focused cards (Account · Password · API token · Preferences). • Settings page restyled with the same EnvConfig section chrome. • Node detail page — Archive and Delete collapsed into a single Archive button. Archive always snapshots into
archive_osquery_nodes
before removing the live row, so every removal is forensically recoverable. The previous two-button shape mapped to the same backend op and read as misleading. • Query detail page — Data cell now renders one nested mini-table per result item (matching the legacy admin's render shape) instead of extruding all keys to the outer table. • Carve detail page — node UUID column linked to node detail. • SideNav gained a Configuration entry under Enrollment. Security pass (in-house audit before push): • H1:
EnvConfigurationHandler
dropped its inline
RefreshConfiguration
call — CSRF-via-GET hazard + DB-write hot-loop closed. Read serves the cached value; refresh still fires from every parts mutation. • H2: All 7 permission-deny sites in
environments_crud.go
now route through
denyEnv
, restoring audit-log visibility on cross-tenant probes against the new CRUD endpoints (the older
environments.go
handlers already did this). • M1:
EnvCertUploadHandler
wraps the body with
MaxBytesReader
at 64 KiB. • M2: AddOptionForm + AddScheduledQueryForm refuse
*proto*
/
constructor
/
prototype
as option/query names. • M3:
substitutePlatformPaths
uses
strings.ReplaceAll
for forward compatibility. Known scopeEnvironment delete is one-arg-keyed (
DELETE /api/v1/environments/{env}
) — the older
EnvActionsHandler
belt-and-braces pattern that requires both name + UUID in the body was deliberately not ported, to keep the new CRUD endpoints REST-idiomatic. Follow-up PR could add an optional
?confirm=
query param for the same protection. • Environment delete does not cascade — nodes, queries, carves, tags scoped to the deleted env are orphaned. Documented in the SPA's confirm copy. Same shape as legacy admin. Follow-up could add cascade or a referential-integrity check. • No restore UI for archived nodes
archive_osquery_nodes
holds the forensic snapshot but there's no "Restore" button yet. Operators can recover via SQL or by letting the agent re-enroll. A future
/archive
page is the natural follow-up. Out of scope (left for future PRs)
gosec
MEDIUM findings on pre-existing code paths (sha1 in node-key hash, missing HTTP timeouts, etc.) — none introduced by this PR. • L2 govulncheck stdlib advisories (
GO-2026-5039
,
GO-2026-5037
) — reachable only via
cmd/tls/handlers/post.go
which this PR doesn't touch; bump Go version separately. Test plan
go test ./...
clean •
go build ./cmd/...
clean •
npm run check
(tsc) clean •
npm test
— 22 files / 106 tests all green after one selector fix for the chip-pad restyle •
npm run build
succeeds (846 KB bundle, pre-existing >500 KB warning unchanged) •
golangci-lint run
— 10 issues, all pre-existing on unchanged files; CI has
only-new-issues: true
so no new flags introduced • Manual smoke against a live dev instance (Proxmox VM at 192.168.99.118): the three new API endpoints return correctly substituted data for all 4 OSes; cert upload rejects malformed PEM with 400 and accepts a valid x509 with 200; assembled config GET returns the cached blob; H2 fix verified by attempting a deny as a non-admin and confirming a
denied access
row landed in `audit_logs`; M1 fix verified by a 100 KB body returning 400. • Fast-forward mergeable onto
upstream/main
(0 commits behind). jmpsec/osctrl