From 9951e53832f570da9c0912359326fe5faf188bb3 Mon Sep 17 00:00:00 2001 From: lorentz Date: Thu, 16 Jul 2026 16:47:19 -0400 Subject: [PATCH] fix(260716-n46): clamp blast-radius date window and resolve per-company Mimecast tenant Bug 1: clamp dateWindow.end to Math.min(createdAt + 24h, Date.now()) so a freshly-detected campaign (<24h old primary report) never sends Mimecast a future end-date -- previously rejected as err_track_and_trace_invalid_end_date and swallowed internally as a false-clean zero-count result. Bug 2 (D-05): add company_id to the reports SELECT and, when the reporting company has its own enabled mimecast_tenants row, resolve a tenant-scoped client via getMimecastClientForTenant() and thread it into getBlastRadius as { client, cacheScope: companyId }. Falls back to the global env-configured client when no company-specific tenant is registered. --- app/api/phishing/campaigns/[id]/route.ts | 64 ++++++++++++++++++++---- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/app/api/phishing/campaigns/[id]/route.ts b/app/api/phishing/campaigns/[id]/route.ts index 1fb4b9c..7728240 100644 --- a/app/api/phishing/campaigns/[id]/route.ts +++ b/app/api/phishing/campaigns/[id]/route.ts @@ -10,6 +10,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { requirePermission } from '@/lib/auth-utils'; import postgresClient from '@/lib/services/postgres-client'; import { getBlastRadius, type BlastRadiusResult } from '@/lib/services/mimecast-blast-radius'; +import { getMimecastClientForTenant } from '@/lib/services/mimecast-client'; import { mergeTimeline } from '@/lib/services/phishing-timeline'; const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; @@ -30,6 +31,7 @@ interface ReportRow { id: string; ticket_id: string; ticket_number: string | null; + company_id: string | null; company_name: string | null; title: string | null; created_at: string; @@ -83,6 +85,12 @@ interface AuditEventRow { created_at: string; } +interface MimecastTenantRow { + client_id: string; + client_secret: string; + base_url: string | null; +} + export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } @@ -112,8 +120,8 @@ export async function GET( // Bulk-fetch linked reports (+ join contacts for requester email — campaigns // has no recipients column, must be derived via this join). const reportsRes = await postgresClient.query( - `SELECT r.id::text, r.ticket_id::text, r.ticket_number, r.company_name, - r.title, r.created_at::text, + `SELECT r.id::text, r.ticket_id::text, r.ticket_number, r.company_id::text, + r.company_name, r.title, r.created_at::text, c.email_address AS requester_email FROM reports r LEFT JOIN contacts c ON c.id = r.requester_contact_id @@ -173,6 +181,7 @@ export async function GET( id: r.id, ticketId: r.ticket_id, ticketNumber: r.ticket_number, + companyId: r.company_id, companyName: r.company_name, title: r.title, createdAt: r.created_at, @@ -255,15 +264,50 @@ export async function GET( | { from?: { email?: string | null } | null } | null; const createdAt = new Date(primaryReport.createdAt); - blastRadius = await getBlastRadius({ - sender: senderIndicator?.value ?? messageHeaders?.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. + 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 ?? messageHeaders?.from?.email ?? '', + recipient: primaryReport.requesterEmail ?? '', + subject: primaryMessage?.subject ?? primaryReport.title ?? '', + dateWindow: { + start: new Date(createdAt.getTime() - 24 * 60 * 60 * 1000), + // Bug 1: clamp the end of the window to now — a freshly-detected + // campaign's primary report is <24h old, which would otherwise + // produce a future `end` that Mimecast's real API rejects + // (err_track_and_trace_invalid_end_date), swallowed internally by + // searchDeliveredMessages() as a false zero-count "clean" result. + end: new Date(Math.min(createdAt.getTime() + 24 * 60 * 60 * 1000, Date.now())), + }, }, - }); + tenantOptions + ); } else { blastRadius = { status: 'unavailable', reason: 'not_configured' }; }