diff --git a/.planning/STATE.md b/.planning/STATE.md index 6d7d0b3..3752204 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-mmf: fix Mimecast blast-radius query scope (tenant-wide fan-out instead of single-recipient) +Last activity: 2026-07-21 — Completed quick task 260721-n49: fix classifier to resolve per-company Mimecast tenant instead of always using the global tenant ## Performance Metrics @@ -132,6 +132,7 @@ None yet. | 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/) | +| 260721-n49 | Fix `gatherCampaignEvidence()` (used by auto-classification on ticket creation) to resolve the reporting company's own `mimecast_tenants` row before calling `getBlastRadius()`, mirroring the campaign-detail route's existing per-tenant resolution — previously it always used the global env-configured (Wulf) tenant, silently returning wrong-tenant (often empty) blast-radius data for any company with its own registered Mimecast tenant | 2026-07-21 | 9f12cd6 | [260721-n49-fix-classifier-mimecast-tenant-scope](./quick/260721-n49-fix-classifier-mimecast-tenant-scope/) | ## Deferred Items diff --git a/.planning/quick/260721-n49-fix-classifier-mimecast-tenant-scope/260721-n49-SUMMARY.md b/.planning/quick/260721-n49-fix-classifier-mimecast-tenant-scope/260721-n49-SUMMARY.md new file mode 100644 index 0000000..f157703 --- /dev/null +++ b/.planning/quick/260721-n49-fix-classifier-mimecast-tenant-scope/260721-n49-SUMMARY.md @@ -0,0 +1,102 @@ +--- +phase: quick-260721-n49 +plan: 01 +subsystem: phishing-triage +tags: [mimecast, classifier, multi-tenant, campaign-classifier, blast-radius] + +requires: + - phase: 260721-mmf + provides: "getBlastRadius() tenant-wide fan-out fix (query-scope, unrelated to this tenant-resolution fix)" +provides: + - "gatherCampaignEvidence() resolves the reporting company's own Mimecast tenant before calling getBlastRadius(), mirroring the already-shipped route-level fix" +affects: [phishing-triage, classifications, mimecast] + +tech-stack: + added: [] + patterns: + - "Per-company Mimecast tenant resolution: SELECT client_id/client_secret/base_url FROM mimecast_tenants WHERE company_id = $1 AND enabled = true ORDER BY id LIMIT 1, then getMimecastClientForTenant() + { client, cacheScope: companyId } passed as getBlastRadius()'s second argument. Falls back to no second argument (global env client) when absent." + +key-files: + created: [] + modified: + - lib/services/campaign-classifier.ts + - lib/services/campaign-classifier.test.ts + +key-decisions: + - "Mirrored app/api/phishing/campaigns/[id]/route.ts's 'Bug 2 (D-05)' tenant-resolution block inline in the classifier rather than extracting a shared helper — the plan's judgment note allowed extraction only if it was a clean, behavior-preserving drop-in for both call sites; the route's block was already correct/tested, so leaving it untouched and duplicating the small inline block in the classifier was the safer choice." + +requirements-completed: [FIX-CLASSIFIER-TENANT] + +duration: 12min +completed: 2026-07-21 +--- + +# Quick Task 260721-n49: Fix classifier Mimecast tenant scope Summary + +**Auto-classification (`classifyCampaign`/`gatherCampaignEvidence`) now queries the reporting company's own registered Mimecast tenant instead of always falling back to the global env-configured (Wulf) tenant.** + +## Performance + +- **Duration:** 12 min +- **Tasks:** 2 completed +- **Files modified:** 2 + +## Accomplishments + +- `gatherCampaignEvidence()` now threads `company_id` from the `reports` table through `CampaignReportSummary.companyId`. +- Before calling `getBlastRadius()`, the classifier queries `mimecast_tenants` for an enabled row matching the primary report's `companyId` and, if found, builds a tenant-scoped client via `getMimecastClientForTenant()` and passes `{ client, cacheScope: companyId }` as the second argument — exactly mirroring the already-shipped, already-tested block in `app/api/phishing/campaigns/[id]/route.ts` ("Bug 2 (D-05)"). +- When no `companyId` is present, or no enabled tenant row exists for it, `getBlastRadius()` is still called with no second argument — the global env-configured Mimecast client fallback is unchanged from before this fix. +- Root cause was verified live against ticket 700716 / company 29683407 ("Seubert and Associates"): before this fix, that company's auto-classification was being computed against the wrong (Wulf) tenant's Mimecast data, producing a silent false-"clean" signal. + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Resolve per-company Mimecast tenant in gatherCampaignEvidence()** - `9f75f21` (fix) +2. **Task 2: Extend campaign-classifier tests for tenant resolution vs. global fallback** - `9f12cd6` (test) + +_Note: `git log --oneline -3` from HEAD: `9f12cd6` (test), `9f75f21` (fix), `f58856e` (docs: pre-dispatch plan)._ + +## Files Created/Modified + +- `lib/services/campaign-classifier.ts` - Added `MimecastTenantRow` interface, `company_id` on `ReportDbRow`/`CampaignReportSummary`, and the per-company tenant-resolution block (query + `getMimecastClientForTenant` + `{ client, cacheScope }`) before the existing `getBlastRadius()` call in `gatherCampaignEvidence()`. +- `lib/services/campaign-classifier.test.ts` - Added a `mimecast_tenants` routing branch to `stageQueries`/`StagedRows`, `company_id` on `ReportFixtureRow` (and all existing report fixtures, set to `null` where not under test), plus two new test cases: one asserting tenant-scoped `{ client, cacheScope }` when an enabled tenant row exists, and a parameterized test asserting no second argument (`undefined`) when `companyId` is null or has no enabled tenant row. + +## Decisions Made + +- Kept the tenant-resolution block inline in the classifier (not extracted into a shared helper) per the plan's explicit judgment note — the route's existing block is already correct and tested, and extracting a shared helper would have required editing it, which the plan disallowed unless the extraction was a clean behavior-preserving drop-in for both call sites. + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered + +None. + +## User Setup Required + +None - no external service configuration required. + +## Verification + +- `npx vitest run lib/services/campaign-classifier.test.ts` — 49/49 tests passed (including the 2 new tenant-resolution/global-fallback tests; all 47 pre-existing tests remained green). +- `npx tsc --noEmit --pretty` — clean, no errors. +- Manual re-read confirmed the classifier's new tenant-resolution block is a line-for-line behavior mirror of `app/api/phishing/campaigns/[id]/route.ts`'s "Bug 2 (D-05)" block (same query, same `getMimecastClientForTenant` build, same `{ client, cacheScope }` shape); the route file itself was not modified. + +## Next Phase Readiness + +- Fix is self-contained to the evidence-gathering path of the campaign classifier; no follow-up work required. +- Companies with their own `mimecast_tenants` row (e.g. Seubert and Associates / 29683407) will now get correct tenant-scoped blast-radius evidence on the next auto-classification run (webhook-triggered ticket creation, or any future manual `classifyCampaign()` invocation) — no backfill of historical `classifications` rows was in scope for this fix. + +--- +*Phase: quick-260721-n49* +*Completed: 2026-07-21* + +## Self-Check: PASSED + +- FOUND: lib/services/campaign-classifier.ts +- FOUND: lib/services/campaign-classifier.test.ts +- FOUND: .planning/quick/260721-n49-fix-classifier-mimecast-tenant-scope/260721-n49-SUMMARY.md +- FOUND commit: 9f75f21 (fix task) +- FOUND commit: 9f12cd6 (test task)