wulf-pulse/.planning/quick/260721-n49-fix-classifier-mimecast-tenant-scope/260721-n49-SUMMARY.md

6 KiB

phase plan subsystem tags requires provides affects tech-stack key-files key-decisions requirements-completed duration completed
quick-260721-n49 01 phishing-triage
mimecast
classifier
multi-tenant
campaign-classifier
blast-radius
phase provides
260721-mmf getBlastRadius() tenant-wide fan-out fix (query-scope, unrelated to this tenant-resolution fix)
gatherCampaignEvidence() resolves the reporting company's own Mimecast tenant before calling getBlastRadius(), mirroring the already-shipped route-level fix
phishing-triage
classifications
mimecast
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.
created modified
lib/services/campaign-classifier.ts
lib/services/campaign-classifier.test.ts
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.
FIX-CLASSIFIER-TENANT
12min 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)