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
This commit is contained in:
lorentz 2026-07-21 16:44:44 -04:00
parent f58856e103
commit 9f75f2160c

View file

@ -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<Campai
// app/api/phishing/campaigns/[id]/route.ts's existing bulk-fetch shape).
const reportsRes = await postgresClient.query<ReportDbRow>(
`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<Campai
title: r.title,
createdAt: r.created_at,
requesterEmail: r.requester_email,
companyId: r.company_id,
}));
const reportIds = reports.map((r) => r.id);
@ -347,15 +358,47 @@ export async function gatherCampaignEvidence(campaignId: string): Promise<Campai
(i) => 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<typeof getMimecastClientForTenant>; cacheScope: string } | undefined;
if (primaryReport.companyId) {
const tenantRes = await postgresClient.query<MimecastTenantRow>(
`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' };