- KNOWN_PHISHING_PATTERNS: the 8 locked DETECT-01 strings - matchesPhishingPatterns: case-insensitive substring match (toLowerCase + includes only, no RegExp/eval), mirrors robotic-classifier.evaluateContains - computePhishingContentHash: sha256 over title+description only (D-04), excludes bump-prone fields like status/last_activity_date
61 lines
2 KiB
TypeScript
61 lines
2 KiB
TypeScript
/**
|
|
* 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');
|
|
}
|