chore(deps): bump golang.org/x/net from 0.29.0 to 0.49.0 in /apps/agent - #8
Conversation
LabelsThe following labels could not be found: Please fix the above issues or remove invalid values from |
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>
84459ca to
182dc4f
Compare
|
@dependabot rebase |
|
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. |
|
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). |
|
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 If you change your mind, just re-open this PR and I'll resolve any conflicts on it. |
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>
…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>
) (#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)
…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>
…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>
…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>
…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>
…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>
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>
#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>
## 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>
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>
Bumps golang.org/x/net from 0.29.0 to 0.49.0.
Commits
d977772go.mod: update golang.org/x dependencieseea413einternal/http3: use go1.25 synctest.Test instead of go1.24 synctest.Run9ace223websocket: add missing call to resp.Body.Close7d3dbb0http2: buffer the most recently received PRIORITY_UPDATE frame35e1306go.mod: update golang.org/x dependencies7c36036http2, webdav, websocket: fix %q verb uses with wrong typeec11ecctrace: fix data race in RenderEventsbff14c5http2: don't PING a responsive server when resetting a stream88a6421dns/dnsmessage: avoid use of "strings" and "math" in dns/dnsmessage123d099http2: support net/http.Transport.NewClientConnDependabot 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 rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot mergewill merge this PR after your CI passes on it@dependabot squash and mergewill squash and merge this PR after your CI passes on it@dependabot cancel mergewill cancel a previously requested merge and block automerging@dependabot reopenwill reopen this PR if it is closed@dependabot closewill close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill 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 versionwill 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 dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)