From 9f75f2160c0e5f846c426d6c1f5574dedc41481a Mon Sep 17 00:00:00 2001 From: lorentz Date: Tue, 21 Jul 2026 16:44:44 -0400 Subject: [PATCH] fix(260721-n49): resolve per-company Mimecast tenant in gatherCampaignEvidence - Thread company_id through reports query and CampaignReportSummary - Mirror the route's Bug 2 (D-05) tenant-resolution block: query mimecast_tenants for an enabled row, build a tenant-scoped client via getMimecastClientForTenant, and pass { client, cacheScope } to getBlastRadius() when one exists - Preserve global env fallback unchanged when no companyId or no enabled tenant row is present --- lib/services/campaign-classifier.ts | 61 ++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/lib/services/campaign-classifier.ts b/lib/services/campaign-classifier.ts index 45c0b7b..3e3e5bb 100644 --- a/lib/services/campaign-classifier.ts +++ b/lib/services/campaign-classifier.ts @@ -17,6 +17,7 @@ import type { AuthResults } from './eml-parser'; import { postgresClient } from './postgres-client'; import { getBlastRadius, type BlastRadiusResult } from './mimecast-blast-radius'; +import { getMimecastClientForTenant } from './mimecast-client'; // ============================================================================= // D-06/D-07: KnowBe4 / Breach Secure Now simulation sender-domain allowlist @@ -213,6 +214,14 @@ interface ReportDbRow { title: string | null; created_at: string; requester_email: string | null; + company_id: string | null; +} + +/** Mirrors app/api/phishing/campaigns/[id]/route.ts's MimecastTenantRow shape verbatim. */ +interface MimecastTenantRow { + client_id: string; + client_secret: string; + base_url: string | null; } interface MessageDbRow { @@ -256,6 +265,7 @@ export interface CampaignReportSummary { title: string | null; createdAt: string; requesterEmail: string | null; + companyId: string | null; } export interface CampaignEvidence { @@ -283,7 +293,7 @@ export async function gatherCampaignEvidence(campaignId: string): Promise( `SELECT r.id::text AS id, r.title, r.created_at::text AS created_at, - c.email_address AS requester_email + c.email_address AS requester_email, r.company_id::text AS company_id FROM reports r LEFT JOIN contacts c ON c.id = r.requester_contact_id WHERE r.campaign_id = $1 @@ -295,6 +305,7 @@ export async function gatherCampaignEvidence(campaignId: string): Promise r.id); @@ -347,15 +358,47 @@ export async function gatherCampaignEvidence(campaignId: string): Promise i.messageId === primaryMessage?.id && i.indicatorType === 'sender' ); const createdAt = new Date(primaryReport.createdAt); - blastRadius = await getBlastRadius({ - sender: senderIndicator?.value ?? primaryMessage?.from.email ?? '', - recipient: primaryReport.requesterEmail ?? '', - subject: primaryMessage?.subject ?? primaryReport.title ?? '', - dateWindow: { - start: new Date(createdAt.getTime() - 24 * 60 * 60 * 1000), - end: new Date(createdAt.getTime() + 24 * 60 * 60 * 1000), + + // Bug 2 (D-05): resolve the reporting company's own registered Mimecast + // tenant, if one exists, and query it directly instead of the global + // env-configured (Wulf) tenant. Falls back to the global client when the + // company has no enabled mimecast_tenants row. Mirrors + // app/api/phishing/campaigns/[id]/route.ts's identical tenant-resolution + // block verbatim — same query, same client build, same options shape. + let tenantOptions: { client: ReturnType; cacheScope: string } | undefined; + if (primaryReport.companyId) { + const tenantRes = await postgresClient.query( + `SELECT client_id, client_secret, base_url + FROM mimecast_tenants + WHERE company_id = $1 AND enabled = true + ORDER BY id LIMIT 1`, + [primaryReport.companyId] + ); + const tenantRow = tenantRes.rows[0]; + if (tenantRow) { + tenantOptions = { + client: getMimecastClientForTenant({ + client_id: tenantRow.client_id, + client_secret: tenantRow.client_secret, + base_url: tenantRow.base_url ?? undefined, + }), + cacheScope: primaryReport.companyId, + }; + } + } + + blastRadius = await getBlastRadius( + { + sender: senderIndicator?.value ?? primaryMessage?.from.email ?? '', + recipient: primaryReport.requesterEmail ?? '', + subject: primaryMessage?.subject ?? primaryReport.title ?? '', + dateWindow: { + start: new Date(createdAt.getTime() - 24 * 60 * 60 * 1000), + end: new Date(createdAt.getTime() + 24 * 60 * 60 * 1000), + }, }, - }); + tenantOptions + ); } else { // No report ever linked to this campaign — nothing to look up (research A6). blastRadius = { status: 'unavailable', reason: 'not_configured' };