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.
This commit is contained in:
lorentz 2026-07-16 16:47:19 -04:00
parent 12250c1e1d
commit 9951e53832

View file

@ -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<ReportRow>(
`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<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 ?? 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' };
}