diff --git a/lib/services/webhook-service.ts b/lib/services/webhook-service.ts index f523396..cc49846 100644 --- a/lib/services/webhook-service.ts +++ b/lib/services/webhook-service.ts @@ -16,6 +16,10 @@ import '../services/workflow-steps'; // Register all workflow step executors import { WorkflowEvent, TicketData } from '../types/workflow'; import { detectPhishingTicket, DetectableTicket } from './phishing-detector'; import { groupReportIntoCampaign } from './campaign-grouping-service'; +import { getCompanyAutomationGate } from './phishing-automation-gate'; +import { parseAndStoreMessage } from './phishing-eml-service'; +import { classifyCampaign, Verdict } from './campaign-classifier'; +import { generateAndPostAcknowledgment } from './triage-note-service'; export class WebhookService { private _autotaskClient: AutotaskClient | null = null; @@ -490,7 +494,75 @@ export class WebhookService { const detection = await detectPhishingTicket(ticket); // D-01/D-08: automatic path short-circuits if already grouped. if (detection.flagged && detection.reportId) { - await groupReportIntoCampaign(detection.reportId, { skipIfAlreadyGrouped: true }); + const grouped = await groupReportIntoCampaign(detection.reportId, { skipIfAlreadyGrouped: true }); + if (grouped?.campaignId) { + await this.runGatedPhishingStages({ + campaignId: grouped.campaignId, + companyId: r.company_id, + reportId: detection.reportId, + ticketId: Number(r.id), + }); + } + } + } + + /** + * Phase 23 D-04/D-06/D-07: runs the opted-in parse -> classify -> report + * chain for a company after detection + grouping have already run + * unconditionally. Each stage is independently gated by + * `phishing_automation_gate` and isolated in its own try/catch so a + * failure in one stage never blocks the webhook response or aborts a + * later stage (T-23-09). + * + * auto_report auto-posts EXCLUSIVELY the acknowledge_user thank-you note, + * and only when the campaign's current verdict is USER_AWARENESS (D-04). + * Every other verdict/action remains proposed-only and manual-approval + * gated — this method never calls approve/remediate/block/purge/warn_user. + */ + private async runGatedPhishingStages(input: { + campaignId: string; + companyId: number | null; + reportId: string; + ticketId: number; + }): Promise { + const { campaignId, companyId, reportId, ticketId } = input; + const gate = await getCompanyAutomationGate(companyId); + + if (gate.autoParse) { + try { + await parseAndStoreMessage({ reportId, ticketId }); + } catch (err) { + console.error('[WEBHOOK] auto_parse stage error', err); + } + } + + let verdict: Verdict | null = null; + + if (gate.autoClassify) { + try { + const result = await classifyCampaign(campaignId); + verdict = result.verdict; + } catch (err) { + console.error('[WEBHOOK] auto_classify stage error', err); + } + } + + if (gate.autoReport) { + try { + if (verdict === null) { + const latest = await postgresClient.query<{ verdict: string | null }>( + `SELECT verdict FROM classifications WHERE campaign_id = $1 ORDER BY created_at DESC LIMIT 1`, + [campaignId] + ); + verdict = (latest.rows[0]?.verdict as Verdict | undefined) ?? null; + } + + if (verdict === 'USER_AWARENESS') { + await generateAndPostAcknowledgment(campaignId); + } + } catch (err) { + console.error('[WEBHOOK] auto_report stage error', err); + } } } }