feat(15-02): implement phishing pattern matcher + content hash
- 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
This commit is contained in:
parent
0e7daf9a6a
commit
aabf5322e9
1 changed files with 61 additions and 0 deletions
61
lib/services/phishing-detector.ts
Normal file
61
lib/services/phishing-detector.ts
Normal file
|
|
@ -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');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue