diff --git a/.planning/STATE.md b/.planning/STATE.md index 4e951f1..1ad7600 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-18 — Completed quick task 260717-v6c: Mark as accidental report action +Last activity: 2026-07-18 — Completed quick task 260718-7v8: Mimecast blast-radius held-message false-positive fix ## Performance Metrics @@ -128,6 +128,7 @@ None yet. | 260716-pgr | Fix confidence display bug (0-1 scale rendered as raw percent, e.g. "1%" instead of "100%") | 2026-07-16 | 3f16268 | [260716-pgr-fix-confidence-display-bug-in-classifica](./quick/260716-pgr-fix-confidence-display-bug-in-classifica/) | | 260717-a19 | Fix phishing simulation-vendor allowlist gaps (3 missing KnowBe4 domains), auto-parse timing race (retry on ticket.update), and parseAndStoreMessage idempotency; reclassified 6 stale Seubert campaigns (all flipped UNWANTED → USER_AWARENESS) | 2026-07-17 | cf04f07 | [260717-a19-fix-phishing-simulation-vendor-allowlist](./quick/260717-a19-fix-phishing-simulation-vendor-allowlist/) | | 260717-v6c | Add "Mark as accidental report" action to the phishing Action Area — closes out a campaign and posts a fixed customer-facing note to the reporter (distinct from the silent "Mark as false positive" action) | 2026-07-18 | 565a0c1 | [260717-v6c-add-a-mark-as-accidental-report-action-t](./quick/260717-v6c-add-a-mark-as-accidental-report-action-t/) | +| 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/) | ## Deferred Items diff --git a/.planning/quick/260718-7v8-fix-mimecast-blast-radius-held-message-f/260718-7v8-SUMMARY.md b/.planning/quick/260718-7v8-fix-mimecast-blast-radius-held-message-f/260718-7v8-SUMMARY.md new file mode 100644 index 0000000..90dc106 --- /dev/null +++ b/.planning/quick/260718-7v8-fix-mimecast-blast-radius-held-message-f/260718-7v8-SUMMARY.md @@ -0,0 +1,110 @@ +--- +phase: quick-260718-7v8 +plan: 01 +subsystem: api +tags: [mimecast, blast-radius, phishing, false-positive-fix] + +# Dependency graph +requires: + - phase: 17-mimecast-blast-radius-lookup + provides: getBlastRadius() fan-out orchestration and MimecastClient +provides: + - Date-scoped getHeldMessages() (optional start/end threaded into request body) + - Sender-domain relevance guard on held rows before counting/merging +affects: [17-mimecast-blast-radius-lookup, 22-approval-ui-livelink-addressable-campaign-review-and-approve] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "Sender-relevance guard: filter external-API rows by domain match against campaign input.sender BEFORE they feed counts or a per-recipient merge — mirrors campaign-classifier's exact-or-proper-subdomain comparison without reusing its allowlist-specific function." + +key-files: + created: [] + modified: + - lib/services/mimecast-client.ts + - lib/services/mimecast-blast-radius.ts + - lib/services/mimecast-blast-radius.test.ts + - lib/services/mimecast-client.test.ts + +key-decisions: + - "getHeldMessages() start/end are pre-formatted strings supplied by the caller (mirrors searchDeliveredMessages), never formatted inside the client method." + - "domainsMatch() is a new local helper in mimecast-blast-radius.ts, not a reuse of campaign-classifier's domainMatchesAllowlist (that helper is allowlist-specific)." + - "Held-row counts (held/matched) and the perRecipient merge both use the same relevantHeldRows array so a filtered-out row can never leak into either output." + +requirements-completed: [BUG-BLAST-RADIUS-HELD] + +# Metrics +duration: 15min +completed: 2026-07-18 +--- + +# Quick Task 260718-7v8: Fix Mimecast Blast-Radius Held-Message False Positives Summary + +**Date-scoped `getHeldMessages()` plus a sender-domain relevance guard eliminate unrelated held-message false positives in the Blast Radius panel.** + +## Performance + +- **Duration:** ~15 min +- **Started:** 2026-07-18T09:35:00Z (approx.) +- **Completed:** 2026-07-18T09:46:50Z +- **Tasks:** 2 +- **Files modified:** 4 + +## Accomplishments +- `MimecastClient.getHeldMessages()` now accepts optional `start`/`end` and threads them into `data[0]` as siblings of `admin`/`searchBy` (backward compatible when omitted; the existing 403 fallback body inherits them automatically via its `{ ...reqBody }` spread). +- `getBlastRadius()` passes the same `startStr`/`endStr` window already computed for `searchDeliveredMessages` into `getHeldMessages()`, so the held-queue lookup is scoped to the reported campaign's window instead of the recipient's entire hold queue. +- Added a `domainsMatch()` sender-relevance guard in `mimecast-blast-radius.ts`: held rows whose sender domain does not match `input.sender`'s domain (exact-or-proper-subdomain, case-insensitive) are filtered out of `relevantHeldRows` before they can contribute to `held`/`matched` counts or the `perRecipient` merge. +- Extended test coverage in both files: date-window threading assertion, unrelated-sender-domain exclusion test, matching-sender-domain override-still-works test, and client-level POST-body-shape tests for `start`/`end` presence/absence. + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Date-scope getHeldMessages and add sender-relevance guard in getBlastRadius** - `94f7dad` (fix) +2. **Task 2: Extend test coverage for date-scoping and sender-relevance filtering** - `67ee680` (test) + +**Plan metadata:** committed separately by the orchestrator after this summary. + +## Files Created/Modified +- `lib/services/mimecast-client.ts` - `getHeldMessages()` gains optional `start`/`end` params, threaded into the request body's `data[0]` alongside `admin`/`searchBy`. +- `lib/services/mimecast-blast-radius.ts` - `getBlastRadius()` passes the shared date window into `getHeldMessages()`; new `domainsMatch()` helper filters held rows by sender-domain relevance before they feed `held`/`matched`/`perRecipient`. +- `lib/services/mimecast-blast-radius.test.ts` - new tests: shared-window assertion, unrelated-sender exclusion, matching-sender override still works. +- `lib/services/mimecast-client.test.ts` - new `describe('getHeldMessages date-scoping')` block: asserts `start`/`end` presence/absence in the POST body via a `request` spy. + +## Decisions Made +- Followed the plan's interface precedent exactly: caller pre-formats `start`/`end` strings (same pattern as `searchDeliveredMessages`); the client method does no date formatting itself. +- `domainsMatch()` intentionally duplicates (rather than imports) the exact-or-proper-subdomain comparison logic from `campaign-classifier.ts`'s `domainMatchesAllowlist`, per the plan's explicit instruction not to reuse that allowlist-specific function. +- Kept the existing "merges delivered/held/threat-event fixtures" test's held row using `from: BASE_INPUT.sender` (a domain match) so it continues to pass unchanged — confirmed via the test run (25/25 passing). + +## Deviations from Plan + +None — plan executed exactly as written. No architectural changes, no scope additions beyond the two locked fixes and their specified test coverage. + +## Issues Encountered + +Two TypeScript errors surfaced in the new client test (`'body' is of type 'unknown'` from destructuring `vi.spyOn(...).mock.calls[0]` directly) — fixed inline by typing the extracted body as `any` off the correct tuple index (`requestSpy.mock.calls[0][2]`) before asserting on `body.data[0]`. This is an in-scope Rule 1 fix (blocking type error in code written as part of this same task), not a deviation from the plan's intent. + +## Known Stubs + +None. + +## Threat Flags + +None — no new network endpoints, auth paths, or trust-boundary changes. This plan only tightens an existing data-filtering boundary (held-message relevance) inside an already-reviewed lookup module. + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness + +The Blast Radius panel (Phase 17/22 consumers) now returns a correctly date-scoped and sender-relevance-filtered held count for any campaign lookup. No follow-up work is required; this was a standalone bug fix confirmed against two live verification scripts referenced in the plan (date-scoping alone: 15→1 false held rows; sender-relevance guard: 1→0). + +--- +*Quick task: 260718-7v8* +*Completed: 2026-07-18* + +## Self-Check: PASSED + +All modified/created files confirmed present on disk; both task commits (94f7dad, 67ee680) confirmed present in git log.