chore: merge quick task worktree (worktree-agent-a498794d9a221b8b7)

This commit is contained in:
lorentz 2026-07-21 16:47:36 -04:00
commit 1cd0d4db48
2 changed files with 154 additions and 19 deletions

View file

@ -247,12 +247,14 @@ interface ReportFixtureRow {
title: string | null;
created_at: string;
requester_email: string | null;
company_id: string | null;
}
interface StagedRows {
reports?: ReportFixtureRow[];
messages?: Array<{ id: string; report_id: string; headers: NormalizedMessage }>;
indicators?: Array<{ id: string; message_id: string; indicator_type: string; value: string }>;
mimecastTenants?: Array<{ client_id: string; client_secret: string; base_url: string | null }>;
}
function stageQueries(rows: StagedRows) {
@ -260,6 +262,9 @@ function stageQueries(rows: StagedRows) {
if (sql.includes('INSERT INTO classifications')) {
return { rows: [{ id: 'classification-1', created_at: '2026-07-16T00:00:00.000Z' }], rowCount: 1 };
}
if (sql.includes('FROM mimecast_tenants')) {
return { rows: rows.mimecastTenants ?? [], rowCount: rows.mimecastTenants?.length ?? 0 };
}
if (sql.includes('FROM reports')) {
return { rows: rows.reports ?? [], rowCount: rows.reports?.length ?? 0 };
}
@ -288,7 +293,7 @@ describe('classifyCampaign', () => {
it('returns exactly one verdict with the full payload shape (returns exactly one verdict)', async () => {
stageQueries({
reports: [
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', cleanSpamMessage)],
indicators: [],
@ -320,7 +325,7 @@ describe('classifyCampaign', () => {
it('inserts exactly one append-only classifications row with no ON CONFLICT', async () => {
stageQueries({
reports: [
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', cleanSpamMessage)],
indicators: [],
@ -353,7 +358,7 @@ describe('classifyCampaign', () => {
async (_label, fixture) => {
stageQueries({
reports: [
{ id: 'report-1', title: fixture.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: fixture.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', fixture)],
indicators: [],
@ -383,7 +388,7 @@ describe('classifyCampaign', () => {
async (_label, fixture) => {
stageQueries({
reports: [
{ id: 'report-1', title: fixture.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: fixture.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', fixture)],
indicators: [],
@ -410,7 +415,7 @@ describe('classifyCampaign', () => {
it('classifies a real non-simulation signal as THREAT with destructive recommended actions (threat tier)', async () => {
stageQueries({
reports: [
{ id: 'report-1', title: threatMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: threatMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', threatMessage)],
indicators: [],
@ -442,8 +447,8 @@ describe('classifyCampaign', () => {
const sharedUrl = 'http://evil-shared.example.test/payload';
stageQueries({
reports: [
{ id: 'report-1', title: 'Invoice attached', created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-2', title: 'Invoice attached', created_at: '2026-07-15T02:00:00.000Z', requester_email: 'reporter2@wulfconsulting.test' },
{ id: 'report-1', title: 'Invoice attached', created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
{ id: 'report-2', title: 'Invoice attached', created_at: '2026-07-15T02:00:00.000Z', requester_email: 'reporter2@wulfconsulting.test' , company_id: null},
],
messages: [
toMessageRow('message-1', 'report-1', cleanSpamMessage),
@ -473,7 +478,7 @@ describe('classifyCampaign', () => {
it('classifies a clean campaign with no indicators and no delivery/click signal as SPAM (spam vs unwanted tier)', async () => {
stageQueries({
reports: [
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', cleanSpamMessage)],
indicators: [],
@ -497,7 +502,7 @@ describe('classifyCampaign', () => {
it('classifies a suspicious-but-contained campaign (one url indicator, delivery contained to reporter) as UNWANTED (spam vs unwanted tier)', async () => {
stageQueries({
reports: [
{ id: 'report-1', title: suspiciousUnwantedMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: suspiciousUnwantedMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', suspiciousUnwantedMessage)],
indicators: [
@ -529,7 +534,7 @@ describe('classifyCampaign', () => {
}));
stageQueries({
reports: [
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL },
{ id: 'report-1', title: cleanSpamMessage.subject, created_at: '2026-07-15T00:00:00.000Z', requester_email: REPORTER_EMAIL , company_id: null},
],
messages: [toMessageRow('message-1', 'report-1', cleanSpamMessage)],
indicators: manyIndicators,
@ -552,4 +557,91 @@ describe('classifyCampaign', () => {
expect(reason.length).toBeLessThan(300);
}
});
// ===========================================================================
// Bug 2 (D-05) parity: per-company Mimecast tenant resolution vs. global
// env fallback — mirrors app/api/phishing/campaigns/[id]/route.ts's
// already-tested tenant-resolution block (260721-n49).
// ===========================================================================
it('resolves the reporting company\'s own Mimecast tenant and scopes getBlastRadius when an enabled mimecast_tenants row exists (tenant resolution)', async () => {
stageQueries({
reports: [
{
id: 'report-1',
title: cleanSpamMessage.subject,
created_at: '2026-07-15T00:00:00.000Z',
requester_email: REPORTER_EMAIL,
company_id: '29683407',
},
],
messages: [toMessageRow('message-1', 'report-1', cleanSpamMessage)],
indicators: [],
mimecastTenants: [
{ client_id: 'tenant-client-id', client_secret: 'tenant-client-secret', base_url: 'https://eu-api.mimecast.com' },
],
});
getBlastRadiusMock.mockResolvedValue({
status: 'ok',
matched: 1,
delivered: 0,
held: 1,
rejected: 0,
clicked: 0,
perRecipient: [],
source: 'fan-out',
});
await classifyCampaign('campaign-1');
expect(getBlastRadiusMock).toHaveBeenCalledTimes(1);
const [, tenantOptions] = getBlastRadiusMock.mock.calls[0];
expect(tenantOptions).toBeDefined();
expect(tenantOptions.cacheScope).toBe('29683407');
expect(tenantOptions.client).toBeTruthy();
const tenantQueryCalls = queryMock.mock.calls.filter(
([sql]) => typeof sql === 'string' && sql.includes('FROM mimecast_tenants')
);
expect(tenantQueryCalls).toHaveLength(1);
expect(tenantQueryCalls[0][0]).toMatch(/company_id = \$1 AND enabled = true/);
});
it.each([
['no companyId on the primary report', null, [] as Array<{ client_id: string; client_secret: string; base_url: string | null }>],
['companyId set but no enabled tenant row', '29683407', []],
])(
'calls getBlastRadius with no tenant scoping when %s (global fallback preserved)',
async (_label, companyId, mimecastTenants) => {
stageQueries({
reports: [
{
id: 'report-1',
title: cleanSpamMessage.subject,
created_at: '2026-07-15T00:00:00.000Z',
requester_email: REPORTER_EMAIL,
company_id: companyId,
},
],
messages: [toMessageRow('message-1', 'report-1', cleanSpamMessage)],
indicators: [],
mimecastTenants,
});
getBlastRadiusMock.mockResolvedValue({
status: 'ok',
matched: 1,
delivered: 0,
held: 1,
rejected: 0,
clicked: 0,
perRecipient: [],
source: 'fan-out',
});
await classifyCampaign('campaign-1');
expect(getBlastRadiusMock).toHaveBeenCalledTimes(1);
expect(getBlastRadiusMock.mock.calls[0][1]).toBeUndefined();
}
);
});

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' };