wulf-pulse/scripts/reanalyze-seubert-burst.ts

106 lines
3.8 KiB
TypeScript
Raw Permalink Normal View History

/**
* reanalyze-seubert-burst.ts
*
* One-shot: (1) reclassify ticket 699415's campaign now that the container
* has been rebuilt with the Phase 23 USER_AWARENESS classifier (the existing
* classification ran against the pre-Phase-23 code and is stale), and
* (2) run /analyze's parse+group steps for the 7 Seubert reports (699419,
* 699421, 699422, 699433, 699435, 699441, 699445) that landed before the
* company's automation gate was switched on and never got EML-parsed.
*
* Usage:
* POSTGRES_HOST=localhost npx tsx scripts/reanalyze-seubert-burst.ts
*/
import { config } from 'dotenv';
import { resolve } from 'path';
config({ path: resolve('/opt/stacks/pulse/.env.local') });
import postgresClient from '../lib/services/postgres-client';
import { detectPhishingTicket, type DetectableTicket } from '../lib/services/phishing-detector';
import { parseAndStoreMessage } from '../lib/services/phishing-eml-service';
import { groupReportIntoCampaign } from '../lib/services/campaign-grouping-service';
import { classifyCampaign } from '../lib/services/campaign-classifier';
const RECLASSIFY_CAMPAIGN_ID = '60a3613c-5b6c-45b1-8441-411a3fbf100c'; // ticket 699415
const UNPARSED_TICKET_IDS = [699419, 699421, 699422, 699433, 699435, 699441, 699445];
async function reanalyzeTicket(ticketId: number) {
const row = await postgresClient.query<{
id: string;
ticket_number: string | null;
title: string | null;
description: string | null;
company_id: number | null;
contact_id: number | null;
created_by_contact_id: number | null;
}>(
`SELECT id, ticket_number, title, description, company_id, contact_id, created_by_contact_id
FROM tickets WHERE id = $1`,
[ticketId]
);
const r = row.rows[0];
if (!r) {
console.log(` ticket ${ticketId}: NOT FOUND in Postgres`);
return;
}
const ticket: DetectableTicket = {
id: Number(r.id),
ticket_number: r.ticket_number,
title: r.title,
description: r.description,
company_id: r.company_id,
contact_id: r.contact_id,
created_by_contact_id: r.created_by_contact_id,
};
const detection = await detectPhishingTicket(ticket);
if (!detection.flagged || !detection.reportId) {
console.log(` ticket ${ticketId}: detector did not flag it (unexpected — it already has a reports row)`);
return;
}
const parseResult = await parseAndStoreMessage({ reportId: detection.reportId, ticketId });
const grouped = await groupReportIntoCampaign(detection.reportId);
console.log(
` ticket ${ticketId}: parse=${JSON.stringify(parseResult)} campaignId=${grouped?.campaignId ?? 'null'} groupMethod=${grouped?.groupMethod ?? 'null'}`
);
if (grouped?.campaignId) {
try {
const result = await classifyCampaign(grouped.campaignId);
console.log(` classified: verdict=${result.verdict}`);
} catch (err) {
console.log(` classify failed: ${err instanceof Error ? err.message : err}`);
}
}
}
async function main() {
console.log('=== Step 1: Reclassify 699415 campaign (stale pre-Phase-23 verdict) ===');
const before = await postgresClient.query<{ verdict: string | null }>(
`SELECT verdict FROM classifications WHERE campaign_id = $1 ORDER BY created_at DESC LIMIT 1`,
[RECLASSIFY_CAMPAIGN_ID]
);
console.log(` verdict before: ${before.rows[0]?.verdict ?? 'none'}`);
const result = await classifyCampaign(RECLASSIFY_CAMPAIGN_ID);
console.log(` verdict after: ${result.verdict}`);
console.log(` reasons: ${JSON.stringify(result.reasons ?? [], null, 2)}`);
console.log('\n=== Step 2: Analyze the 7 unparsed Seubert reports ===');
for (const ticketId of UNPARSED_TICKET_IDS) {
await reanalyzeTicket(ticketId);
}
console.log('\nDone.');
process.exit(0);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});