feat(23-05): wire gated parse->classify->acknowledge chain into webhook
- triggerPhishingDetection now captures groupReportIntoCampaign's result and, when a campaignId exists, calls new runGatedPhishingStages - runGatedPhishingStages reads the per-company automation gate and conditionally runs parseAndStoreMessage, classifyCampaign, and (only for USER_AWARENESS verdicts) generateAndPostAcknowledgment - each stage isolated in its own try/catch (T-23-09); detection + grouping remain unconditional (D-07); auto_report never posts any other action (D-04, T-23-08)
This commit is contained in:
parent
e0f22f27c9
commit
e1193bf476
1 changed files with 73 additions and 1 deletions
|
|
@ -16,6 +16,10 @@ import '../services/workflow-steps'; // Register all workflow step executors
|
||||||
import { WorkflowEvent, TicketData } from '../types/workflow';
|
import { WorkflowEvent, TicketData } from '../types/workflow';
|
||||||
import { detectPhishingTicket, DetectableTicket } from './phishing-detector';
|
import { detectPhishingTicket, DetectableTicket } from './phishing-detector';
|
||||||
import { groupReportIntoCampaign } from './campaign-grouping-service';
|
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 {
|
export class WebhookService {
|
||||||
private _autotaskClient: AutotaskClient | null = null;
|
private _autotaskClient: AutotaskClient | null = null;
|
||||||
|
|
@ -490,7 +494,75 @@ export class WebhookService {
|
||||||
const detection = await detectPhishingTicket(ticket);
|
const detection = await detectPhishingTicket(ticket);
|
||||||
// D-01/D-08: automatic path short-circuits if already grouped.
|
// D-01/D-08: automatic path short-circuits if already grouped.
|
||||||
if (detection.flagged && detection.reportId) {
|
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<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue