Commit Graph

6495 Commits

Author SHA1 Message Date
rcourtman 35bbb5d552 Add vSphere placement card to workload drawer
When a vSphere VM opens in the workload drawer, the System card
shows CPUs and Node (the runtime host) but vCenter, Datacenter,
and Cluster context were nowhere to be found. Add a small vSphere
card next to System / Guest Info / Tags showing those three fields,
matching the card-shaped layout the drawer already uses.

Extend WorkloadGuest with a narrow vmware shape (connectionName,
vcenterHost, datacenterName, clusterName) and populate it in
the useWorkloads mapping when platformType is vmware-vsphere.
The card only renders when at least one field is present.

Tools / snapshots / virtual disks / network adapters are not on
this card; surfacing them would require mapping more fields onto
WorkloadGuest and a separate decision on what's drawer-worthy.
2026-05-23 09:12:45 +01:00
rcourtman 62a0027ff8 Prune vSphere drawer sections to operator signal
The VIRTUAL HARDWARE section was rendering 20+ rows including
capability toggles (CPU/memory hot-add) and boot configuration
(EFI legacy boot, boot delay, boot retry, boot order, default
setup mode) that operators don't act on from a monitoring
surface. The GUEST section dumped Host/Instance/BIOS UUIDs that
are API-shaped identifiers, not operator scan candidates. The
TOOLS section padded out with install metadata (install type,
upgrade policy, auto-update capability, attempt count, reboot
components / time) that's the same kind of noise.

Trim each section to the rows an operator actually reads:
- Hardware: Guest OS, Hardware version, CPU topology, Memory size,
  Upgrade attention, Upgrade error, Instant clone frozen (when
  true), Enter setup mode (when true). Capability toggles and
  default boot config stay in the raw payload.
- Guest: Guest OS, Guest hostname, Guest IPs. UUIDs and
  datastoreUrl drop.
- State: Connection state replaces the redundant Power row (power
  is already on the workload row and SYSTEM card).
- Tools: Run state, Version status (only when not current),
  Version, Guest reboot (only when requested), Last install error.
  Install metadata stays in the raw payload.

Drop the now-unused formatMilliseconds and bootOrderLabel helpers
and their ResourceVMwareBootDevice import.

Test assertions are updated to match the trimmed shape with
comments explaining the operator-signal principle.
2026-05-23 09:04:34 +01:00
rcourtman 787dfe56fe Delete VsphereVirtualMachinesTable
Now that VmwareOverview routes VMs through WorkloadsSurface, the
bespoke VsphereVirtualMachinesTable + its test are dead code.
Remove them and flip the platform-overview guardrail to assert the
new shape (no VsphereVirtualMachinesTable, presence of
WorkloadsSurface with forcedPlatform/forcedViewMode for vSphere).

The shared formatVmwarePowerState / getVmwarePowerStateVariant
helpers in vmwarePageModel stay; VsphereHostsTable still uses them
for the ESXi host power column.
2026-05-23 08:50:51 +01:00
rcourtman d96d5612fd Route vSphere VMs through WorkloadsSurface
The vSphere overview's VM table was a bespoke component that
parallel-implemented what Proxmox, Docker, and Kubernetes already
get from the shared WorkloadsSurface. The result was an
overview page that lacked the Grouped/List, Bars/Trends/Columns,
and metric history toggles operators expect on a Pulse platform
page.

Wire VmwareOverview to useWorkloadsState + WorkloadsFilter +
WorkloadsSurface using the same embedded shape Proxmox uses:
forcedPlatform='vmware-vsphere', forcedViewMode='vm', shared
metricDisplayMode and metricHistoryRange persistent signals,
storage-scoped status and column visibility, compactGroupHeaders
so VsphereHostsTable keeps its dedicated layout above. The
existing power-state vocabulary maps cleanly: normalizeWorkloadStatus
already collapses online->running and offline->stopped, and vSphere
resource IDs (vm-XXXX hashes) match the workload chart key
candidates so sparkline mode resolves without extra plumbing.

VsphereVirtualMachinesTable stays in the tree for now; deletion
lands once Phase 1 is observed stable.
2026-05-23 08:43:01 +01:00
rcourtman ab5ed3ea6f Revert "Auto-expand vSphere drawer section on platform pages"
This reverts commit 9008e5f45e.
2026-05-22 21:41:44 +01:00
rcourtman 9008e5f45e Auto-expand vSphere drawer section on platform pages
Mirror the TrueNAS auto-open pattern: when a resource has VMware
detail sections, expand them by default in the row detail drawer.
Adds hasVMwareDetailSections to resourceDetailDrawerVmwareModel,
plumbs initialShowVMwareDetails through useResourceDetailDrawerState
and ResourceDetailDrawer, and lets PlatformResourceDetailTableRow
auto-detect from the resource the same way it already does for
TrueNAS. Matters now that the Tools and Health columns are gone
from the VMs table; Tools state lives in this drawer section.
2026-05-22 21:30:38 +01:00
rcourtman 0a7bd5d49e Align vSphere tables with shared platform conventions
Strip the bespoke colored pill components (HealthPill, ToolsPill,
StatusPill) from the vSphere VMs, Datastores, and Networks tables.
Status now reads from the leading StatusDot in the name cell, the
same way Proxmox, Docker, Kubernetes, and TrueNAS already convey it.
The Tools and Health columns on VMs and the State column on
Datastores and Networks are removed; the underlying data stays
reachable through the row detail drawer.

Drop the cluster-services inline subtitle on Hosts and VMs and the
managedObjectId / datastoreUrl subtitles on the VMs, Datastores,
and Networks name cells, so row density matches Proxmox and Docker.
Move that information into the row title attribute.

Replace the hand-rolled emerald progress bar in CapacityCell with the
shared StackedDiskBar so datastore capacity colouring responds to
fill thresholds rather than always rendering green.

Promote powerStateVariant and formatPowerState into vmwarePageModel
as getVmwarePowerStateVariant and formatVmwarePowerState; both Hosts
and VMs tables now share them. Export platformChipStatusDot from
sharedPlatformPage so vSphere filter chip definitions stop
redeclaring the same helper, and add leading status dots to the
Datastores, Networks, and Alerts chips that were missing them.
2026-05-22 21:13:38 +01:00
rcourtman 5016cbc2ba Add vSphere network inventory
Project vCenter network inventory through canonical resources and add the vSphere Networks table backed by vCenter network topology. Align resource presentation coalescing so state and resource APIs share the same host contract.
2026-05-22 20:26:56 +01:00
rcourtman 6c7d64ae43 Carry vSphere cluster services
Project vCenter cluster HA and DRS service state through the VMware resource facet so existing hosts and VMs expose cluster posture as read-only topology context.
2026-05-22 19:18:46 +01:00
rcourtman 9f98f8fcb9 Carry vSphere VM hardware config
Project vCenter VM hardware, CPU, memory, and boot configuration through the VMware resource facet and shared vSphere details so operators can inspect virtual hardware posture as read-only monitoring context.
2026-05-22 18:54:36 +01:00
rcourtman add3984f25 Carry vSphere VMware Tools status
Project vCenter VMware Tools runtime facts through the VMware resource facet and vSphere VM surface so operators can see Tools run state, version posture, upgrade policy, install attempts, and guest reboot requests as read-only monitoring context.
2026-05-22 18:22:49 +01:00
rcourtman 10c3c66f92 Carry vSphere VM virtual disks
Project vCenter VM hardware disk facts through the VMware resource facet and vSphere VM surface so operators can see virtual disk backing, capacity, datastore, and bus placement as read-only monitoring context.
2026-05-22 16:45:08 +01:00
rcourtman 6a7936ba4e Carry vSphere VM network adapters
Project vCenter VM hardware Ethernet adapter facts through the VMware resource facet and vSphere VM surface so operators can see vNIC backing network, MAC address, connection state, and adapter flags as read-only monitoring context.
2026-05-22 15:40:25 +01:00
rcourtman 7ffe2e581c Carry vSphere snapshot trees
Project VI JSON VM snapshot trees through the VMware resource facet and shared drawer so vSphere VM detail shows current snapshot, tree entries, and quiesce state as read-only workload context.
2026-05-22 12:43:28 +01:00
rcourtman db690b0dce Add vSphere health tab
Surface vSphere alarm and health signals on a dedicated route-backed Health tab so the admitted VMware page exposes the shared incident proof path without duplicating health rows on Overview.
2026-05-22 09:24:18 +01:00
rcourtman abf2203307 Correct VMware platform readiness classification
Move vSphere back to the admitted first-lab-ready stage in the support manifest, regenerate shared projections, and keep onboarding/navigation copy from presenting VMware as fully supported before live vCenter proof.
2026-05-22 09:13:29 +01:00
rcourtman 3cd1517883 Surface vSphere activity timeline
Add a global resource timeline endpoint for provider activity and wire vSphere Activity to VMware timeline changes. Seed mock VMware activity through the same supplemental-change path and keep the relevant resource contract tests current.
2026-05-22 08:54:43 +01:00
rcourtman 06a73244a2 Surface vSphere activity 2026-05-22 08:22:36 +01:00
rcourtman ffd2a7efed Surface vSphere health signals 2026-05-22 08:08:10 +01:00
rcourtman 2ee40382d7 Make vSphere VM table API-native 2026-05-22 07:55:15 +01:00
rcourtman b19beb1976 Make vSphere datastores API-native 2026-05-22 07:38:57 +01:00
rcourtman 444aa1175e Render TrueNAS storage row depth 2026-05-22 07:09:18 +01:00
rcourtman a57af72fbb Nest TrueNAS dataset topology rows 2026-05-22 06:54:27 +01:00
rcourtman 51f79efe72 Infer TrueNAS storage topology ownership 2026-05-22 06:42:26 +01:00
rcourtman 141269e31e Harden TrueNAS inventory ownership 2026-05-22 06:30:31 +01:00
rcourtman b63953ee0e Share TrueNAS detail table rendering 2026-05-22 06:25:22 +01:00
rcourtman d10079f9e3 Extract shared TrueNAS detail table 2026-05-22 05:55:31 +01:00
rcourtman 4a371c6b1f Add TrueNAS alert detail rows 2026-05-22 05:42:38 +01:00
rcourtman f9bd009a1c Add TrueNAS service detail rows 2026-05-21 21:47:10 +01:00
rcourtman 5bc952043e Promote TrueNAS native inline details 2026-05-21 21:40:50 +01:00
rcourtman d3d75911fd Add TrueNAS protection detail rows 2026-05-21 21:05:42 +01:00
rcourtman 85364cd01c Add native TrueNAS system details 2026-05-21 20:19:41 +01:00
rcourtman 417b459c3d Add native TrueNAS storage details 2026-05-21 18:16:27 +01:00
rcourtman 29439fe14b Render resource context as detail tables 2026-05-21 17:53:40 +01:00
rcourtman 5bffb581bb Extract resource detail summary presentations 2026-05-21 17:45:48 +01:00
rcourtman 7ac97afc32 Use table summaries for inline resource details 2026-05-21 17:11:39 +01:00
rcourtman 433da8537f Render TrueNAS detail sections as tables 2026-05-21 17:05:43 +01:00
rcourtman 1794a83002 Open TrueNAS inline details by default 2026-05-21 16:57:10 +01:00
rcourtman 2e5e54d598 Fit TrueNAS detail tables 2026-05-21 16:50:40 +01:00
rcourtman 64a3fbc07e Fit TrueNAS protection table to overview width
- Remove the redundant Signal column from the TrueNAS protection ledger
- Keep replication state visible in target detail and outcome badge
- Add a guardrail for the fitted protection table width
2026-05-21 16:38:53 +01:00
rcourtman 946ebfde1d Tighten TrueNAS native inventory tables
- Scope the TrueNAS resource fetch to native TrueNAS and agent-origin system rows
- Group overview inventory counts to keep the table scannable without horizontal overflow
- Remove internal service/resource ids from visible TrueNAS service rows
2026-05-21 16:34:38 +01:00
rcourtman 1118e09fcd Clear server-side CSRF token on logout
clearSession invalidated the session-store record and zeroed both
session/CSRF cookies on the client side, but never deleted the
matching entry from the CSRF store. The orphaned record stuck around
for up to 4 hours (CSRF TTL) after every logout. Not exploitable —
the session itself was gone, so the stale CSRF entry could not be
used — but the asymmetry was real: InvalidateUserSessions (password
change) and InvalidateOldSessionFromRequest (re-login) both already
called DeleteCSRFToken, and logout was the missing path.

Add the same call in clearSession alongside InvalidateSession.

Regression test TestClearSession_DeletesServerSideCSRFToken sets up a
real session + CSRF token pair, calls clearSession with a request
carrying the session cookie, and asserts the CSRF token no longer
validates against the session ID afterward.
2026-05-21 16:26:00 +01:00
rcourtman 64505ac25c Tighten DOMPurify config in LLM markdown renderer
Two hardening adjustments to renderMarkdown in aiChatUtils.ts:

  - Pin ALLOWED_URI_REGEXP to ^(?:https?:|mailto:). The previous
    config left href scheme filtering to DOMPurify's library defaults,
    which strips javascript:/data: in current versions but is version-
    dependent. Making the allowlist explicit means the safety property
    survives a future DOMPurify upgrade that loosens the internal
    default.

  - Drop 'class' from ALLOWED_ATTR. The LLM has no legitimate reason
    to apply arbitrary CSS classes, and allowing them on sanitized
    markup is a UI-redress surface: an attacker who can influence the
    chat output (prompt injection, compromised model endpoint) could
    inject something like <div class="fixed inset-0 z-50 bg-black">
    to overlay the page or hide content.

Also expanded the marked.setOptions comment to document that the
sanitizer is the real XSS gate but marked options are kept tight as
defence-in-depth: a future DOMPurify-config loosening shouldn't mean
script tags from the LLM hit the DOM.

Regression tests added in aiChatUtils.test.ts:
  - javascript: hrefs stripped
  - data: hrefs stripped
  - class attribute stripped from <div>
  - https links preserved with target=_blank rel=noopener noreferrer
2026-05-21 16:23:12 +01:00
rcourtman 77a6a8c695 Read RBAC identity from request context, not response header
RequirePermission was reading the authenticated username from
w.Header().Get("X-Authenticated-User"). The header is set by checkAuth
upstream, but response headers are mutable across the handler chain:
any middleware sitting between checkAuth and RequirePermission that
wrote that header would substitute an arbitrary identity, and the
RBAC authorizer would make its access decision against the
substituted value. Identity belongs on the request context, not in a
mutable response surface.

Read internalauth.GetUser(r.Context()) first — checkAuth already
calls attachUserContext on every auth path that produces a real user,
so the context is the authoritative source. Keep a defensive,
warn-logged fallback to the response header for the one upstream path
that sets the header but skips attachUserContext (the anonymous-user
flow), so the existing test contract is preserved while the warning
flags the source path to plug.

Regression test TestRequirePermissionUsesContextUsername constructs
the attack: a request whose context says "real-authenticated-user"
and whose response header says "evil-spoofed-user". The RBAC
authorizer must observe the context value.
2026-05-21 16:19:25 +01:00
rcourtman 0ac04c8d39 Validate SAML LogoutResponse before clearing session
handleSAMLSLO was an unauthenticated force-logout DoS: any cross-origin
GET or POST to /api/saml/{id}/slo cleared the user's session and
returned a 302 to /?logout=success. No SAML payload was required, no
signature validation, no IdP-identity check.

Add ValidateLogoutResponse to SAMLService wrapping crewjam/saml's
ServiceProvider.ValidateLogoutResponseRequest, which validates the
XML-DSig against the configured IdP certificate and the standard
LogoutResponse temporal and target invariants.

In handleSAMLSLO:
  - extract and validate the provider ID
  - require either a SAMLResponse (response to our SLO request) or a
    SAMLRequest (IdP-initiated SLO) parameter — reject bare requests
    with 400 and do not clear session
  - reject IdP-initiated LogoutRequest with 400: we don't track
    in-flight request IDs to bind one to yet, and silently treating it
    as a force-logout signal is the same bug we are closing
  - on SAMLResponse, call service.ValidateLogoutResponse. On failure
    return 403 and do not clear session; audit-log the failure
  - only when validation passes do we clear the session and redirect

Regression tests:
  - TestHandleSAMLSLO_RejectsEmptyPayload (400, no redirect, no session
    state mutation)
  - TestHandleSAMLSLO_RejectsInvalidLogoutResponse (403 on garbage
    SAMLResponse value)
  - TestHandleSAMLSLO_RejectsIDPInitiatedLogoutRequest (400 on
    SAMLRequest-only payload)

The prior TestHandleSAMLSLO_Redirects test, which asserted the unsafe
"clear session and 302 on any GET" behaviour, is replaced by the
three above.
2026-05-21 16:15:53 +01:00
rcourtman 1dfc4ee6d3 Require .sshsig sidecar for install.sh --archive path
The --archive PATH flag (and the Proxmox LXC bootstrap which
propagates --archive into the container) reached install_pulse_archive
without going through download_release_archive, which is the only
place that previously verified the cryptographic signature on a
Pulse release tarball. That made --archive a clean signature-
verification bypass: anyone who could swap a local tarball
between download and install (shared /tmp on a Proxmox host, a
maintainer testing a swapped local file, etc.) would get
arbitrary root code execution at extract time.

install.sh:
- install_pulse_archive now requires ${archive_path}.sshsig and
  verifies it via the existing verify_release_signature helper
  before tar runs. Missing or invalid signature is a hard
  fail-closed.
- download_release_archive now keeps the .sshsig alongside the
  archive at ${archive_path}.sshsig instead of stashing it in a
  mktemp file that got deleted post-verify. install_pulse_archive
  re-verifies it; no behavior change for the curl|bash path beyond
  the extra integrity check at extract time.
- Proxmox LXC bootstrap now pct push'es ${container_archive_source}.sshsig
  alongside the archive into the container so the in-container
  install_pulse_archive run can verify. Missing sidecar fails the
  bootstrap (cleanup_on_error tears down the half-provisioned
  container).
- Cleanup paths that remove the temp archive on success/failure
  now also remove the sidecar.
- --help advertises the .sshsig requirement.

install-sh-smoke.yml:
- Smoke test now pulls ${tarball}.sshsig alongside the tarball
  before running install.sh --archive inside the test container.

Trade-off: maintainer flows that did `install.sh --archive
/path/to/local.tar.gz` against an unsigned local tarball will now
fail with a clear "Required signature sidecar not found" error.
Maintainers can either pull the corresponding .sshsig from the
release alongside the tarball, or skip --archive in favour of the
canonical download path.
2026-05-21 16:13:57 +01:00
rcourtman d890904ce6 Stop auto-escalating SameSite to None on proxied requests
getCookieSettings auto-set SameSite=None on the session and CSRF
cookies whenever a request arrived via a trusted proxy with
X-Forwarded-Proto: https. The original intent was "be more permissive
for proxied deployments," but in practice that disabled the
browser-side CSRF defence for every Pulse deployment behind a reverse
proxy — which is nearly all of them.

SameSite=None tells the browser to attach the cookie on arbitrary
cross-site requests. That is only required for cross-origin iframe
embedding scenarios Pulse does not document or support. The
top-level-navigation cases that actually matter (OIDC/SAML callback
landing, Cloudflare-tunnel access from a bookmark) all work under Lax,
which still attaches cookies on top-level navigations and only blocks
cross-site sub-resource requests.

Always return SameSiteLaxMode. The Secure flag still tracks the
actual connection state via isConnectionSecure(r). Together with the
earlier CSRF bypass fix (require token regardless of Authorization
header), the browser-side and server-side defences are now both in
place.

Test expectations updated: three cases that previously asserted
SameSiteNoneMode for the proxied-HTTPS / Cloudflare-tunnel / direct-
TLS-with-Forwarded paths now assert SameSiteLaxMode, with comments
flagging them as regression coverage.
2026-05-21 16:11:30 +01:00
rcourtman 2305131d8a Move eval-model-matrix off self-hosted runner
Self-hosted runners attached to a public repo are a known
severe-risk pattern: any workflow that gains the ability to run
PR-controlled code escalates to RCE on the runner host. Even
though this workflow is workflow_dispatch only today, the runner
itself sits one workflow edit away from arbitrary code execution
on whatever machine it's pinned to.

Changes:

- runs-on: self-hosted -> ubuntu-24.04. Reachability of base_url
  from the GitHub-hosted runner is now the dispatcher's
  responsibility. Add tailscale/* steps (like deploy-v6-preview-
  demo.yml or deploy-demo-server.yml already do) if pointing at
  a private Pulse instance.

- Drop the admin/admin fallback on PULSE_EVAL_USER and
  PULSE_EVAL_PASS. The previous default was
  ${{ secrets.PULSE_EVAL_USER || 'admin' }} which silently ran
  the eval as admin/admin if the secrets were missing. Added a
  fail-closed precheck that refuses to proceed unless both
  secrets are set.

- Route inputs.scenario, inputs.models, inputs.providers, and
  inputs.base_url through env: indirection instead of inline
  ${{ }} interpolation into shell. Defense-in-depth against
  script injection from the dispatch payload.

Net result: kills the audit's only CRITICAL finding and the
companion MEDIUM (admin/admin fallback). Eval can still run
exactly as before once secrets are set and base_url is reachable.
2026-05-21 16:09:09 +01:00
rcourtman 226f0bdf72 Report restricted hot-dev probes as unavailable
Render managed runtime status probes as unavailable instead of 000 when a restricted shell can see loopback listeners but cannot run local HTTP probes. Refs architecture post-RC canonicalization follow-up.
2026-05-21 15:51:40 +01:00
rcourtman a77420e88a Make hot-dev local-only by default
Keep the managed dev runtime on loopback unless PULSE_DEV_LAN=true is set, so installed LAN agents cannot accidentally attach to a developer session. Harden managed status checks for restricted local shells. Refs architecture post-RC canonicalization follow-up.
2026-05-21 15:26:53 +01:00