GitHub
06/09/2026, 10:53 PM/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 scope
• Environment 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/osctrlGitHub
06/10/2026, 5:56 AM