diff --git a/frontend-modern/public/docs/API.md b/frontend-modern/public/docs/API.md index 4f5e37e6d..f88740261 100644 --- a/frontend-modern/public/docs/API.md +++ b/frontend-modern/public/docs/API.md @@ -100,6 +100,7 @@ Returns the unified resource list with pagination + aggregations. Requires `moni Query params: - `type`: comma-separated list (e.g., `agent`, `vm`, `system-container`, `container`, `docker-service`, `storage`, `pbs`, `pmg`, `k8s-cluster`, `k8s-node`, `pod`, `k8s-deployment`, `physical_disk`, `ceph`) - `source`: comma-separated list (e.g., `proxmox`, `agent`, `docker`, `pbs`, `pmg`, `kubernetes`) +- `excludeSource`: comma-separated list of sources that must not be present on a matching resource - `status`: comma-separated list (`online`, `offline`, `warning`, `unknown`) - `parent`: parent resource ID - `cluster`: cluster name diff --git a/frontend-modern/public/docs/CODE_SIGNING_POLICY.md b/frontend-modern/public/docs/CODE_SIGNING_POLICY.md index cd83ce9e1..d4718aa89 100644 --- a/frontend-modern/public/docs/CODE_SIGNING_POLICY.md +++ b/frontend-modern/public/docs/CODE_SIGNING_POLICY.md @@ -75,19 +75,43 @@ Normal stable publication and stable dry runs select `signpath` directly. and verified independently after publication. - The GitHub-hosted `build-release-candidate.yml` assembly job emits SLSA v1 provenance for every candidate file after complete local validation. Its - portable Sigstore bundle is published as - `release-build-provenance.sigstore.json` and covered by the immutable - candidate manifest. Verification rejects self-hosted provenance. + Sigstore bundle is then added to the immutable candidate as + `release-build-provenance.sigstore.json`; publication cannot replace that + bundle without failing the candidate manifest. This preserves the exact + builder evidence for offline verification instead of recreating provenance + in the later publication job. +- The exact-version OCI Helm chart is published only by the hosted + `publish-helm-chart.yml` workflow. Its SHA-256 manifest digest and GitHub + build-provenance attestation must bind to the release source commit before + the digest enters `release-activation.json`. Activation recovery repeats + that verification, and Helm Pages refuses to advertise a chart whose OCI + tag, signer workflow, source commit, or digest has drifted from the immutable + activation packet. +- Release activation requires GitHub CLI 2.97.0 or newer, which includes the + literal signer-identity matcher fix. The shared + `scripts/require-safe-gh-attestation.sh` guard enforces this floor. The + published checksum manifest must carry build provenance from the exact + `build-release-candidate.yml` workflow and release source commit; + repository-level provenance is not sufficient. Immutable releases created + before portable candidate bundles remain verified against their original + `create-release.yml` publication provenance. Both paths reject provenance + emitted from a self-hosted runner. - Every new release is assembled and validated as a draft. Its activation marker is uploaded and digest-checked before publication; GitHub must then report the published release as immutable, protecting its tag and complete - asset set from replacement. + asset set from replacement. A GitHub-hosted publication preflight proves the + repository setting before compilation, signing, private staging, or draft + assembly begins, and activation repeats that setting check immediately before + publication to catch later drift. - Customer-facing image aliases, Helm indexes, paid-runtime pointers, and demo environments are not promoted until `gh release verify --repo rcourtman/Pulse` validates GitHub's signed release attestation and `gh release verify-asset --repo rcourtman/Pulse` binds the - downloaded activation marker to that attestation. Operators can use the same - commands to verify the packet and any downloaded release asset independently. + downloaded activation marker to that attestation. The activation verifier + also binds the release's downloaded `checksums.txt` to that immutable packet + and verifies its exact workflow and source provenance. Operators can use the + same commands to verify the packet and any downloaded release asset + independently. ## Project roles diff --git a/frontend-modern/public/docs/PRODUCTION_SECURITY.md b/frontend-modern/public/docs/PRODUCTION_SECURITY.md index 939b9fe98..6d2fc393f 100644 --- a/frontend-modern/public/docs/PRODUCTION_SECURITY.md +++ b/frontend-modern/public/docs/PRODUCTION_SECURITY.md @@ -36,9 +36,10 @@ temperature sensors, Docker or Podman sockets, host-local storage state, and some platform integrations require root or equivalent access. That is a real security boundary, not a cosmetic implementation detail. -The default posture limits that boundary: +The fresh-install posture limits that boundary: -- command execution is disabled unless an operator explicitly enables it; +- the service is marked `monitoring-only`, remote configuration cannot promote + it, and its credential omits `agent:exec`; - the health and Prometheus listener binds to `127.0.0.1:9191` by default; - generated systemd units apply service hardening including `NoNewPrivileges=true`, private temporary storage, and kernel/control-group @@ -48,6 +49,13 @@ The default posture limits that boundary: - Proxmox guest Docker inventory through `pct exec` is disabled by default and requires an explicit server setting. +The advanced **legacy combined command profile** is a separate trust decision. +It marks the root collector command-capable and gives its credential execution +scope so the same process can accept governed server requests. Existing +unmarked installations remain in a visible `legacy` compatibility state during +the migration. Agent Doctor reports both local authority and credential scope +so an over-scoped monitoring install is not mistaken for a safe default. + On standard Linux systemd hosts the installer also offers a supported least-privilege profile: `--least-privilege` runs the service as a dedicated `pulse-agent` system user, with optional `--grant-smart` and `--grant-pct` diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx index a598e639b..0f1926b21 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx @@ -254,7 +254,9 @@ describe('infrastructure operations model', () => { expect(infrastructureInstallerSectionSource).toContain( 'PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true', ); - expect(infrastructureInstallerSectionSource).toContain('bounded pct exec'); + expect(infrastructureInstallerSectionSource).toMatch( + /bounded[\s\S]{0,20}?pct exec<\/code>/, + ); expect(infrastructureInstallerSectionSource).toContain('Install on a Kubernetes node'); expect(infrastructureInstallerSectionSource).toContain( 'state.handleInstallProfileChange(presentation().preferredProfile)', @@ -288,10 +290,10 @@ describe('infrastructure operations model', () => { 'Generate an install token first. Pulse will then build copy-ready commands', ); expect(infrastructureInstallerSectionSource).toContain( - 'Allow Pulse-scoped command requests on this agent for Patrol actions and opted-in Proxmox LXC Docker inventory', + 'Install the transitional combined runtime that can accept server command requests', ); expect(infrastructureInstallerSectionSource).toContain( - 'Enable Pulse command execution (Patrol actions and Proxmox LXC Docker inventory)', + 'Enable legacy combined command profile', ); expect(infrastructureInstallerSectionSource).not.toContain('Patrol auto-fix'); expect(infrastructureInstallerSectionSource).not.toContain('auto-fix requires Pulse Pro'); diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx index 84add8d14..768497ec8 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx @@ -92,6 +92,9 @@ vi.mock('../useInfrastructureOperationsState', () => ({ acknowledgeNoToken: vi.fn(), commandsUnlocked: () => true, confirmedNoToken: () => true, + customCaPath: () => '', + insecureMode: () => false, + selectedAgentUrl: () => 'http://pulse', getAgentConnectionUpgradeCommand: (connection: Connection, installFlags: string[] = []) => `upgrade ${connection.id}${installFlags.length > 0 ? ` ${installFlags.join(' ')}` : ''}`, getAgentConnectionUpgradeCommandRequiresToken: () => false, diff --git a/frontend-modern/src/components/Settings/__tests__/NodeModalSetupGuideSection.test.tsx b/frontend-modern/src/components/Settings/__tests__/NodeModalSetupGuideSection.test.tsx index 23c4fff25..232c6c8a8 100644 --- a/frontend-modern/src/components/Settings/__tests__/NodeModalSetupGuideSection.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/NodeModalSetupGuideSection.test.tsx @@ -111,7 +111,9 @@ describe('NodeModalSetupGuideSection', () => { expect(screen.getAllByText(/Pulse Agent root service/i).length).toBeGreaterThan(0); expect(screen.getByText(/temperatures, SMART, ZFS, Ceph, and mdadm/i)).toBeInTheDocument(); expect(screen.getByText(/Docker inside Proxmox LXCs:/i)).toBeInTheDocument(); - expect(screen.getAllByText(/Pulse command execution/i).length).toBeGreaterThan(0); + expect(screen.getAllByText(/does not grant remote\s+command authority/i).length).toBeGreaterThan( + 0, + ); const tlsOverride = screen.getByRole('checkbox', { name: /Skip TLS certificate verification when downloading the installer/i, }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 496cc4973..c6928f191 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -890,13 +890,13 @@ describe('settings architecture guardrails', () => { 'coarse deployment and lifecycle buckets', ); expect(EN_MESSAGES['settings.general.telemetry.description']).toContain( - 'aggregate resource and outcome counts, coarse feature flags, and content-free Patrol, Assistant, and capability-API usage counters', + 'aggregate resource and outcome counts, coarse feature flags, content-free Patrol, Assistant, and capability-API usage counters, and four session-deduplicated workload-history adoption counts', ); expect(EN_MESSAGES['settings.general.telemetry.description']).not.toContain( 'Pulse Intelligence loop adoption', ); expect(EN_MESSAGES['settings.general.telemetry.description']).toContain( - 'identifiers, URLs, paths, locale, browser events, prompts, chat messages, command text, action output, token values, names, email addresses, or IP addresses', + 'identifiers, URLs, paths, locale, raw browser events, an event-level clickstream, prompts, chat messages, command text, action output, token values, names, email addresses, or IP addresses', ); expect(generalSettingsPanelSource).toContain('settings.general.telemetry.payloadAriaLabel'); expect(generalSettingsPanelSource).toContain('settings.general.telemetry.resetId'); diff --git a/frontend-modern/src/features/alerts/AlertDeliveryLogCard.test.tsx b/frontend-modern/src/features/alerts/AlertDeliveryLogCard.test.tsx index a464dc46d..c720fec13 100644 --- a/frontend-modern/src/features/alerts/AlertDeliveryLogCard.test.tsx +++ b/frontend-modern/src/features/alerts/AlertDeliveryLogCard.test.tsx @@ -118,7 +118,7 @@ describe('AlertDeliveryLogCard', () => { expect(screen.getByText(/Completed attempts are retained for 7 days/)).toBeInTheDocument(); expect( - screen.getByText(/failures that exhausted retries remain available for 30 days/), + screen.getByText(/Failures that exhausted retries remain available for 30 days/), ).toBeInTheDocument(); const timestamps = Array.from(container.querySelectorAll('time')); expect(timestamps[0]).toHaveAttribute('datetime', log.entries[0].timestamp); diff --git a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts index 32803ba7b..2d6a7f9cf 100644 --- a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts +++ b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts @@ -415,7 +415,7 @@ describe('platform overview layout guardrails', () => { it('keeps remaining platform phone identity and actions usable', () => { expect(truenasStorageTopologyTableSource).toMatch( - /sortKey="kind"[\s\S]{0,120}?class="platform-table-mobile-w-15 md:w-\[10%\]"/, + /sortKey="kind"[\s\S]{0,160}?platform-table-mobile-w-15 md:w-\[10%\]/, ); expect(truenasStorageTopologyTableSource).toContain("return 'pl-6 sm:pl-11'"); expect(truenasStorageTopologyTableSource).toMatch( @@ -491,7 +491,9 @@ describe('platform overview layout guardrails', () => { expect(truenasPageSurfaceSource).toContain('