wulf-pulse/lib/services/eml-parser.fixtures.ts
lorentz 4df4816b21 test(16-01): add failing tests for parseEml/parseAuthResults/extractUrls/buildBodyPreview
RED: covers EVID-03 (normalized headers, structured auth verdicts,
Received chain, URLs, attachment metadata incl. related flag),
EVID-04 (no-network spy, truncated body preview), and the DoS size
guard (oversized buffer rejected before simpleParser). New synthetic
fixtures: rich multipart, auth-results-original, inline/CID attachment,
long-body, fuzzy-URL, and an oversized-buffer generator. None of these
exports exist on eml-parser.ts yet.
2026-07-15 10:28:22 -04:00

227 lines
7.9 KiB
TypeScript

/**
* Synthetic fixtures for eml-parser.test.ts. Nothing here is real customer
* content — all addresses, subjects, and bodies are invented for testing
* only (per this milestone's explicit synthetic-fixture-only constraint).
*/
import type { Attachment } from '@/lib/types/autotask';
function makeAttachment(overrides: Partial<Attachment> & { id: number }): Attachment {
return {
attachmentType: 'FILE_ATTACHMENT',
fullPath: '',
title: '',
publish: 1,
contentType: 'message/rfc822',
...overrides,
};
}
/** Tier 1: exact `rfc.eml` present among message/rfc822 attachments (Microsoft "Report Message" flow). */
export const RFC_EML_TIER_ATTACHMENTS: Attachment[] = [
makeAttachment({ id: 1, title: 'rfc.eml', fullPath: 'rfc.eml' }),
makeAttachment({ id: 2, title: 'OriginatingEmail.eml', fullPath: 'OriginatingEmail.eml' }),
];
/** Same tier, but with mixed case to verify case-insensitive matching. */
export const RFC_EML_TIER_ATTACHMENTS_UPPERCASE: Attachment[] = [
makeAttachment({ id: 1, title: 'RFC.EML', fullPath: 'RFC.EML' }),
makeAttachment({ id: 2, title: 'OriginatingEmail.EML', fullPath: 'OriginatingEmail.EML' }),
];
/** Tier 2: KnowBe4 PhishER flow — versioned filename, no literal rfc.eml. */
export const KNOWBE4_TIER_ATTACHMENTS: Attachment[] = [
makeAttachment({
id: 10,
title: 'phish_alert_sp2_2.0.0.0.eml',
fullPath: 'phish_alert_sp2_2.0.0.0.eml',
}),
makeAttachment({ id: 11, title: 'OriginatingEmail.eml', fullPath: 'OriginatingEmail.eml' }),
];
/** Tier 3: only OriginatingEmail.eml present (older tickets, majority-case fallback). */
export const ORIGINATING_ONLY_ATTACHMENTS: Attachment[] = [
makeAttachment({ id: 20, title: 'OriginatingEmail.eml', fullPath: 'OriginatingEmail.eml' }),
];
/** Ambiguous: two non-OriginatingEmail message/rfc822 candidates, no rfc.eml — falls back to OriginatingEmail.eml. */
export const AMBIGUOUS_WITH_FALLBACK_ATTACHMENTS: Attachment[] = [
makeAttachment({ id: 30, title: 'weird_name_1.eml', fullPath: 'weird_name_1.eml' }),
makeAttachment({ id: 31, title: 'weird_name_2.eml', fullPath: 'weird_name_2.eml' }),
makeAttachment({ id: 32, title: 'OriginatingEmail.eml', fullPath: 'OriginatingEmail.eml' }),
];
/** Ambiguous with no fallback available — selectOriginalMessage should return null. */
export const AMBIGUOUS_NO_FALLBACK_ATTACHMENTS: Attachment[] = [
makeAttachment({ id: 40, title: 'weird_name_1.eml', fullPath: 'weird_name_1.eml' }),
makeAttachment({ id: 41, title: 'weird_name_2.eml', fullPath: 'weird_name_2.eml' }),
];
/** No .eml attachments at all — selectOriginalMessage should return null. */
export const NO_EML_ATTACHMENTS: Attachment[] = [
makeAttachment({
id: 50,
title: 'screenshot.png',
fullPath: 'screenshot.png',
contentType: 'image/png',
}),
];
/** Empty attachment list. */
export const EMPTY_ATTACHMENTS: Attachment[] = [];
// ---------------------------------------------------------------------------
// Synthetic raw .eml buffers for parseEml (EVID-03/EVID-04). All addresses,
// bodies, and content below are invented for testing only — no real
// customer email content per this milestone's Out of Scope constraint.
// ---------------------------------------------------------------------------
/**
* Rich multipart fixture: text + html bodies (each carrying the same URL),
* one non-inline base64 attachment, and an Authentication-Results header
* with spf=pass, dkim=fail, dmarc=none.
*/
export const RICH_MULTIPART_EML = Buffer.from(
`From: "Attacker Corp" <attacker@evil-example.test>
To: victim@wulfconsulting.test
Cc: cc-user@wulfconsulting.test
Reply-To: reply@evil-example.test
Return-Path: <bounce@evil-example.test>
Subject: Urgent: verify your account
Date: Mon, 15 Jul 2026 12:00:00 +0000
Message-ID: <rich1@evil-example.test>
Authentication-Results: mx.wulfconsulting.test; spf=pass smtp.mailfrom=evil-example.test; dkim=fail header.d=evil-example.test; dmarc=none header.from=evil-example.test
Received: from mx1.example.test by mx2.example.test; Mon, 15 Jul 2026 11:59:00 +0000
Received: from mx0.example.test by mx1.example.test; Mon, 15 Jul 2026 11:58:00 +0000
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="BOUNDARY1"
--BOUNDARY1
Content-Type: multipart/alternative; boundary="BOUNDARY2"
--BOUNDARY2
Content-Type: text/plain; charset="UTF-8"
Please visit http://evil-example.test/verify to verify your account.
--BOUNDARY2
Content-Type: text/html; charset="UTF-8"
<html><body><p>Please visit <a href="http://evil-example.test/verify">this link</a> to verify your account.</p></body></html>
--BOUNDARY2--
--BOUNDARY1
Content-Type: application/pdf; name="invoice.pdf"
Content-Disposition: attachment; filename="invoice.pdf"
Content-Transfer-Encoding: base64
SGVsbG8gV29ybGQh
--BOUNDARY1--
`
);
/** Same as RICH_MULTIPART_EML but also carries Authentication-Results-Original. */
export const RICH_MULTIPART_WITH_AUTH_ORIGINAL_EML = Buffer.from(
`From: "Attacker Corp" <attacker@evil-example.test>
To: victim@wulfconsulting.test
Subject: Urgent: verify your account (remediated)
Date: Mon, 15 Jul 2026 12:00:00 +0000
Message-ID: <rich2@evil-example.test>
Authentication-Results: mx.wulfconsulting.test; spf=fail smtp.mailfrom=evil-example.test; dkim=fail header.d=evil-example.test; dmarc=fail header.from=evil-example.test
Authentication-Results-Original: mx.wulfconsulting.test; spf=pass smtp.mailfrom=evil-example.test; dkim=pass header.d=evil-example.test; dmarc=pass header.from=evil-example.test
MIME-Version: 1.0
Content-Type: text/plain; charset="UTF-8"
Body text for the remediated-header fixture.
`
);
/**
* Inline/CID attachment fixture: html references cid:sig123, wrapped in
* multipart/related so mailparser marks the image attachment `related: true`
* (Pitfall 5 — inline parts must be kept, not dropped).
*/
export const INLINE_ATTACHMENT_EML = Buffer.from(
`From: sender@evil-example.test
To: victim@wulfconsulting.test
Subject: Newsletter with inline logo
Date: Mon, 15 Jul 2026 12:00:00 +0000
Message-ID: <inline1@evil-example.test>
MIME-Version: 1.0
Content-Type: multipart/related; boundary="RELBOUND"
--RELBOUND
Content-Type: multipart/alternative; boundary="ALTBOUND"
--ALTBOUND
Content-Type: text/plain; charset="UTF-8"
Plain text body with an inline logo.
--ALTBOUND
Content-Type: text/html; charset="UTF-8"
<html><body><p>Hello</p><img src="cid:sig123"></body></html>
--ALTBOUND--
--RELBOUND
Content-Type: image/png; name="sig.png"
Content-Disposition: inline
Content-ID: <sig123>
Content-Transfer-Encoding: base64
iVBORw0KGgo=
--RELBOUND--
`
);
/** Long-body fixture — long enough to force buildBodyPreview truncation. */
const LONG_PARAGRAPH =
'This is a long paragraph of synthetic phishing-style body text repeated ' +
'several times to exceed the body preview truncation threshold. '.repeat(20);
export const LONG_BODY_EML = Buffer.from(
`From: sender@evil-example.test
To: victim@wulfconsulting.test
Subject: Long body fixture
Date: Mon, 15 Jul 2026 12:00:00 +0000
Message-ID: <long1@evil-example.test>
MIME-Version: 1.0
Content-Type: text/plain; charset="UTF-8"
${LONG_PARAGRAPH}
`
);
/** Fuzzy (scheme-less) www. URL fixture, to exercise linkify-it's fuzzyLink mode. */
export const FUZZY_URL_EML = Buffer.from(
`From: sender@evil-example.test
To: victim@wulfconsulting.test
Subject: Fuzzy URL fixture
Date: Mon, 15 Jul 2026 12:00:00 +0000
Message-ID: <fuzzy1@evil-example.test>
MIME-Version: 1.0
Content-Type: text/plain; charset="UTF-8"
Visit www.evil-example.com/login to reset your password.
`
);
/** Oversized buffer — exceeds MAX_EML_BYTES, must be rejected before simpleParser runs. */
export function makeOversizedEmlBuffer(maxBytes: number): Buffer {
const header = `From: sender@evil-example.test
To: victim@wulfconsulting.test
Subject: Oversized fixture
Date: Mon, 15 Jul 2026 12:00:00 +0000
Message-ID: <oversized1@evil-example.test>
MIME-Version: 1.0
Content-Type: text/plain; charset="UTF-8"
`;
const padding = 'A'.repeat(maxBytes + 1024 - header.length);
return Buffer.from(header + padding);
}