/** * 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); });