Commit Graph

15 Commits

Author SHA1 Message Date
rcourtman c035c5b514 Cover the missing arms of the SSRF, clone and classifier guards
A second pass targeting PARTIALLY covered functions rather than untouched
ones, so every case here is an arm the existing suites never reached.
Percentages are per-function coverage, measured before and after.

- pkg/securityutil: the SSRF guards, which is where the uncovered arms
  actually matter. isCarrierGradeNATIPv4 and isLocalNetworkIP are pinned at
  the first and last address of 100.64.0.0/10 and just outside both ends,
  across loopback, link-local, every RFC1918 range and IPv6 unique-local.
  joinURLPath, IsLocalNetworkHost, resolveOutboundIPAddrs and
  cloneRestrictedTransport to 100 percent, resolvePermittedOutboundIPs to
  96.8, with the transport clone asserted independent of its source.
- internal/models: eleven deep-copy helpers from as low as 25 percent to 100.
  Every one asserts real independence, mutating each nested slice, map and
  pointer field of the clone and checking the original is untouched, which is
  the failure mode a deep-copy helper actually has.
- internal/alerts: metricClearThreshold 28.6 to 100, resourceTypeLabel and
  alertspecsMetricTriggered 50 to 100, the four canonical spec-id and
  tracking-key builders 66.7 to 100, inferCanonicalKindFromLegacyAlert to 100,
  and the backup-snapshot and ack-identity predicates.
- internal/servicediscovery: the four fingerprint generators to 100, each
  asserted for both stability and sensitivity; the three command builders and
  ValidateResourceID on their exact output and each rejection reason.
- internal/storagehealth: zfsScanActive and firstNonEmpty.

cephClusterSourceRank is deliberately left at 75 percent: its default arm is
unreachable because normalizeCephClusterSource can only return the two cases
above it. That is recorded rather than faked.

No source file is modified. Adversarial review returned no rejects and flagged
nine re-hit subtests; all nine were removed and every target function
re-measured at an identical percentage, proving they carried nothing.

PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT=test-only branch coverage, no source or contract change
2026-07-25 00:39:15 +01:00
rcourtman 4a2335ce7f fix(ai): harden local compatible providers 2026-07-24 12:35:31 +01:00
rcourtman 278673aa0f Add Go branch-coverage tests for twelve pure backend helpers
New *_branchcov0718_test.go files raise coverage of previously-uncovered pure
functions across ten packages. Covered areas include securityutil SSRF and URL
validation, truenas path and telemetry parse helpers, storagehealth SMART and
physical-disk risk assessment, vmware inventory sort keys and error classifiers,
servicediscovery token filtering and readiness, telemetry evidence-from-history,
models ToFrontend converters and frontend NormalizeCollections normalizers,
actionplanner type predicates and canonical resource-id sort, config API-token
accessors, and licensing state accessors.

Test-only, with no runtime or subsystem-contract change. Verified in a clean
worktree at HEAD with go vet and package tests green, gofmt clean, and every
named target function moved from 0 percent to covered.
2026-07-19 15:10:09 +01:00
rcourtman 64fb3d198d fix(securityutil): dial every permitted resolved IP instead of pinning the first
The restricted outbound dialer resolved a hostname, validated the results,
then dialed only the first permitted IP. When localhost resolves to ::1
ahead of 127.0.0.1 and the target service binds only 127.0.0.1 (Ollama's
default), the dial failed with 'connect: connection refused' even though
curl and browsers connect fine via address-family fallback. This broke the
AI provider setup quickstart: the blessed http://localhost:11434 Ollama URL
failed Pulse's connection test on IPv6-first hosts (setup-friction class of
#847/#1003).

Try each permitted IP from the validating resolution in order until one
connects. Every candidate is still validated against the outbound policy
from the same resolution, so the DNS-rebinding pinning guarantee is
unchanged. Fixes all consumers of the shared client (Ollama provider, OIDC,
SSO, connection probes, availability poller); api-contracts wording updated
to match. Live-verified against Ollama 0.32.1 bound to 127.0.0.1 only:
TestConnection via http://localhost:11434 failed before, passes after.
2026-07-17 23:35:45 +01:00
rcourtman df4e02ef89 Add Go branch-coverage tests for models config securityutil and aicontracts helpers
Adds table-driven branch-coverage unit tests for previously uncovered pure
functions across internal/models, internal/config, pkg/securityutil and
pkg/aicontracts. New test files only, with no source changes.

Covers model collection normalizers, reflect-based metric deep-copy,
removed-host agent bookkeeping, report-schedule and entitlement-billing
normalization, effective AI control and Patrol autonomy resolution,
websocket-origin host normalization, and command-safety classifiers. 61
TestBranchCov functions in 8 files, all vet and gofmt clean.
2026-07-16 14:20:13 +01:00
rcourtman 47f2bdaed4 Add Go branch-coverage tests for pure config and helper packages
Adds table-driven branch-coverage unit tests for previously untested pure
helper functions across internal/alerts/config, internal/config,
internal/ai/safety, internal/ai/modelresolution, internal/operationreceipt,
internal/models, internal/securityutil and pkg/securityutil. New test files
only, with no source changes.

Covers alert-config normalization and validation, sensitive-path and
redaction classifiers, URL normalizers, provider model resolution,
operation-receipt decoding, credential masking, and account-to-org role
mapping. 57 TestBranchCov functions in 12 files, all vet and gofmt clean.
2026-07-16 10:19:51 +01:00
rcourtman 93fff4f1d8 Add an explicit operator override for plaintext HTTP to non-local Pulse hosts
Fleets on networks numbered from nominally public IP space (issue
#1522: an AD estate on 192.20.0.0/16) cannot pass the agent's
local-network plaintext heuristic, and the only workaround was pointing
a .internal DNS alias at the server, which bypasses the same control
less visibly than a flag would. --allow-plaintext-http
(PULSE_AGENT_ALLOW_PLAINTEXT_HTTP) records process-wide consent once at
agent startup before any module validates a URL, covers every agent
transport including the websocket command channel, warns at startup
that the API token travels in cleartext, defaults closed, and is never
emitted by generated install commands or settable by the server.
2026-07-14 16:42:02 +01:00
rcourtman 042e7ef966 Harden remaining CodeQL security boundaries 2026-07-09 19:46:40 +01:00
rcourtman e7334e66e4 Allow local DNS agent HTTP URLs
Refs #1522
2026-07-06 13:53:19 +01:00
rcourtman 314429b879 Fix v5 agent update recovery
Issue: #1515
2026-07-05 10:08:19 +01:00
rcourtman 6344d17fd1 Allow local Pulse HTTP agent URLs
Refs #1505
2026-07-03 00:24:40 +01:00
rcourtman b6f7091952 fix: block SSRF bypass through HTTP proxy in restricted outbound client
NewRestrictedOutboundHTTPClient validates target IPs via a custom
DialContext, but when an HTTP proxy is configured (HTTP_PROXY env var)
DialContext only validates the proxy's address — the actual target host
is never checked. This allows requests to cloud metadata service
addresses (169.254.169.254) and other blocked IPs through the proxy.

Add a restrictedRoundTripper wrapper that validates the request URL
hostname against resolvePermittedOutboundIP before forwarding. This
provides defense-in-depth that works regardless of proxy configuration,
while the existing DialContext guard continues to prevent DNS rebinding
for direct connections.
2026-06-26 13:05:23 +01:00
rcourtman eb98c13896 Allow insecure dev HTTP agent runtime URLs 2026-04-23 13:48:54 +01:00
rcourtman 8a42bb432e Harden commercial callback and transport URL policy 2026-04-22 10:57:21 +01:00
rcourtman 9c8387be6f Export restricted outbound HTTP security helpers 2026-04-22 10:05:57 +01:00