diff --git a/.planning/STATE.md b/.planning/STATE.md index 14c7a88..6d7d0b3 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -28,7 +28,7 @@ See: .planning/PROJECT.md (updated 2026-07-14) Phase: Milestone v3.0 complete Plan: — Status: Awaiting next milestone -Last activity: 2026-07-21 — Completed quick task 260721-fy8: fix mimecast and qbo sync scheduler dispatch and reschedule mimecast cron +Last activity: 2026-07-21 — Completed quick task 260721-mmf: fix Mimecast blast-radius query scope (tenant-wide fan-out instead of single-recipient) ## Performance Metrics @@ -131,6 +131,7 @@ None yet. | 260718-7v8 | Fix Mimecast blast-radius false positives — date-scope `getHeldMessages()` and add a sender-domain relevance guard so unrelated held mail in a recipient's queue no longer inflates held/matched counts or overwrites a genuinely delivered recipient's status | 2026-07-18 | b7d6be4 | [260718-7v8-fix-mimecast-blast-radius-held-message-f](./quick/260718-7v8-fix-mimecast-blast-radius-held-message-f/) | | 260718-9qg | Add self-contained `QBO_INTEGRATION_HANDOFF.md` documenting Pulse's QuickBooks Online OAuth2 flow, token storage/refresh, sandbox/production API base URLs, and gotchas (deletion-diffing, CSRF state gap, NEXTAUTH_URL legacy var) for a new app's team | 2026-07-18 | ea8a36b | [260718-9qg-create-a-quickbooks-online-integration-h](./quick/260718-9qg-create-a-quickbooks-online-integration-h/) | | 260721-fy8 | Fix missing `mimecast-sync`/`qbo` scheduler dispatch branches (both silently fell through to a generic Autotask full sync) and reschedule `mimecast-sync` off the 2am 3-way cron collision with `qbo-sync-2am` and `veeam-full` | 2026-07-21 | db7db98 | [260721-fy8-fix-mimecast-and-qbo-sync-scheduler-disp](./quick/260721-fy8-fix-mimecast-and-qbo-sync-scheduler-disp/) | +| 260721-mmf | Fix Mimecast blast-radius query scope — dropped the single-recipient `to`/`recipient` filter from `searchDeliveredMessages`/`getHeldMessages` so the fan-out returns every delivered/held message across the whole tenant for a campaign's sender+subject+date-window, not just whether it reached the original reporter's mailbox | 2026-07-21 | 534eda3 | [260721-mmf-fix-mimecast-blast-radius-scope](./quick/260721-mmf-fix-mimecast-blast-radius-scope/) | ## Deferred Items diff --git a/.planning/quick/260721-mmf-fix-mimecast-blast-radius-scope/260721-mmf-SUMMARY.md b/.planning/quick/260721-mmf-fix-mimecast-blast-radius-scope/260721-mmf-SUMMARY.md new file mode 100644 index 0000000..6414351 --- /dev/null +++ b/.planning/quick/260721-mmf-fix-mimecast-blast-radius-scope/260721-mmf-SUMMARY.md @@ -0,0 +1,52 @@ +--- +phase: quick-260721-mmf +plan: 01 +subsystem: mimecast-blast-radius +tags: [mimecast, phishing, blast-radius, bugfix] +key-decisions: + - Dropped `to`/`recipient` from both fan-out calls entirely rather than making them optional-but-still-passed — Mimecast's client already treats both params as optional, so omitting them is the minimal change that broadens scope without touching BlastRadiusInput or the client contract + - Kept domainsMatch(h.from, input.sender) as the sole scoping mechanism for held rows (T-17-01) since getHeldMessages has no server-side sender filter — unchanged from the prior fix in 260718-7v8 + - recipient stays a required BlastRadiusInput field and is still forced into perRecipientMap as 'unknown' when no row matches it, preserving the reporter-always-represented guarantee +status: complete +--- + +# Quick Task 260721-mmf: Fix Mimecast blast-radius query scope + +**One-liner:** Removed the accidental single-recipient scoping (`to`/`recipient` params) from both Mimecast fan-out calls in `getBlastRadius()`, so blast-radius counts and `perRecipient` now reflect every recipient of a reported phishing campaign across the tenant, not just the original reporter's mailbox. + +## What was done + +### Task 1 — Broaden the fan-out query (`lib/services/mimecast-blast-radius.ts`, commit `58202e0`) +- Removed `to: input.recipient` from the `client.searchDeliveredMessages({...})` call — now passes only `from: input.sender`, `subject: input.subject`, `start: startStr`, `end: endStr`. +- Removed `recipient: input.recipient` from the `client.getHeldMessages({...})` call — now passes only `start: startStr`, `end: endStr`. +- Left the `deliveredResult.error` guard, the `relevantHeldRows` domainsMatch post-filter, the per-recipient merge `Map`, and the `perRecipientMap.has(input.recipient)` reporter-fallback all unchanged — the Map already keyed on `row.to`, so no merge-logic change was needed for multi-recipient correctness. +- Updated the inline comments above the fan-out `Promise.all` and above the per-recipient merge block to document that the query is now tenant-wide, scoped only by sender+subject+date-window, and that the Map naturally produces one entry per distinct recipient. +- `BlastRadiusInput` was not touched — `recipient` remains required (T-17-01) and is still used to label the reporter in the merge. + +### Task 2 — Update tests for the broadened fan-out (`lib/services/mimecast-blast-radius.test.ts`, commit `534eda3`) +- Updated the "calls getHeldMessages with the SAME start/end window..." test: `getHeldMessages` assertion changed from `{ recipient, start, end }` to an exact `{ start, end }` object (no `recipient` key); added a new assertion that the `searchDeliveredMessages` call argument does not have a `to` property. +- Added a new test, "returns every distinct recipient from a multi-recipient delivered result (true blast radius)": mocks `searchDeliveredMessages` with three delivered rows sharing the same sender/subject but three distinct `to` addresses (reporter, coworker-a, coworker-b), and asserts `delivered === 3`, `matched === 3`, and all three recipients appear in `perRecipient` with status `'delivered'`. +- All other existing tests (unavailable, cache hit, injected tenant client, Bug 1 defense-in-depth, unrelated-sender held filter, same-domain held override) left unchanged and still pass. + +## Verification + +- `npx vitest run lib/services/mimecast-blast-radius.test.ts` — 13/13 tests passed. +- `npx tsc --noEmit --pretty` — clean, no errors anywhere in the repo. +- `grep -n "to: input.recipient\|recipient: input.recipient" lib/services/mimecast-blast-radius.ts` — no matches (both scoping params fully removed from the API calls). + +## Deviations from plan + +None. Both tasks executed exactly as planned. + +## Environment note (execution anomaly, not a code deviation) + +This quick task's executor session was dispatched expecting an isolated git worktree (`/opt/stacks/pulse/.claude/worktrees/agent-a136e97a6eadf5305`), matching the standard `worktree_branch_check` protocol. During setup it was discovered that no such worktree actually existed on disk or in git's internal worktree registry (`git worktree list`) — the executor was, in practice, operating directly against the main checkout at `/opt/stacks/pulse` on `master`. The coordinator confirmed this was the intended non-worktree-isolation fallback path for this quick task (equivalent to `workflow.use_worktrees=false`) and explicitly approved committing directly to `master`, consistent with the plain sequential-commit pattern already used for quick task `260721-fy8` immediately prior in this same repo. No work was lost: the one `git reset --hard` run during verification was a no-op (master was already at the expected pre-dispatch-plan commit `f50b8a3a`, confirmed via `git reflog`), and the working tree was clean throughout. Both task commits (`58202e0`, `534eda3`) landed directly on `master` as a result. + +## Self-Check + +- FOUND: lib/services/mimecast-blast-radius.ts (modified, commit 58202e0) +- FOUND: lib/services/mimecast-blast-radius.test.ts (modified, commit 534eda3) +- FOUND commit 58202e0 in git log +- FOUND commit 534eda3 in git log + +## Self-Check: PASSED