Commit Graph

142 Commits

Author SHA1 Message Date
rcourtman dbca44937b Add privacy-safe telemetry lifecycle and outcome signals 2026-07-23 01:10:38 +01:00
rcourtman 057cf74629 Add alert intent policies and delivery receipts 2026-07-20 20:27:39 +01:00
rcourtman 83672a00a8 Harden Operational Trust rollout contracts 2026-07-19 15:11:02 +01:00
rcourtman cf0486492e Build operational trust lifecycle foundation 2026-07-19 15:10:38 +01:00
rcourtman b5d852a279 fix(notifications): webhook dialer tries every permitted resolved IP instead of pinning the first
Same defect class as 64fb3d198 (pkg/securityutil): the SSRF-pinned webhook
dialer resolved the host and dialed only the first permitted IP, so a host
resolving to ::1 ahead of 127.0.0.1 while the receiver listens on one
loopback family — or a multi-A-record host with a dead leading address —
got a hard connection failure even though curl works. The dialer now tries
each permitted resolved IP in resolution order; every candidate still
passes the private-IP/allowlist validation, so rebinding protection is
unchanged.

Evaluated migrating onto securityutil.NewRestrictedOutboundHTTPClient and
rejected a straight swap: the webhook client's isPrivateIP blocks a wider
range set (CGNAT, benchmarking, TEST-NET), its redirect policy allows
re-validated cross-origin hops where securityutil enforces same-origin,
Proxy is deliberately nil, and the private-IP allowlist is a live per-CIDR
runtime hook — a swap would loosen the SSRF policy and change behavior.

Adds a resolver test seam on NotificationManager and a regression test
pinning the ::1-first fallback; notifications contract records the
multi-IP dial obligation.

Closes chip task_5e91afd9.
2026-07-17 23:49:02 +01:00
rcourtman 4bd9b0c079 Show timezone on alert email start times
Alert start times are stored in UTC and the email templates rendered
them with no zone conversion or label, so the Started line read as a
local clock while showing UTC (#1582). Convert to the server's local
zone and include the zone name.
2026-07-16 09:29:01 +01:00
rcourtman 76dc690840 Suppress recovery notifications when the firing never left the queue
An alert that resolved while its firing notification was still in the
grouping window or waiting in the persistent queue (alert delay pushes
activation close to resolution; quiet-hours replay defers delivery)
produced a recovery-only notification: CancelAlert dropped the queued
firing, but LastNotified had been set optimistically at dispatch, so
the resolved-notification gate believed the firing had been sent.

CancelAlert now reports whether it cancelled a firing notification that
had not been delivered (grouping window entries and pending queue rows;
mid-send rows are excluded because their delivery may still complete),
and handleAlertResolved suppresses the recovery in that case. This also
covers the quiet-hours replay bypass: a recovery only follows a deferred
firing if the replay was actually delivered.

Addresses #1553
2026-07-11 18:28:28 +01:00
rcourtman 175309b8f7 Thread alert emails with their resolved notifications
Every email covering a single alert now carries In-Reply-To and
References headers set to a deterministic incident thread ID derived
from the alert ID and firing start time, so mail clients thread the
firing, re-notification, and resolved emails of one incident together.
Message-ID stays unique per send because re-notified incidents send
multiple emails and some providers de-duplicate on Message-ID. Grouped
emails skip threading since firing and resolved batches rarely contain
the same alert set.

Addresses #1543 (discussion)
2026-07-11 18:22:07 +01:00
rcourtman a44e5513ca Register template funcs on the enhanced ntfy payload path
The ntfy branch of prepareEnhancedWebhookExecution parsed the payload
template without templateFuncMap(), so the built-in ntfy preset (which
uses {{.Type | title}}) failed to parse and Test sends returned HTTP
400 with "function \"title\" not defined". Register the func map on
that parse, matching the generic webhook path, and add a regression
test that renders the actual built-in ntfy preset template.

Addresses #1549
2026-07-11 18:11:22 +01:00
rcourtman 24b2e40e92 Harden scanned request and storage boundaries
Harden CodeQL-scanned request, command, path, and frontend sinks across relay proxying, availability probes, connection probing, notification CLI execution, report storage, licensing persistence, preview bootstrapping, tooltip rendering, logging, and test identity generation.
2026-07-09 17:22:29 +01:00
rcourtman 4382919447 Add MSP report scheduling and alert rollup 2026-07-07 20:37:18 +01:00
rcourtman c0ac0762da Fix security scan findings
Harden proxy-auth admin role checks, metrics listener exposure, Teams webhook escaping, and dependency lockfiles.
2026-07-01 09:55:35 +01:00
rcourtman 22ed97f562 fix(email): eliminate data race on provider username resolution
sendViaProviderWithAddresses mutated the shared e.config.Username for
provider-specific defaults (SendGrid, Postmark, SparkPost, Resend).
If concurrent goroutines sent email simultaneously, this was a data
race on the config struct.

Move the resolution into negotiateAuth via a local variable
(resolveProviderUsername helper) so the shared config is never mutated.
2026-06-27 17:36:51 +01:00
rcourtman 1cf92e6c10 Pin metadata GET zero-record payload contract
Adds TestContract_MetadataGetPayloadsUseZeroRecordsInsteadOf404: empty
guest/docker metadata maps must serialize as {} (never null) and a
missing resource must return a 200 zero record echoing the requested ID
(never a 404). This is the proof companion to the
metadata_handlers_shared.go consolidation in the previous commit — it
was authored with that change but lost to a shared-index race at commit
time.
2026-06-10 10:53:57 +01:00
rcourtman a223b02dd5 Stamp tenant identity into alert webhook payloads
Alert webhook payloads now carry the tenant that fired them, so MSP/PSA
receivers (ConnectWise and similar) can route tickets by client without
inferring the tenant from which webhook endpoint fired.

- WebhookPayloadData gains TenantID/TenantName, exposed to custom
  templates as {{.TenantID}}/{{.TenantName}}.
- Defaults come from PULSE_TENANT_ID/PULSE_TENANT_NAME (already injected
  into provider-hosted client runtimes; name falls back to ID).
- Shared-process multi-tenant orgs override via a lazy org-backed
  resolver wired in MultiTenantMonitor.GetMonitor, so display-name
  renames are picked up without restart.
- Generic service template emits a tenant block when identity is set,
  omits it otherwise; single-tenant payloads are unchanged.
- Notifications and monitoring subsystem contracts updated with the
  tenant-identity ownership boundary; guardrail test pins the org
  wiring.
2026-06-10 10:03:39 +01:00
rcourtman 85ec355268 Preserve webhook mentions in list API and resolved notifications (#1118)
Back-port v5 fixes 5997fd81f and 0a7b93a84 to v6:
- GetWebhooks list response now includes the configured mention so the UI
  shows it after reload instead of blanking it.
- sendResolvedWebhook now assigns data.Mention (v6 set it for grouped/
  firing webhooks but dropped it on resolved), and the Discord/Slack/Teams/
  Mattermost ResolvedPayloadTemplate strings gained {{if .Mention}} guards.

Without these, a configured @everyone/@channel was silently omitted from
resolved/cleared notifications. Adds list-API and per-service resolved
mention regression tests.
2026-06-04 09:29:47 +01:00
rcourtman faefe6edc8 Remove 198 unreachable Go functions
Dead-code sweep. Functions flagged unreachable by golang.org/x/tools/cmd/deadcode
and confirmed unused across pulse, pulse-enterprise, pulse-pro and pulse-mobile by
adversarial cross-repo verification. Cross-module reachability was checked
explicitly (only pkg/ exported symbols are importable by other modules; internal/
packages and _test.go files are not). go build, go vet and test-compile all pass.
2026-06-03 12:29:37 +01:00
rcourtman d6964832a0 Add entitlement-gated report branding 2026-06-02 18:11:25 +01:00
rcourtman d8f154addd Fix alert escalation notification scheduling
Refs #1444
2026-06-01 14:16:57 +01:00
rcourtman d3934e19e3 Harden repository advisory boundaries 2026-05-24 08:15:29 +01:00
rcourtman 31331b5451 Fix grouped notification cancellation 2026-05-13 13:00:43 +01:00
rcourtman 1836e2a3cc Fix mixed quiet-hours notification replay queueing 2026-05-13 12:14:56 +01:00
rcourtman 6112fcd5ae Replay quiet-hours alert notifications 2026-05-13 11:54:56 +01:00
rcourtman b0464b6c59 Fix notification queue race test 2026-05-01 22:26:01 +01:00
rcourtman af8a5f0740 Port RC3 maintenance fixes from v5
Refs #1440, #1444, #1451
2026-05-01 20:28:11 +01:00
rcourtman 05fa111ca1 Stabilize backend race tests for v6 RC publish 2026-04-11 22:46:34 +01:00
rcourtman 675dff7da5 Preserve queued resolved notifications on cancellation 2026-04-01 17:10:10 +01:00
rcourtman b8a551ce22 Forward-port webhook JSON template escaping 2026-04-01 17:04:40 +01:00
rcourtman a253016327 Harden Apprise server URL base handling 2026-04-01 15:50:30 +01:00
rcourtman 22a59a8646 Harden email MIME transport construction 2026-03-29 14:50:36 +01:00
rcourtman b398c9c405 Harden notification queue persistence root 2026-03-29 14:35:36 +01:00
rcourtman 53f41fdb45 Harden webhook request URL validation 2026-03-29 13:18:40 +01:00
rcourtman 9ac8048acd Harden email delivery and TLS probing 2026-03-29 13:02:36 +01:00
rcourtman d6536932fc Harden outbound URLs and file-backed storage 2026-03-29 12:47:55 +01:00
rcourtman ff8b64d3ef fix(notifications): classify webhook HTTP retries canonically 2026-03-27 12:15:17 +00:00
rcourtman 7c9eac5539 Centralize alert webhook mention metadata 2026-03-19 05:21:22 +00:00
rcourtman 98757dec63 Centralize alert webhook labels 2026-03-19 05:16:02 +00:00
rcourtman 3aea37c3d2 Centralize alert webhook service metadata 2026-03-19 05:10:52 +00:00
rcourtman 778a2577b6 feat: Pulse v6 release 2026-03-18 16:06:30 +00:00
rcourtman 9b531c547d Fix recovery notifications silently disabled by config PUT (#1332)
Two fixes for missing recovery/resolved notifications:

1. API config PUT handler now preserves notifyOnResolve when the client
   omits it from the request body. Go decodes a missing bool as false,
   which silently disabled recovery notifications on older clients.

2. CancelAlert now always cleans up the cooldown record even when the
   alert has already left the pending buffer, preventing stale cooldown
   entries from suppressing future alert cycles.
2026-03-09 11:28:28 +00:00
rcourtman 0dd3fc779b Fix alert disable notification suppression 2026-03-07 18:40:08 +00:00
rcourtman 464d3f8486 Fix stale queued notification delivery 2026-03-05 23:46:35 +00:00
rcourtman a3fcaafbdb test(notifications): remove queue stats race with background processor 2026-03-03 21:37:27 +00:00
rcourtman eb2397d99a fix(notifications): route escalation notifications to selected channels only (#1259)
Escalation was calling SendAlert() which always sends to all enabled
channels, ignoring the per-level channel selection (email/webhook/all).

Add SendAlertToChannels() that snapshots only the requested channel
configs and uses a distinct "_escalation" queue type so the dequeue
handler skips cooldown writes — preventing interference with the alert
manager's own re-notify cadence.
2026-02-26 20:49:10 +00:00
rcourtman 77bd2e70d9 fix(notifications): add service-specific resolved webhook templates (#1259)
Backport from v6 (88d5865a8). Recovery webhook notifications were using
the firing PayloadTemplate which services like Telegram, Teams, Discord
etc. silently rejected as malformed. Now uses a three-tier template
pipeline matching the firing path:
- Tier 1: Custom user template (if configured)
- Tier 2: Service-specific ResolvedPayloadTemplate (Discord green embed,
  Telegram chat_id+text, Slack header blocks, Teams MessageCard/Adaptive,
  PagerDuty event_action:"resolve", Pushover, Gotify, Mattermost)
- Tier 3: Generic JSON fallback (backward compatible)

Also adds Event, ResolvedAt, ResolvedAtISO fields to WebhookPayloadData.
2026-02-24 23:28:33 +00:00
rcourtman 82ccb662f9 fix(notifications): use service-specific templates for resolved webhooks (#1068)
Recovery notifications for Discord, Slack, Teams, PagerDuty, and other
service webhooks were sending a generic JSON payload that lacked the
required format (e.g. Discord needs `embeds`, Slack needs `blocks`),
causing resolved notifications to silently fail.

- Add `prepareResolvedWebhookData` to build template data with Level="resolved"
- Route resolved webhooks through service-specific templates with full
  URL rendering, Telegram ChatID extraction, and PagerDuty routing_key
- Custom user templates take precedence over built-in service templates
- Return errors on service template failures instead of falling back to
  generic payloads that endpoints would reject
- Fix PagerDuty template to send event_action="resolve" for resolved alerts
2026-02-24 10:49:52 +00:00
rcourtman 8a48acef1d fix: hotfix 5.1.5 — node duplication, alert scrambling, ntfy resolved formatting
- fix(models): filter nodes by instance in UpdateNodesForInstance to prevent
  PVE node duplication across poll cycles (#1214, #1192, #1217)
- fix(alerts): sort GetActiveAlerts output for stable ordering, preventing
  hostname scrambling in frontend (#1218)
- fix(notifications): add ntfy-specific resolved webhook formatting with
  plain-text body and proper headers (#1213)
- fix(frontend): respect "hide Docker update actions" setting in
  DockerFilter Update All button (#1219)
- fix(frontend): add missing v prefix to GitHub release tag URLs (#1195)
- fix(monitoring): reduce disk detection warning from Warn to Debug to
  eliminate log spam for pass-through disks (#1216)
- chore: bump VERSION to 5.1.5
2026-02-08 11:48:22 +00:00
rcourtman b3fa409b74 Allow SMTP auth over unencrypted connections, fix rate limit persistence, sanitize diagnostics export
- Replace Go stdlib smtp.PlainAuth (which refuses credentials without TLS)
  with a custom plainAuth that respects the user's explicit transport choice
- Remove TLS guard from LoginAuth for the same reason
- Add RateLimit field to EmailConfig so the user's configured value is
  persisted instead of being silently overwritten with 60
- Implement actual sanitization in the "Export for GitHub" diagnostics
  button (was previously ignored — both exports produced identical data)

Related to #1189
2026-02-04 15:42:47 +00:00
rcourtman 05266d9062 Show node display name in alerts instead of raw Proxmox node name
Alerts previously showed the raw Proxmox node name (e.g., "on pve") even
when users configured a display name (e.g., "SPACEX") via Settings or the
host agent --hostname flag. This affected the alert UI, email notifications,
and webhook payloads.

Add NodeDisplayName field to the alert chain: cache display names in the
alert Manager (populated by CheckNode/CheckHost on every poll), resolve
them at alert creation via preserveAlertState, refresh on metric updates,
and enrich at read time in GetActiveAlerts. Update models.Alert, the
syncAlertsToState conversion, email templates, Apprise body text, webhook
payloads, and all frontend rendering paths.

Related to #1188
2026-02-04 14:26:44 +00:00
rcourtman b9eee668e5 test: expand security regression coverage 2026-02-04 10:28:41 +00:00