docs: publish security review evidence

This commit is contained in:
rcourtman 2026-08-16 10:22:20 +01:00
parent effcf2d50c
commit 787a191a5e
12 changed files with 458 additions and 26 deletions

View file

@ -175,3 +175,11 @@ and measure their own workload.
Pulse is intended to be the operational monitoring layer for the platforms it
supports. It is not a SIEM, a general log-ingestion platform, or a substitute
for an organization's incident escalation and change-control process.
## Independent review
Pulse does not currently claim an independent security certification. The
[Independent Security Review Scope](SECURITY_REVIEW.md) maps the main trust
boundaries to source files, tests, reproducible baseline commands, and the
private disclosure channel so external reviewers can evaluate concrete
behavior rather than marketing claims.

View file

@ -64,6 +64,7 @@ Pulse Pro capabilities.
## Security, privacy, and access
- [Production deployment and security](PRODUCTION_SECURITY.md)
- [Independent security review scope](SECURITY_REVIEW.md)
- [Security guide](../SECURITY.md)
- [Privacy and telemetry disclosure](PRIVACY.md)
- [OIDC and SSO](OIDC.md)

160
docs/SECURITY_REVIEW.md Normal file
View file

@ -0,0 +1,160 @@
# Independent Security Review Scope
Pulse does not currently claim an independent security certification or a
third-party penetration-test attestation. This page gives reviewers a concrete,
reproducible starting point and makes the intended trust boundaries explicit.
Before reviewing the code, read:
- [Production Deployment and Security](PRODUCTION_SECURITY.md) for the
least-privilege deployment model and operational checklist;
- the canonical [Security Policy](../SECURITY.md) for reporting and hardening;
- [Agent Security](AGENT_SECURITY.md) for root privilege, command execution,
guest access, and update verification;
- [Installation](INSTALL.md) for the signed, version-pinned server installer.
## Suggested review boundaries
### 1. Authentication, authorization, and tenant isolation
Review browser sessions, API-token scopes, organization binding, proxy
authentication, and denial behavior. Confirm that a credential valid for one
route or organization cannot acquire management, agent, or cross-tenant
authority elsewhere.
Starting points:
- `internal/api/auth.go`
- `internal/api/authorization.go`
- `internal/api/middleware.go`
- `internal/api/api_token_scope_transport_integration_test.go`
- `internal/api/middleware_tenant_authorization_test.go`
- `pkg/auth/`
### 2. Credential storage and configuration transfer
Review encryption-key creation and permissions, AES-GCM use, encrypted
configuration persistence, backup/restore behavior, and export/import
authorization. Confirm that missing or unsafe key material fails closed rather
than silently replacing a key and orphaning encrypted data.
Starting points:
- `internal/crypto/crypto.go`
- `internal/crypto/crypto_test.go`
- `internal/config/persistence.go`
- `internal/config/persistence_fail_test.go`
- `internal/api/config_transfer_authorization.go`
- `internal/api/config_transfer_authorization_test.go`
### 3. Agent privilege and command authority
Review the Linux agent's default root profile as a privileged monitoring
boundary. Confirm that command execution remains disabled by default, enabling
it does not bypass policy and approval, report-only credentials cannot acquire
execution authority, and agent health endpoints remain loopback-bound unless
explicitly changed.
Starting points:
- `cmd/pulse-agent/main.go`
- `internal/agentexec/`
- `internal/api/agent_command_authorization.go`
- `internal/api/agent_command_authorization_test.go`
- `scripts/install.sh`
- `docs/AGENT_SECURITY.md`
### 4. Installer and update supply chain
Review the server installer's release pinning and Ed25519 verification, agent
checksum and signature enforcement, executable validation, self-test, and
atomic replacement behavior. Include the documented first automatic v5-to-v6
agent update limitation in the threat model.
Starting points:
- `install.sh`
- `docs/INSTALL.md`
- `internal/agentupdate/update.go`
- `internal/agentupdate/update_http_test.go`
- `.github/workflows/create-release.yml`
- `docs/CODE_SIGNING_POLICY.md`
### 5. Discovery and outbound network boundaries
Confirm that network discovery is disabled by default, probes only the
documented Proxmox ports, honors subnet allowlists and blocklists, and applies
host, concurrency, and timeout bounds. Review outbound URL validation and SSRF
protections for integrations and probes.
Starting points:
- `internal/config/config.go`
- `internal/discovery/`
- `pkg/discovery/`
- `internal/securityutil/outbound_http.go`
- `pkg/securityutil/outbound_http.go`
### 6. Container and host separation
Review the block on storing host SSH private keys inside Pulse containers,
Docker/Podman socket handling, Proxmox guest inventory opt-ins, and data-volume
permissions. Confirm that API-only Proxmox monitoring does not require Pulse
software on the hypervisor.
Starting points:
- `SECURITY.md`
- `docs/AGENT_SECURITY.md`
- `internal/hostagent/proxmox_lxc_filesystems.go`
- `internal/monitoring/docker_detection.go`
- `internal/api/contract_test.go`
- `internal/dockeragent/`
### 7. Production-scale failure behavior
Review behavior under concurrent reads and writes, stale data, partial platform
responses, retention, and large resource sets. Pulse includes simulated
500-node API and metrics-store regression coverage; treat that as reproducible
engineering evidence, not as a production certification.
Starting points:
- `internal/api/load_test.go`
- `pkg/metrics/store_loadtest_test.go`
- `pkg/metrics/store_slo_test.go`
## Reproducible baseline
From a clean checkout with the documented Go and Node versions installed:
```bash
go test ./internal/crypto ./pkg/auth ./internal/agentupdate \
./internal/agentexec ./internal/discovery ./pkg/discovery \
./internal/securityutil ./pkg/securityutil
go test ./internal/api \
-run '^TestLoad_500Node_(ConcurrentResources|ConcurrentMetricsHistory|MixedEndpoints)$' \
-count=1
go test ./pkg/metrics \
-run '^TestLoadTest500Nodes_WriteAndQueryIntegration$' \
-count=1
python3 scripts/check_public_docs.py
```
These commands are a baseline, not a substitute for manual review, dependency
analysis, fuzzing, deployment-specific testing, or an independent threat model.
## Reporting findings
Please report suspected vulnerabilities privately to
<security@pulseapp.io>. Include the affected version or commit, deployment
model, required privileges, reproduction steps, expected impact, and any
suggested containment. Do not include live credentials or customer data.
General defects and hardening suggestions that do not disclose an exploitable
weakness can use the public [issue tracker](https://github.com/rcourtman/Pulse/issues).
The canonical disclosure contact and current security guidance remain in the
[Security Policy](../SECURITY.md).

View file

@ -6165,6 +6165,25 @@ it is read from the purchase-start query, validated, and passed to the
license-server portal handoff. No agent enrollment, report, ack, or update
route reads or emits it, and the agent-facing payload shapes are unchanged.
### Generic host onboarding preserves API-first Proxmox setup
`frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`
now makes the least-privilege Proxmox choice visible before an operator copies
a host-agent command. The generic host installer remains canonical for
host-local, Docker, and Kubernetes telemetry, but it routes PVE and PBS users
to the existing platform-connection flows first and states that normal
Proxmox inventory and metrics do not require a root agent. The root agent stays
an explicit augmentation for SMART, temperatures, and host-local storage
detail. `ConnectionEditor` also reconciles its selected credential slot when a
direct onboarding route changes in place, so following the PVE or PBS link from
the already-mounted agent dialog replaces the agent installer with the matching
API credential form instead of changing only the dialog title. This adds no
enrollment, token, command, report, discovery, or update authority and does not
replace the existing PVE or PBS setup handlers. Source guards in
`InfrastructureOperationsModel.test.tsx` and `ConnectionEditor.test.tsx` pin
the API-first copy, both canonical onboarding routes, and the reactive
credential-slot handoff.
### Platform connection panel state waits on the infrastructure capability
`Settings.tsx` constructs `useInfrastructureSettingsState` for every settings

View file

@ -1,18 +1,19 @@
{
"version": 1,
"base_sha": "d3a345bd66d635b59427cdfb06bd030a053daded",
"verified_at": "2026-08-15T09:17:52Z",
"base_sha": "effcf2d50c076310c36aba0876a69b3178587e10",
"verified_at": "2026-08-16T09:16:52Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/api/patrolAttention.ts",
"frontend-modern/src/features/patrol/PatrolRecentWorkPanel.tsx"
"frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx",
"frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx"
],
"content_sha256": {
"frontend-modern/src/api/patrolAttention.ts": "035e201a4c9a67c230f67004113b632c8e9ee82edf36bea0acaf86d5b9725b0e",
"frontend-modern/src/features/patrol/PatrolRecentWorkPanel.tsx": "b62d2cd4ccd5c2da4442a54d04863047f667fa5f076975046d3c2a0dc15e560e"
"frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx": "1c6289caaad0230b2a1e3301235c701c760182f203dc0af7170972b8a11768e8",
"frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx": "8007a0da1ef3140cc32174dea3bf52857aa351441d0011f862c3fd27b83649a2"
},
"routes": [
"/patrol"
"/settings/infrastructure?add=agent",
"/settings/infrastructure?add=pve"
],
"viewports": [
{
@ -25,19 +26,16 @@
}
],
"states": [
"Recently handled populated with six newest server-filtered verified Patrol receipts in calm operator language",
"Raw executor lifecycle telemetry and full opaque resource ids are absent from the compact receipt list",
"Executing-agent and independent-observer trust labels render from the canonical evidence class",
"Refresh failure keeps the last truthful receipts visible with an explicit unavailable message",
"Refresh recovery clears the error and reloads the same six verified receipts",
"Activity history handoff remains adjacent to verified receipts",
"Phone-width receipt list stays within the 390-pixel document width"
"Agent installer explains that Proxmox inventory and metrics start with a narrowly scoped API token and do not require a root agent",
"API-first guidance links to PVE, PBS, and the production security model before the install-token step",
"PVE route displays the API credential form with API Token selected and the least-privilege API inventory explanation",
"Desktop dialogs remain within the 1280-pixel document width",
"Phone-width agent and PVE dialogs remain within the 390-pixel document width"
],
"interactions": [
"refreshed the populated Recently handled list and confirmed compact action, trust, and resource presentation",
"stopped the disposable server and refreshed to exercise the error state",
"confirmed six existing receipts remained visible during the refresh error",
"restored the healthy server and refreshed to clear the error",
"resized to 390 by 844 and confirmed document width stayed at 390"
"opened the generic Pulse Agent onboarding route and inspected the API-first guidance",
"followed Connect Proxmox VE through the API and confirmed the mounted dialog retargeted from the agent installer to the PVE credential form",
"repeated the agent-to-PVE handoff at 390 by 844 and confirmed the API inventory and root-boundary copy remained visible",
"confirmed document and dialog widths at desktop and phone viewports without horizontal overflow"
]
}

View file

@ -175,3 +175,11 @@ and measure their own workload.
Pulse is intended to be the operational monitoring layer for the platforms it
supports. It is not a SIEM, a general log-ingestion platform, or a substitute
for an organization's incident escalation and change-control process.
## Independent review
Pulse does not currently claim an independent security certification. The
[Independent Security Review Scope](SECURITY_REVIEW.md) maps the main trust
boundaries to source files, tests, reproducible baseline commands, and the
private disclosure channel so external reviewers can evaluate concrete
behavior rather than marketing claims.

View file

@ -64,6 +64,7 @@ Pulse Pro capabilities.
## Security, privacy, and access
- [Production deployment and security](PRODUCTION_SECURITY.md)
- [Independent security review scope](SECURITY_REVIEW.md)
- [Security guide](../SECURITY.md)
- [Privacy and telemetry disclosure](PRIVACY.md)
- [OIDC and SSO](OIDC.md)

View file

@ -0,0 +1,160 @@
# Independent Security Review Scope
Pulse does not currently claim an independent security certification or a
third-party penetration-test attestation. This page gives reviewers a concrete,
reproducible starting point and makes the intended trust boundaries explicit.
Before reviewing the code, read:
- [Production Deployment and Security](PRODUCTION_SECURITY.md) for the
least-privilege deployment model and operational checklist;
- the canonical [Security Policy](../SECURITY.md) for reporting and hardening;
- [Agent Security](AGENT_SECURITY.md) for root privilege, command execution,
guest access, and update verification;
- [Installation](INSTALL.md) for the signed, version-pinned server installer.
## Suggested review boundaries
### 1. Authentication, authorization, and tenant isolation
Review browser sessions, API-token scopes, organization binding, proxy
authentication, and denial behavior. Confirm that a credential valid for one
route or organization cannot acquire management, agent, or cross-tenant
authority elsewhere.
Starting points:
- `internal/api/auth.go`
- `internal/api/authorization.go`
- `internal/api/middleware.go`
- `internal/api/api_token_scope_transport_integration_test.go`
- `internal/api/middleware_tenant_authorization_test.go`
- `pkg/auth/`
### 2. Credential storage and configuration transfer
Review encryption-key creation and permissions, AES-GCM use, encrypted
configuration persistence, backup/restore behavior, and export/import
authorization. Confirm that missing or unsafe key material fails closed rather
than silently replacing a key and orphaning encrypted data.
Starting points:
- `internal/crypto/crypto.go`
- `internal/crypto/crypto_test.go`
- `internal/config/persistence.go`
- `internal/config/persistence_fail_test.go`
- `internal/api/config_transfer_authorization.go`
- `internal/api/config_transfer_authorization_test.go`
### 3. Agent privilege and command authority
Review the Linux agent's default root profile as a privileged monitoring
boundary. Confirm that command execution remains disabled by default, enabling
it does not bypass policy and approval, report-only credentials cannot acquire
execution authority, and agent health endpoints remain loopback-bound unless
explicitly changed.
Starting points:
- `cmd/pulse-agent/main.go`
- `internal/agentexec/`
- `internal/api/agent_command_authorization.go`
- `internal/api/agent_command_authorization_test.go`
- `scripts/install.sh`
- `docs/AGENT_SECURITY.md`
### 4. Installer and update supply chain
Review the server installer's release pinning and Ed25519 verification, agent
checksum and signature enforcement, executable validation, self-test, and
atomic replacement behavior. Include the documented first automatic v5-to-v6
agent update limitation in the threat model.
Starting points:
- `install.sh`
- `docs/INSTALL.md`
- `internal/agentupdate/update.go`
- `internal/agentupdate/update_http_test.go`
- `.github/workflows/create-release.yml`
- `docs/CODE_SIGNING_POLICY.md`
### 5. Discovery and outbound network boundaries
Confirm that network discovery is disabled by default, probes only the
documented Proxmox ports, honors subnet allowlists and blocklists, and applies
host, concurrency, and timeout bounds. Review outbound URL validation and SSRF
protections for integrations and probes.
Starting points:
- `internal/config/config.go`
- `internal/discovery/`
- `pkg/discovery/`
- `internal/securityutil/outbound_http.go`
- `pkg/securityutil/outbound_http.go`
### 6. Container and host separation
Review the block on storing host SSH private keys inside Pulse containers,
Docker/Podman socket handling, Proxmox guest inventory opt-ins, and data-volume
permissions. Confirm that API-only Proxmox monitoring does not require Pulse
software on the hypervisor.
Starting points:
- `SECURITY.md`
- `docs/AGENT_SECURITY.md`
- `internal/hostagent/proxmox_lxc_filesystems.go`
- `internal/monitoring/docker_detection.go`
- `internal/api/contract_test.go`
- `internal/dockeragent/`
### 7. Production-scale failure behavior
Review behavior under concurrent reads and writes, stale data, partial platform
responses, retention, and large resource sets. Pulse includes simulated
500-node API and metrics-store regression coverage; treat that as reproducible
engineering evidence, not as a production certification.
Starting points:
- `internal/api/load_test.go`
- `pkg/metrics/store_loadtest_test.go`
- `pkg/metrics/store_slo_test.go`
## Reproducible baseline
From a clean checkout with the documented Go and Node versions installed:
```bash
go test ./internal/crypto ./pkg/auth ./internal/agentupdate \
./internal/agentexec ./internal/discovery ./pkg/discovery \
./internal/securityutil ./pkg/securityutil
go test ./internal/api \
-run '^TestLoad_500Node_(ConcurrentResources|ConcurrentMetricsHistory|MixedEndpoints)$' \
-count=1
go test ./pkg/metrics \
-run '^TestLoadTest500Nodes_WriteAndQueryIntegration$' \
-count=1
python3 scripts/check_public_docs.py
```
These commands are a baseline, not a substitute for manual review, dependency
analysis, fuzzing, deployment-specific testing, or an independent threat model.
## Reporting findings
Please report suspected vulnerabilities privately to
<security@pulseapp.io>. Include the affected version or commit, deployment
model, required privileges, reproduction steps, expected impact, and any
suggested containment. Do not include live credentials or customer data.
General defects and hardening suggestions that do not disclose an exploitable
weakness can use the public [issue tracker](https://github.com/rcourtman/Pulse/issues).
The canonical disclosure contact and current security guidance remain in the
[Security Policy](../SECURITY.md).

View file

@ -1,4 +1,4 @@
import { Component, For, type JSX, Show, createMemo, createSignal } from 'solid-js';
import { Component, For, type JSX, Show, createEffect, createMemo, createSignal } from 'solid-js';
import type { ConnectionType, ProbeCandidate } from '@/api/connections';
import { AddressProbeStep } from './AddressProbeStep';
import {
@ -54,6 +54,13 @@ export const ConnectionEditor: Component<ConnectionEditorProps> = (props) => {
const showCredentialSlot = () => activeType() !== null;
const autoDetectLabels = createMemo(() => getInfrastructureAutoDetectLabels());
createEffect(() => {
const routeType = props.initialType ?? null;
const routeCandidate = props.initialCandidate ?? null;
setSelectedType(routeType);
setSelectedCandidate(routeCandidate);
});
const chooseCandidate = (candidate: ProbeCandidate) => {
if (props.onSelectCandidate) {
props.onSelectCandidate(candidate);

View file

@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
import { ConnectionsAPI, type ProbeResponse } from '@/api/connections';
import { createSignal } from 'solid-js';
import { ConnectionsAPI, type ConnectionType, type ProbeResponse } from '@/api/connections';
import { ConnectionEditor } from '../ConnectionEditor';
const connectionsApiMock = vi.hoisted(() => ({
@ -174,6 +175,23 @@ describe('ConnectionEditor', () => {
expect(screen.queryByRole('button', { name: /probe api endpoint/i })).not.toBeInTheDocument();
});
it('retargets the credential slot when a direct route changes type', async () => {
const [initialType, setInitialType] = createSignal<ConnectionType>('agent');
const renderSlot = vi.fn((props) => <div data-testid="slot">slot:{props.type}</div>);
render(() => (
<ConnectionEditor
initialType={initialType()}
renderCredentialSlot={renderSlot}
onClose={() => {}}
/>
));
expect(screen.getByTestId('slot').textContent).toBe('slot:agent');
setInitialType('pve');
await waitFor(() => expect(screen.getByTestId('slot').textContent).toBe('slot:pve'));
});
it('skips probe setup when an initialType is supplied in edit mode', () => {
const renderSlot = vi.fn((props) => <div data-testid="slot">slot:{props.type}</div>);

View file

@ -17,6 +17,7 @@ import {
type InfrastructureCommandSection,
type InstallProfile,
} from './infrastructureOperationsModel';
import { buildInfrastructureOnboardingPath } from './infrastructureWorkspaceModel';
import { useInfrastructureOperationsContext } from './useInfrastructureOperationsState';
export type InfrastructureInstallerFocus =
@ -48,10 +49,10 @@ const INSTALLER_FOCUS_PRESENTATION: Record<
agent: {
title: 'Install on a host',
description:
'Start here to add the first system you want Pulse to monitor, then expand into Docker, Kubernetes, Proxmox, and related infrastructure.',
'Install the agent for host-local, Docker, or Kubernetes telemetry. API-backed platforms such as Proxmox start under Platform connections.',
recommendationTitle: 'Recommended install model',
recommendationDetail:
'Pulse Agent is a low-overhead background service. Machines in Pulse are systems with the agent installed and reporting full node-local telemetry such as CPU, memory, disks, network I/O, temperatures, SMART disk health, services, Docker, or Kubernetes coverage. Use Availability checks for ping-only or agentless device monitoring. For Proxmox clusters, keep the cluster API connection for platform inventory and add the agent to each node for host-level augmentation.',
'Pulse Agent is a low-overhead background service. It collects node-local telemetry such as CPU, memory, disks, network I/O, temperatures, SMART disk health, services, Docker, and Kubernetes. For Proxmox, start with a dedicated read-only or narrowly scoped API token; no root agent is required for normal platform inventory and metrics. Add the agent only to nodes that need host-local augmentation. Use Availability checks for ping-only or agentless device monitoring.',
preferredProfile: 'auto',
platforms: ALL_AGENT_PLATFORMS,
},
@ -247,6 +248,41 @@ export const InfrastructureInstallerSection: Component<InfrastructureInstallerSe
</p>
</div>
<Show when={focus() === 'agent'}>
<div
data-testid="proxmox-api-first-guidance"
class="rounded-md border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-950 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-50"
>
<p class="font-semibold">Adding Proxmox? Start with the API connection.</p>
<p class="mt-1 text-xs text-blue-800 dark:text-blue-200">
Pulse can monitor normal PVE and PBS inventory, guests, storage, utilization, and
status without installing software as root on the Proxmox host. Add a host agent later
only for telemetry the API cannot provide, such as SMART, temperatures, and host-local
storage detail.
</p>
<div class="mt-2 flex flex-wrap gap-x-4 gap-y-2 text-xs font-medium">
<a
href={buildInfrastructureOnboardingPath('pve')}
class="text-blue-700 underline decoration-blue-300 underline-offset-2 hover:text-blue-900 dark:text-blue-200 dark:hover:text-white"
>
Connect Proxmox VE through the API
</a>
<a
href={buildInfrastructureOnboardingPath('pbs')}
class="text-blue-700 underline decoration-blue-300 underline-offset-2 hover:text-blue-900 dark:text-blue-200 dark:hover:text-white"
>
Connect PBS through the API
</a>
<a
href="/docs/PRODUCTION_SECURITY"
class="text-blue-700 underline decoration-blue-300 underline-offset-2 hover:text-blue-900 dark:text-blue-200 dark:hover:text-white"
>
Review the production security model
</a>
</div>
</div>
</Show>
<Show when={focus() === 'docker'}>
<div class="rounded-md border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-950 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-50">
<p class="font-semibold">Docker inside Proxmox LXCs</p>

View file

@ -206,6 +206,22 @@ describe('infrastructure operations model', () => {
expect(autoProfile?.description).toContain('every detected PVE / PBS service');
});
it('keeps generic host onboarding explicit about the API-first Proxmox path', () => {
expect(infrastructureInstallerSectionSource).toContain(
'Adding Proxmox? Start with the API connection.',
);
expect(infrastructureInstallerSectionSource).toContain(
'no root agent is required for normal platform inventory and metrics',
);
expect(infrastructureInstallerSectionSource).toContain(
"buildInfrastructureOnboardingPath('pve')",
);
expect(infrastructureInstallerSectionSource).toContain(
"buildInfrastructureOnboardingPath('pbs')",
);
expect(infrastructureInstallerSectionSource).toContain('/docs/PRODUCTION_SECURITY');
});
it('keeps the Docker install profile aligned with the shared Docker and Podman label', () => {
const dockerProfile = INSTALL_PROFILE_OPTIONS.find((option) => option.value === 'docker');
@ -252,7 +268,7 @@ describe('infrastructure operations model', () => {
'Pulse Agent is a low-overhead background service.',
);
expect(infrastructureInstallerSectionSource).toContain(
'Machines in Pulse are systems with the agent installed',
'API-backed platforms such as Proxmox start under Platform connections.',
);
expect(infrastructureInstallerSectionSource).toContain(
'Use Availability checks for ping-only or agentless device monitoring.',
@ -262,9 +278,9 @@ describe('infrastructure operations model', () => {
'before asking for administrator privileges',
);
expect(infrastructureInstallerSectionSource).toContain(
'For Proxmox clusters, keep the cluster API',
'For Proxmox, start with a dedicated read-only or narrowly scoped API token',
);
expect(infrastructureInstallerSectionSource).toContain('host-level');
expect(infrastructureInstallerSectionSource).toContain('host-local');
expect(infrastructureInstallerSectionSource).toContain('augmentation.');
expect(infrastructureInstallerSectionSource).toContain('Installation commands');
expect(infrastructureInstallerSectionSource).toContain(