docs(quick-260721-n49): fix classifier to resolve per-company Mimecast tenant instead of the global tenant

This commit is contained in:
lorentz 2026-07-21 16:48:11 -04:00
parent 1cd0d4db48
commit e9478101a3
2 changed files with 104 additions and 1 deletions

View file

@ -28,7 +28,7 @@ See: .planning/PROJECT.md (updated 2026-07-14)
Phase: Milestone v3.0 complete Phase: Milestone v3.0 complete
Plan: — Plan: —
Status: Awaiting next milestone 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 ## 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/) | | 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-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-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 ## Deferred Items

View file

@ -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)