Skip to content

chore(deps): bump golang.org/x/net from 0.29.0 to 0.49.0 in /apps/agent - #8

Closed
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/go_modules/apps/agent/golang.org/x/net-0.49.0
Closed

chore(deps): bump golang.org/x/net from 0.29.0 to 0.49.0 in /apps/agent#8
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/go_modules/apps/agent/golang.org/x/net-0.49.0

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Jan 14, 2026

Copy link
Copy Markdown
Contributor

Bumps golang.org/x/net from 0.29.0 to 0.49.0.

Commits
  • d977772 go.mod: update golang.org/x dependencies
  • eea413e internal/http3: use go1.25 synctest.Test instead of go1.24 synctest.Run
  • 9ace223 websocket: add missing call to resp.Body.Close
  • 7d3dbb0 http2: buffer the most recently received PRIORITY_UPDATE frame
  • 35e1306 go.mod: update golang.org/x dependencies
  • 7c36036 http2, webdav, websocket: fix %q verb uses with wrong type
  • ec11ecc trace: fix data race in RenderEvents
  • bff14c5 http2: don't PING a responsive server when resetting a stream
  • 88a6421 dns/dnsmessage: avoid use of "strings" and "math" in dns/dnsmessage
  • 123d099 http2: support net/http.Transport.NewClientConn
  • Additional commits viewable in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot merge will merge this PR after your CI passes on it
  • @dependabot squash and merge will squash and merge this PR after your CI passes on it
  • @dependabot cancel merge will cancel a previously requested merge and block automerging
  • @dependabot reopen will reopen this PR if it is closed
  • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

@dependabot @github

dependabot Bot commented on behalf of github Jan 14, 2026

Copy link
Copy Markdown
Contributor Author

Labels

The following labels could not be found: dependencies, go. Please create them before Dependabot can add them to a pull request.

Please fix the above issues or remove invalid values from dependabot.yml.

Bumps [golang.org/x/net](https://github.com/golang/net) from 0.29.0 to 0.49.0.
- [Commits](golang/net@v0.29.0...v0.49.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot
dependabot Bot force-pushed the dependabot/go_modules/apps/agent/golang.org/x/net-0.49.0 branch from 84459ca to 182dc4f Compare January 26, 2026 15:36
@ToddHebebrand

Copy link
Copy Markdown
Collaborator

@dependabot rebase

@dependabot @github

dependabot Bot commented on behalf of github Feb 8, 2026

Copy link
Copy Markdown
Contributor Author

The dependabot.yml entry that created this PR has been deleted so this PR can't be rebased. Please close the PR so Dependabot can create a new one with the current dependabot.yml.

@ToddHebebrand

Copy link
Copy Markdown
Collaborator

Closing: the Go agent was moved from apps/agent/ to agent/. These dependency updates target the old path. Dependabot should auto-create new PRs for agent/ (see dependabot.yml).

@dependabot @github

dependabot Bot commented on behalf of github Feb 8, 2026

Copy link
Copy Markdown
Contributor Author

OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting @dependabot ignore this major version or @dependabot ignore this minor version. You can also ignore all major, minor, or patch releases for a dependency by adding an ignore condition with the desired update_types to your config file.

If you change your mind, just re-open this PR and I'll resolve any conflicts on it.

@dependabot
dependabot Bot deleted the dependabot/go_modules/apps/agent/golang.org/x/net-0.49.0 branch February 8, 2026 18:11
ToddHebebrand added a commit that referenced this pull request Apr 12, 2026
Two issues found while end-to-end-verifying the 2026-04-11 RLS work
against a fresh test DB. Both would block self-hosted upgraders and
your own prod first boot, even after the parent BYPASSRLS PR landed.

1. autoMigrate: auto-derive DATABASE_URL_APP, hard-fail on bypassrls.

   Before: if DATABASE_URL_APP was unset, the API silently fell back to
   DATABASE_URL (the superuser), bypassing every RLS policy. The only
   signal was a single console.warn buried in boot logs. A self-hosted
   upgrader who never set the env var would silently lose tenant
   isolation while the policies sat installed but inert.

   After:
   - New deriveAppConnectionString() helper builds a breeze_app URL by
     swapping user+password on DATABASE_URL using BREEZE_APP_DB_PASSWORD
     or POSTGRES_PASSWORD. Default config (DATABASE_URL + POSTGRES_PASSWORD)
     now self-configures with no operator action.
   - The probe at the end of autoMigrate now THROWS instead of warning
     when the app connection has BYPASSRLS or SUPERUSER. The container
     refuses to start in any state where RLS isn't actually enforced.
   - 7 new unit tests covering the URL derivation: basic swap, query
     param preservation, host/port preservation, special-char passwords
     round-tripped through percent encoding, null/empty/unparseable
     inputs, postgres:// vs postgresql:// schemes.

2. Migration ordering: new bootstrap file fixes fresh-install crash.

   autoMigrate sorts files alphabetically. On a fresh install (new
   self-hosted user, prod container on first boot) the alphabetical
   order of the 2026-04-11 RLS migrations violates real dependencies:

     bucket-c-phase-6-user-scoped-rls.sql  (sort #8) references
       breeze_current_user_id()        — defined in users-rls.sql (#17)
       breeze_has_partner_access(uuid) — defined in partners-rls.sql (#14)
       users.partner_id, users.org_id  — added in users-rls.sql (#17)

     bucket-c-sessions-rls.sql  (sort #9) references
       breeze_current_user_id()        — defined in users-rls.sql (#17)

   Dev never hit it because dev was migrated incrementally in commit
   merge order, not fresh. A wipe-and-replay against the test DB caught
   it: phase-6 fails with 'function public.breeze_current_user_id()
   does not exist'.

   Fix: a new migration (2026-04-11-a-rls-function-bootstrap.sql) that
   sorts before every other 2026-04-11 file because of the leading -a-
   segment. It pre-creates the three dependencies in idempotent form
   (CREATE OR REPLACE FUNCTION, ADD COLUMN IF NOT EXISTS) using verbatim
   bodies copied from the originating migrations. When users-rls.sql
   and partners-rls.sql later run, their CREATE OR REPLACE statements
   are no-ops against the objects already created here.

   Verified idempotent against the dev DB (where the originating
   migrations are already applied) — both runs of the bootstrap show
   CREATE FUNCTION + ADD COLUMN IF NOT EXISTS skip notices, audit query
   still returns zero RLS gaps.

Verification:
- 26/26 autoMigrate unit tests green
- 5/5 RLS coverage contract test green against a freshly-migrated test DB
- Fresh-install smoke test (DATABASE_URL_APP unset, POSTGRES_PASSWORD set,
  empty DB) — autoMigrate completes, derives app URL, probe passes, seed runs
- Negative smoke test (no password sources) — hard-fails with the
  actionable error message exactly as designed
- Bootstrap migration applied 2x against dev DB — fully idempotent,
  zero RLS gaps remain in pg_catalog audit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ToddHebebrand added a commit that referenced this pull request Apr 22, 2026
…500)

## Summary

Closes 13 of 14 findings from the 2026-04-20 security review of
`feature/device-org-move-wip`. 12 discrete tasks + 2 correction commits,
each independently revertible.

Full plan:
`docs/superpowers/plans/2026-04-20-security-review-remediations.md`.

## Findings addressed

| # | Sev | Area | Fix |
|---|---|---|---|
| #1 | HIGH | uninstall scripts | Publish SHA256SUMS + surface
hash/verify cmd in `AddDeviceModal` |
| #2 | HIGH | public-download token in URL | One-time Redis handle via
new `POST /enrollment-keys/:id/download-handle`; back-compat `?token=`
path retained with deprecation warning |
| #3 | HIGH | enrollment-key write routes unrated | New `userRateLimit`
middleware (10/60s per user) on all 5 write routes (create / rotate /
delete / installer-link / bootstrap-token) |
| #4 | MEDIUM | 8h viewer JWT | Dropped to 2h; added `jti` claim;
Redis-backed revocation + per-tunnel-session revoke-on-close |
| #5 / #6 | MEDIUM | tunnels.ts `:id` / `siteId` unvalidated |
`zValidator('param'/'query', …)` on 7 routes |
| #7 | MEDIUM | install.bat template | `assertValidEnrollmentKey`
(64-hex regex) + quoted `ENROLLMENT_KEY` |
| #8 | MEDIUM | viewer HTTP allowlist | `isLocalhost()` replaces
`isPrivateHost()` — 127.0.0.1 / ::1 / localhost only |
| #9 | MEDIUM | VNC server reason → UI | `KNOWN_VNC_REASONS` allowlist +
`friendlyReason()` fallback (incl. ARD form) |
| #10 | MEDIUM | siteId ownership on enroll create | Query `sites` WHERE
`sites.orgId = orgId` before insert |
| #12 | MEDIUM | short-link claim race | Atomic UPDATE folds expiry +
max-usage into WHERE; claim-before-insert |
| #13 | LOW | helper install templates | `Exec=%q` on Linux;
`xml.EscapeText` on Darwin plist |
| #14 | LOW | session config perms | 0644 → 0600 on 3 write sites |

Finding #11 (encrypted `enrollment.json` inside installer zip) and other
low-severity items were intentionally deferred — documented in the
plan's "Out of scope" section.

## Test plan

- [x] API: `pnpm --filter @breeze/api test:run` — 3247 passed, 25
skipped
- [x] Viewer: `npx vitest run` in `apps/viewer` — 114 passed
- [x] Web: `npx vitest run` in `apps/web` — 279 passed
- [x] Agent helper: `cd agent && go test -race ./internal/helper/...` —
ok
- [ ] Manual smoke: trigger a macOS installer download from staging and
confirm the agent enrolls (exercises finding #2 + #7 + #10 end-to-end).
- [ ] Manual smoke: open a WebRTC session, hit DELETE `/tunnels/:id`,
confirm the viewer sees a 401 on its next poll (exercises finding #4).
- [ ] Visual review: uninstall-script SHA256 block renders correctly
below each download link.

## Notes for the reviewer

- Two commits (`7db3b1fb`, `845e2483`, `6be03849`) have muddled
boundaries because several subagents staged changes concurrently in the
same worktree. The tree state is correct; only the commit-per-task
narrative got blurred. Squash-merge will render this moot.
- `enrollmentKeys.ts` grew to 1333 lines. It's cohesive but approaching
hard-to-navigate; candidate for a follow-up split of
`publicEnrollmentRoutes` + `publicShortLinkRoutes` + `serveInstaller`
into a `enrollmentKeys.public.ts`.
- `/:id/download-handle` uses `===` on SHA-256 digests rather than
`timingSafeEqual`. Low risk given both sides are digests + the route is
auth-gated + rate-limited, but the convention elsewhere
(`apiKeyAuth.ts`) uses `timingSafeEqual`. Trailing TODO worth picking
up.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ToddHebebrand added a commit that referenced this pull request May 29, 2026
) (#997)

Security-review batch 2. Each finding re-verified against source, fixed TDD, spec + quality reviewed, then run through a 5-agent PR review that caught and fixed a HIGH fail-open.

- #10 agent-auth skip carve-out → exact-match allowlist (closes nested-/approve bypass) + regression test
- #9 deleted dead optionalAuthMiddleware + eitherAuthMiddleware (fall-through bypass foot-gun) + docs fix
- #6 drift-proof secret-key strip predicate (catches backup_s3_*) + Load read-back; strip now fails CLOSED
- #8 secrets.yaml chmod-enforce failure fatal (agent.yaml stays 0644 Helper-readable)
- #5 DNS-provider sync via DNS-pinning safeFetch; on-prem RFC1918 opt-in gated on explicit IS_HOSTED self-host signal (fail-closed); fixed a Critical IPv4-mapped-IPv6 hex-form metadata bypass in the shared SSRF guard (hardens webhooks/SSO/S1)
- #7 access_reviews partner-axis rows → Shape-4 dual-axis RLS (DB-verified: contract test 17/17 + live cross-tenant forge rejected)
- #3 accurate loud ENABLE_2FA warning (warn-only by design: no boot-refusal, no gate change)
- #2 PAM actuate route gated OFF by default (PAM_ACTUATOR_ENABLED); Track-6 hardening deferred as design

Excludes #1 (handled separately). Plan: docs/superpowers/plans/2026-05-29-security-review-batch2-fixes.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)
ToddHebebrand added a commit that referenced this pull request May 31, 2026
…rd controls

Security review of the remote-desktop (WebRTC) pipeline found authorization
was established only at connect/offer time and never re-enforced against, nor
revocable from, a live session. Implements the 8 confirmed findings:

- #1 Re-check remote-access policy at both WebRTC offer endpoints (JWT /offer
  and the viewer-token path) so disabling the policy blocks (re)starts.
- #2 Send stop_desktop to the agent on session end/suspend and add agent-side
  idle + max-duration watchdog timers (enforces the previously-dead
  idleTimeoutMinutes / maxSessionDurationHours).
- #3 Terminate a user's live remote sessions on deactivate/suspend and
  partner-abuse-suspend (new remoteSessionTeardown service).
- #4 Recheck viewer-session revocation in the desktop-WS ping loop so a live
  legacy socket closes within one interval after revocation.
- #5 Stop resurrecting ended sessions: reject disconnected/failed in
  validateViewerSessionAccess, drop endedAt:null resurrection from both offer
  sinks, revoke viewer tokens on the passive disconnect paths.
- #6 Single-source the viewer-token TTL so the advertised expiry matches the
  real 2h signed TTL.
- #7 Add a per-direction clipboard policy capability (host->viewer defaults off
  on hosted via IS_HOSTED); the agent enforces it since the viewer is untrusted.
- #8 Audit clipboard and filedrop transfers (direction/size/filename).

Tests: viewerTokenTtl.test.ts, clipboard gate_test.go, desktop policy parse
test; updated existing mocks/test calls for the new signatures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ToddHebebrand added a commit that referenced this pull request May 31, 2026
…rd controls (#1020)

## Remote-desktop session security hardening

Implements the 8 confirmed findings from the remote-desktop (WebRTC)
pipeline security review. Root cause: authorization was established only
at connect/offer time and was never re-enforced against, nor revocable
from, a live session.

**Findings fixed**
- **#1** Policy re-checked at both WebRTC offer endpoints (JWT `/offer`
+ viewer-token path).
- **#2** Server sends `stop_desktop` on end/suspend; agent gains idle +
max-duration watchdog timers (enforces the previously-dead
`idleTimeoutMinutes`/`maxSessionDurationHours`).
- **#3** Live remote sessions terminated on user deactivate/suspend and
partner-abuse-suspend (new `remoteSessionTeardown` service).
- **#4** Viewer-revocation rechecked in the desktop-WS ping loop → live
legacy socket closes within one interval.
- **#5** No session resurrection: reject `disconnected`/`failed` in
`validateViewerSessionAccess`, drop `endedAt:null` resurrection from
both offer sinks, revoke on the passive disconnect paths.
- **#6** Single-sourced viewer-token TTL (advertised == real 2h).
- **#7** Per-direction clipboard policy capability; agent-enforced
(channel/Watch/Receive gated). Host→viewer defaults **off** on hosted
(`IS_HOSTED`).
- **#8** Clipboard + filedrop transfer audit (direction/size/filename).

**Tests added:** `viewerTokenTtl.test.ts`, `clipboard/gate_test.go`,
`handlers_desktop_policy_test.go`; updated existing mocks/test calls for
the new signatures.

**Local verification:** API `tsc --noEmit` = 0 errors; targeted vitest
118 passed + `remote.test.ts` 15/15; agent `go test`
clipboard/filedrop/heartbeat green (clipboard/heartbeat also under
`-race`).

**Pre-existing failures (not from this PR):** the full local `pnpm test`
shows 12 failures across 10 files in unrelated subsystems
(`oauthInteraction`, `partner_multi_org_orgid`/discovery-scan,
`encryptedColumnRegistry`, …) — disjoint from this diff (which only
touches remote-desktop), and all 5200 other tests pass. Relying on CI
for the authoritative full-suite gate.

**Decisions to confirm**
- Clipboard host→viewer defaults **off on hosted** / on for self-hosted
(preserve behavior); admin-overridable via policy.
- #5 means a viewer that drops (tab crash / network blip) must create a
**fresh** session to reconnect — flag for the web viewer UI.

**Follow-ups:** route #8 audit into the central tamper-evident
`audit_logs` (currently agent diagnostic logs); add a web UI toggle for
the clipboard capability.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ToddHebebrand added a commit that referenced this pull request Jun 12, 2026
…o-op

PR review (4 specialized agents) found the eventWs site-scope fix (#8) never
took effect: POST /events/ws-ticket runs behind authMiddleware only, but it
read c.get('permissions').allowedSiteIds — and `permissions` is populated
solely by requirePermission, which does not run on that route. So allowedSiteIds
was always undefined, the ticket carried no restriction, and the dispatch
filter was dead code. Source the restriction from auth.allowedSiteIds (set by
authMiddleware) instead.

Test hardening (the gap that hid the bug):
- eventWs: route-level test proving allowedSiteIds threads identity -> ticket
  (goes red if reverted to c.get('permissions')); unrestricted companion.
- eventDispatcher: cover the per-client filter predicate incl. fail-closed on
  throw (drop for that client, no crash, other clients unaffected).
- alerts/rules PUT + alertTemplates/rules PATCH: add missing deny+granted tests.
- patches/approvals: assert the gate uses devices:execute (not just "a perm");
  add a requireMfa-rejects test so a dropped requireMfa() is caught.
- sensitiveData: cover secure_delete destructive deny + the missing-device
  fail-closed branch (findingDevices.length mismatch -> 403, no queueCommand).
- tags: strengthen site-narrowing tests to serialize the WHERE and assert the
  siteId condition is/ isn't present (were passing trivially under the mock).

Comment cleanup: drop transient tracking tokens (FINDING #11, #1047 class) and
the point-in-time publisher stats in buildSiteFilter; keep the durable facts.

279 affected tests pass; tsc -p apps/api clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ToddHebebrand added a commit that referenced this pull request Jun 12, 2026
…uthz review (#1278)

## Summary

Two-pass multi-tenant security review (15 resource groups, **795
endpoints audited**) found **no cross-tenant read/write or
privilege-escalation hole** — org-axis RLS
(`breeze_has_org_access`/`breeze_has_partner_access`, enabled+forced)
holds everywhere, and forged cross-org ids are rejected at the DB. The
two historical RLS bug classes (nested-EXISTS bound-param on
`script_execution_batches`; dual-axis blindspot on
`custom_field_definitions`) are confirmed remediated.

This PR closes the **11 confirmed intra-tenant findings**. Each fix
mirrors an already-correct sibling and ships a regression test.

## Findings fixed

| # | Sev | File | Fix |
|---|-----|------|-----|
| 1 | **HIGH** | `routes/sensitiveData.ts` | Gate `POST /remediate`
destructive actions (encrypt/quarantine/secure_delete) by
`allowedSiteIds` before queuing agent commands |
| 2 | MED | `routes/tags.ts` | Narrow `GET /tags` + `/tags/devices` by
`allowedSiteIds` |
| 3 | MED | `routes/patches/approvals.ts` |
`requirePermission(devices:execute)` + `requireMfa` on
approve/decline/defer/bulk-approve |
| 4 | MED | `services/aiToolsTicketing.ts` | Site-gate `manage_tickets`
by-id actions via `deviceInSiteScope` |
| 5 | MED | `routes/softwareInventory.ts` | Audit approve/deny/clear
software-policy mutations |
| 6 | MED | `routes/alerts/*`, `alertTemplates/*` |
`ALERTS_WRITE`/`ALERTS_ACKNOWLEDGE` (+MFA) on
rule/policy/routing/template/state-change mutations |
| 7 | MED | `routes/tunnels.ts` | `devices:execute`+MFA on allowlist
mutations; validate `body.siteId` belongs to org |
| 8 | MED | `routes/eventWs.ts` | Carry `allowedSiteIds` onto ws ticket
+ per-client fail-closed dispatch filter |
| 9 | MED | `services/ticketService.ts` | Audit
assign/comment/alert_link/alert_unlink |
| 10 | MED | `routes/sensitiveData.ts` | Site-narrow `GET /dashboard`
aggregate |
| 11 | MED | `routes/cisHardening.ts` | Re-check device site scope in
`POST /remediate/approve` |

**Three classes:** site sub-axis (RLS does not model `site` —
app-layer-only, 6 fixes), RBAC tier-vs-permission gaps (3), missing
tamper-evident audit writes (2).

## ⚠️ Reviewer notes

1. **`eventWs` (#8) fails closed — behavior change.** No event publisher
currently emits `siteId` (all 56 publish sites checked). The fix
delivers only on a positive site match, so **site-restricted users
receive no live events** until publishers add `siteId`. Strictly safer
than the prior cross-site leak, but it degrades the live feed for those
users. Tracked as a follow-up (see linked issue); unrestricted users
unaffected. The one cross-file edit lives here: a generic, site-agnostic
`filter` predicate on `eventDispatcher.ts`.
2. **Tests use Drizzle mocks** — they assert the gate is invoked, not
real RLS/permission resolution. Recommend a live `breeze_app` repro for
the HIGH (#1) and the RBAC findings before release.

## Verification

- `tsc --noEmit -p apps/api` clean (only the two known pre-existing
`agents.test.ts`/`apiKeyAuth.test.ts` errors).
- Affected suites: **267 tests / 17 files** pass.
- Sibling regression suites: **94 tests / 10 files** pass.
- 24 files changed, disjoint ownership (one subagent per file set).

Full review write-up:
`docs/superpowers/specs/2026-06-12-security-review-tenant-authz-results.md`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ToddHebebrand added a commit that referenced this pull request Jun 14, 2026
…sers get in-site events (#1347)

Follow-up to #1278 (finding #8 of the 2026-06-12 multi-tenant security
review).

## Problem
#1278 added a fail-closed, per-client site filter to the events WS
(`buildSiteFilter` in `routes/eventWs.ts`): a site-restricted user only
receives an event we can positively attribute to one of their allowed
sites. But **no publisher emitted `siteId`**, so site-restricted users
received **no live events at all** — safe versus the prior leak, but a
degraded live feed.

## Fix
Put `siteId` on the wire at publish time so `buildSiteFilter` resumes
in-site delivery with **no further eventWs change**.

**Infrastructure**
- `siteId` is now a first-class **top-level** field on `BreezeEvent` and
a `PublishOptions.siteId` option (`services/eventBus.ts`). Empty-string
/ `null` normalise to "no attribution"; preserved across DLQ retry. The
WS filter already reads top-level `siteId` first.
- New `services/deviceSiteResolver.ts`: a shared, short-TTL-cached
`deviceId → siteId` resolver for publishers that hold only a `deviceId`.
Fails open to `undefined` (org-level) and never throws — a resolution
failure just means the event publishes org-level (fail-closed for
site-restricted users, no-op for unrestricted).

**Publishers wired with siteId**
- `alert.triggered` / `alert.resolved` (`alertService.ts`) — via
resolver; config-policy `alert.triggered` uses the device record already
loaded.
- `session.login` / `session.logout` (`agents/sessions.ts`).
- `device.online`, `device.offline` (`agentWs.ts` connect/disconnect) +
`device.offline` (`jobs/offlineDetector.ts`).
- `device.updated`, `device.main_agent_silent`,
`monitoring.check_failed` / `monitoring.check_recovered`
(`agents/heartbeat.ts`).

**Org-level events** (e.g. `user.login`) carry no `siteId` and remain
**fail-closed** for site-restricted users (withheld) — explicit and
tested. Unrestricted users are unaffected throughout.

## Tests
- `eventBus.test.ts` — top-level `siteId` emission, omission for
org-level events, empty-string/null normalisation.
- `deviceSiteResolver.test.ts` — cache hit/miss, not-found caching,
fail-open on DB error, no DB call for empty `deviceId`.
- `eventDispatcher.test.ts` — **end-to-end** through the real
`dispatch()` + the real `buildSiteFilter`: a site-restricted client
receives in-site events, drops out-of-site events, and is withheld
org-level events; an org admin receives all.

Run:
\`\`\`
pnpm --filter @breeze/api exec vitest run \
  src/services/eventBus.test.ts \
  src/services/deviceSiteResolver.test.ts \
  src/services/eventDispatcher.test.ts \
  src/routes/eventWs.test.ts \
  src/routes/agents/sessions.test.ts \
  src/routes/agents/heartbeat.test.ts \
  src/routes/agentWs.test.ts \
  src/jobs/offlineDetector.test.ts
\`\`\`
Result: all green (eventBus/dispatcher/resolver 35;
eventWs+sessions+offlineDetector 33; heartbeat+agentWs 56). `tsc
--noEmit` for `@breeze/api`: 0 errors.

## Phase-1 follow-up
This covers the marquee live-feed publishers. The remaining
device-scoped publishers (peripheral-control, CIS/compliance, incident,
sensitive-data command-result handlers) are a trivial follow-up now that
the `siteId` option and the resolver exist.

Closes #1280

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ToddHebebrand pushed a commit that referenced this pull request Jun 30, 2026
The /incidents/feed huntress + s1 union legs applied only the org filter,
letting a site-restricted user read every org Huntress/S1 finding (deviceId
+ hostnames-in-titles), bypassing their site allowlist (RLS can't see site).

- FIX 1 (CRITICAL): thread the caller's resolved site-allowed device ids into
  buildIncidentFeed and push the same null-device-OR-in-list predicate the
  dedicated EDR routes use (huntress.ts / sentinelOne.ts resolveSiteAllowedDeviceIds)
  onto the huntress + s1 legs. Tracked/breeze leg stays unfiltered.
- FIX 2: gate the EDR legs behind devices:read — a caller with alerts:read but
  not devices:read sees only native tracked incidents (legs omitted; a
  source=huntress filter then yields an empty feed, not an error).
- FIX 3: test that a second promote of the same (org,source_type,source_ref)
  returns 409 (not 500) via the 23505 .cause chain.
- FIX 4: drop the dead `source` param from resolveFindingLinkOut.
- FIX 5: match the constraint name in isPgUniqueViolation(err,
  'incidents_source_ref_unique') so unrelated 23505s still throw.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ToddHebebrand pushed a commit that referenced this pull request Jun 30, 2026
#8)

Seed a device in a disallowed site + a device-bound and a provider-level
(null-device) huntress finding; assert the device-bound finding is excluded
for a site-restricted caller (allowedDeviceIds = []) while the null-device
finding survives. DEFERRED when no integration Postgres (it.runIf).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ToddHebebrand added a commit that referenced this pull request Jun 30, 2026
## Summary

Makes the Incidents page **EDR-aware**: it now renders a unified feed
that unions live EDR detections (from connected endpoint-security
providers) with manually-tracked incident-response records, instead of
showing only tracked incidents.

- **New `GET /incidents/feed`** endpoint returns a severity-ranked union
of EDR findings + tracked incidents
- **Schema**: adds `source_type` / `source_ref` and `affected_users` to
incidents; create-path accepts and persists an EDR source link
- **EDR console link-out**: findings derive a deep link back to the
originating EDR console
- **Web**: Incidents page renders the unified feed with per-status
colors; removed the misleading client-side severity filter

## Security / tenant isolation

- EDR feed legs are gated on **site-RBAC + `devices:read`** — a user
without device read or outside the site scope cannot see EDR findings
(#8)
- `hasDevicesRead` + `allowedDeviceIds` are **required** on
`IncidentFeedParams` so the gate can't be bypassed by omission
- Integration coverage added for the site-RBAC exclusion path

## Migration

`2026-06-29-incidents-edr-source-link.sql` — idempotent add of
source-link columns.

## Tests

- API: `incidents.helpers.test.ts`, `incidents.test.ts`, plus
`incidents-feed.integration.test.ts` (real-DB RBAC exclusion)
- Web: `IncidentsPage.test.tsx`, `incidents.test.ts`

Spec: `docs/superpowers/specs/2026-06-29-edr-incidents-feed-design.md`
Plan: `docs/superpowers/plans/2026-06-29-edr-incidents-feed.md`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Todd Hebebrand <todd@lanternops.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ToddHebebrand added a commit that referenced this pull request Jul 12, 2026
Hardens the agent→API trust boundary against a **compromised enrolled
agent**, from a focused security review. 10 findings (5 HIGH, 5 MEDIUM)
were independently verified, then fixed and re-verified.

## Findings & fixes

**HIGH**
- **#1 renew-cert quarantine bypass** — the route read org mTLS policy
and wrote device state *after* its DB context closed, so under forced
RLS as the unprivileged role those reads/writes silently returned 0 rows
and a `quarantine` policy fell back to `auto_reissue`. Now reads policy
+ all device writes inside `withSystemDbAccessContext`, fails closed on
a missing org row or any 0-row write, and only returns cert material
after metadata persists (else revokes the issued cert).
- **#2 previous-token self-renewal** — a superseded token (5-min grace)
could mint durable new agent/watchdog/helper credentials. Now rejected
(401) on both rotate-token and renew-cert; rotate-token UPDATE is a
compare-and-swap bound to the authenticating token hash (409, mints
nothing, on a losing race).
- **#3 established WS bypass containment** — quarantine and tenant
suspension/deletion didn't sever a live agent socket. Now they do; plus
a lightweight per-message lifecycle recheck on the command-claim and
command-result paths.
- **#4 duplicate WS orphan** — a second socket replaced the tracked one
without closing the first, leaving an orphan revocation couldn't reach.
Now single-socket-per-agent (superseded socket closed on connect) with
connection-identity-guarded ping state.
- **#5 private-key redaction** — the agent regex missed PKCS#8 and left
the key body intact. Now removes whole PEM blocks (PKCS#8 +
RSA/EC/DSA/OPENSSH/ENCRYPTED), plus a server-side redaction layer at
result ingest for pre-update agents.

**MEDIUM**
- **#6** CSV formula-injection — 5 web exporters now neutralize
agent-controlled cells via the shared helper.
- **#7** watchdog diagnostics never reached the API (wrong wire shape /
status / false "completed"); now speaks the log contract and reports
failure honestly.
- **#8** WS command-result completions weren't audited (REST path was);
now emit `agent.command.result.submit` once per real transition.
- **#9** `logs.ts` ingest had no audit event; added a content-free one.
- **#10** heartbeat overwrote security-relevant state with no history;
now emits `agent.heartbeat.state_change` on genuine transitions only.

## PR-review follow-up (same branch)
A multi-agent review verified the auth cluster clean (no
bypass/RLS/silent-mint) and caught issues in the redaction layer, all
fixed here: a **ReDoS** in the new server-side redactor (bounded →
linear), an unredacted `error` field, truncated-key passthrough, and
redaction parity (ported the agent's
AWS-key/bearer/JWT/connection-string/secret-pair patterns server-side).

## Verification
- `apps/api` typecheck clean; **171** unit tests pass
- `apps/web` **15** tests pass
- Go build + `-race` (executor / watchdog / breeze-watchdog) pass

## Pre-merge follow-ups (not done here)
1. **Real-Postgres integration test for renew-cert (#1).** The fix's
premise — a contextless read returns 0 rows under forced RLS, a
system-context read returns the real row — is proven only via mocks.
Should be closed with one integration test against real Postgres per the
repo's RLS-contract discipline.
2. **Cross-replica socket severance.** `disconnectAgent` is in-process
only, so a live socket on another API replica isn't severed immediately
on quarantine/tenant-suspension (mitigated by the per-message recheck;
matches the existing decommission convention). Fine if prod is
single-API-replica per region — worth confirming.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Todd Hebebrand <todd@lanternops.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant