feat(16-01): implement selectOriginalMessage three-tier attachment selection
GREEN: rfc.eml exact match -> single non-OriginatingEmail message/rfc822 candidate (covers KnowBe4 versioned filenames) -> OriginatingEmail.eml fallback -> null. Case-insensitive on both content-type and filename (checked via title/fullPath basename). All 8 selectOriginalMessage tests pass; tsc clean for eml-parser files.
This commit is contained in:
parent
2fde1156da
commit
e4718ae71a
1 changed files with 62 additions and 0 deletions
62
lib/services/eml-parser.ts
Normal file
62
lib/services/eml-parser.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* EML/MIME evidence parser for the phishing-triage pipeline (Phase 16).
|
||||
*
|
||||
* Turns a raw RFC822/MIME `.eml` buffer (an attacker-controlled email a
|
||||
* user reported as phishing/spam) into a normalized, structured
|
||||
* `NormalizedMessage` — headers, structured SPF/DKIM/DMARC verdicts,
|
||||
* URLs, and attachment metadata — using `mailparser` for MIME parsing
|
||||
* and a small hand-rolled RFC 8601 tokenizer for Authentication-Results.
|
||||
*
|
||||
* Hard invariant (SC#3 / EVID-04 / T-16-03): this module must never fetch
|
||||
* or execute anything found in a message. It never dereferences an
|
||||
* extracted URL, never renders `mail.html`, and never performs any
|
||||
* outbound network call while parsing. This is test-enforced with a
|
||||
* `global.fetch` spy in eml-parser.test.ts.
|
||||
*
|
||||
* Also implements `selectOriginalMessage` (EVID-02) — a pure, I/O-free
|
||||
* decision over ticket attachment metadata already in hand, choosing the
|
||||
* originally-reported message from a ticket's attachment list.
|
||||
*/
|
||||
|
||||
import type { Attachment } from '@/lib/types/autotask';
|
||||
|
||||
/** Basename of an attachment's filename, lowercased, for tier matching. */
|
||||
function attachmentName(att: Attachment): string {
|
||||
const raw = att.fullPath || att.title || '';
|
||||
const base = raw.split('/').pop() || raw;
|
||||
return base.toLowerCase();
|
||||
}
|
||||
|
||||
function isMessageRfc822(att: Attachment): boolean {
|
||||
return (att.contentType || '').toLowerCase() === 'message/rfc822';
|
||||
}
|
||||
|
||||
/**
|
||||
* Three-tier `.eml` attachment selection, empirically validated against 15
|
||||
* real phishing tickets (see 16-RESEARCH.md Pitfall 1):
|
||||
*
|
||||
* 1. An attachment named exactly `rfc.eml` (case-insensitive) among
|
||||
* `message/rfc822` attachments — the Microsoft "Report Message" flow.
|
||||
* 2. Else, among `message/rfc822` attachments, exclude any named exactly
|
||||
* `OriginatingEmail.eml` (case-insensitive) — if exactly one candidate
|
||||
* remains, select it (covers KnowBe4's versioned filenames, e.g.
|
||||
* `phish_alert_sp2_2.0.0.0.eml`).
|
||||
* 3. Else (0 or 2+ ambiguous candidates after step 2) — fall back to
|
||||
* `OriginatingEmail.eml` if present; otherwise return null.
|
||||
*/
|
||||
export function selectOriginalMessage(attachments: Attachment[]): Attachment | null {
|
||||
const rfc822Attachments = attachments.filter(isMessageRfc822);
|
||||
|
||||
const exactRfcEml = rfc822Attachments.find((att) => attachmentName(att) === 'rfc.eml');
|
||||
if (exactRfcEml) return exactRfcEml;
|
||||
|
||||
const nonOriginatingCandidates = rfc822Attachments.filter(
|
||||
(att) => attachmentName(att) !== 'originatingemail.eml'
|
||||
);
|
||||
if (nonOriginatingCandidates.length === 1) return nonOriginatingCandidates[0];
|
||||
|
||||
const originatingFallback = rfc822Attachments.find(
|
||||
(att) => attachmentName(att) === 'originatingemail.eml'
|
||||
);
|
||||
return originatingFallback ?? null;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue