diff --git a/lib/services/phishing-detector.ts b/lib/services/phishing-detector.ts new file mode 100644 index 0000000..224860b --- /dev/null +++ b/lib/services/phishing-detector.ts @@ -0,0 +1,61 @@ +/** + * Phishing Detector + * + * Shared detection core called by both the webhook path (Plan 03) and the + * cron sweep (Plan 03). Matches a ticket's title+description against the 8 + * locked DETECT-01 patterns, computes a content hash for D-04 idempotency. + * + * One deterministic, testable detector with no duplicated matching logic + * between callers (CONTEXT.md discretion: "both call the same underlying + * logic"). + */ + +import { createHash } from 'crypto'; + +// ============================================================================= +// Pure detection logic — pattern matcher + content hash +// ============================================================================= + +/** + * The 8 locked DETECT-01 patterns (case-insensitive substring match). + */ +export const KNOWN_PHISHING_PATTERNS: readonly string[] = [ + 'Phishing Report', + 'Spam Alert', + 'Phishing Alert - Email Security Report', + 'KnowBe4 Phish Alert Report', + 'Source: KnowBe4 Phish Alert Button', + 'userSubmissionsReportMessage', + 'reported message destinations', + 'Microsoft directly', +]; + +/** + * Case-insensitive substring match against the locked pattern list — mirrors + * robotic-classifier.evaluateContains (.toLowerCase() + .includes() only, + * NO regex, NO eval). + */ +export function matchesPhishingPatterns( + title: string | null, + description: string | null +): { flagged: boolean; matched: string[] } { + const haystack = `${title ?? ''} ${description ?? ''}`.toLowerCase(); + const matched = KNOWN_PHISHING_PATTERNS.filter((pattern) => + haystack.includes(pattern.toLowerCase()) + ); + return { flagged: matched.length > 0, matched }; +} + +/** + * sha256 over title+description only (D-04) — does NOT include + * last_activity_date, status, or any bump-prone field, so status/assignee + * churn never forces reprocessing. + */ +export function computePhishingContentHash( + title: string | null, + description: string | null +): string { + return createHash('sha256') + .update(JSON.stringify({ title: title ?? '', description: description ?? '' })) + .digest('hex'); +}