572 lines
16 KiB
TypeScript
572 lines
16 KiB
TypeScript
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||
|
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
||
|
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||
|
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||
|
import * as dns from 'dns';
|
||
|
import { promisify } from 'util';
|
||
|
|
||
|
const resolveTxt = promisify(dns.resolveTxt);
|
||
|
|
||
|
let testServer: any;
|
||
|
|
||
|
tap.test('setup test SMTP server', async () => {
|
||
|
testServer = await startTestSmtpServer();
|
||
|
expect(testServer).toBeTruthy();
|
||
|
expect(testServer.port).toBeGreaterThan(0);
|
||
|
});
|
||
|
|
||
|
tap.test('CSEC-05: DMARC record parsing', async () => {
|
||
|
// Test DMARC record parsing
|
||
|
const testDmarcRecords = [
|
||
|
{
|
||
|
domain: 'example.com',
|
||
|
record: 'v=DMARC1; p=reject; rua=mailto:dmarc@example.com; ruf=mailto:forensics@example.com; adkim=s; aspf=s; pct=100',
|
||
|
description: 'Strict DMARC with reporting'
|
||
|
},
|
||
|
{
|
||
|
domain: 'relaxed.com',
|
||
|
record: 'v=DMARC1; p=quarantine; adkim=r; aspf=r; pct=50',
|
||
|
description: 'Relaxed alignment, 50% quarantine'
|
||
|
},
|
||
|
{
|
||
|
domain: 'monitoring.com',
|
||
|
record: 'v=DMARC1; p=none; rua=mailto:reports@monitoring.com',
|
||
|
description: 'Monitor only mode'
|
||
|
},
|
||
|
{
|
||
|
domain: 'subdomain.com',
|
||
|
record: 'v=DMARC1; p=reject; sp=quarantine; adkim=s; aspf=s',
|
||
|
description: 'Different subdomain policy'
|
||
|
}
|
||
|
];
|
||
|
|
||
|
console.log('DMARC Record Analysis:\n');
|
||
|
|
||
|
for (const test of testDmarcRecords) {
|
||
|
console.log(`Domain: _dmarc.${test.domain}`);
|
||
|
console.log(`Record: ${test.record}`);
|
||
|
console.log(`Description: ${test.description}`);
|
||
|
|
||
|
// Parse DMARC tags
|
||
|
const tags = test.record.match(/(\w+)=([^;]+)/g);
|
||
|
if (tags) {
|
||
|
console.log('Tags:');
|
||
|
tags.forEach(tag => {
|
||
|
const [key, value] = tag.split('=');
|
||
|
const tagMeaning = {
|
||
|
'v': 'Version',
|
||
|
'p': 'Policy',
|
||
|
'sp': 'Subdomain Policy',
|
||
|
'rua': 'Aggregate Reports',
|
||
|
'ruf': 'Forensic Reports',
|
||
|
'adkim': 'DKIM Alignment',
|
||
|
'aspf': 'SPF Alignment',
|
||
|
'pct': 'Percentage',
|
||
|
'fo': 'Forensic Options'
|
||
|
}[key] || key;
|
||
|
console.log(` ${tagMeaning}: ${value}`);
|
||
|
});
|
||
|
}
|
||
|
console.log('');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tap.test('CSEC-05: DMARC alignment testing', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
|
||
|
// Test DMARC alignment scenarios
|
||
|
const alignmentTests = [
|
||
|
{
|
||
|
name: 'Fully aligned',
|
||
|
fromHeader: 'sender@example.com',
|
||
|
mailFrom: 'sender@example.com',
|
||
|
dkimDomain: 'example.com',
|
||
|
expectedResult: 'pass'
|
||
|
},
|
||
|
{
|
||
|
name: 'SPF aligned only',
|
||
|
fromHeader: 'noreply@example.com',
|
||
|
mailFrom: 'bounce@example.com',
|
||
|
dkimDomain: 'otherdomain.com',
|
||
|
expectedResult: 'pass' // One aligned identifier is enough
|
||
|
},
|
||
|
{
|
||
|
name: 'DKIM aligned only',
|
||
|
fromHeader: 'sender@example.com',
|
||
|
mailFrom: 'bounce@different.com',
|
||
|
dkimDomain: 'example.com',
|
||
|
expectedResult: 'pass' // One aligned identifier is enough
|
||
|
},
|
||
|
{
|
||
|
name: 'Neither aligned',
|
||
|
fromHeader: 'sender@example.com',
|
||
|
mailFrom: 'bounce@different.com',
|
||
|
dkimDomain: 'another.com',
|
||
|
expectedResult: 'fail'
|
||
|
},
|
||
|
{
|
||
|
name: 'Subdomain relaxed alignment',
|
||
|
fromHeader: 'sender@example.com',
|
||
|
mailFrom: 'bounce@mail.example.com',
|
||
|
dkimDomain: 'auth.example.com',
|
||
|
expectedResult: 'pass' // With relaxed alignment
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const test of alignmentTests) {
|
||
|
console.log(`\nTesting DMARC alignment: ${test.name}`);
|
||
|
console.log(` From header: ${test.fromHeader}`);
|
||
|
console.log(` MAIL FROM: ${test.mailFrom}`);
|
||
|
console.log(` DKIM domain: ${test.dkimDomain}`);
|
||
|
|
||
|
const email = new Email({
|
||
|
from: test.fromHeader,
|
||
|
to: ['recipient@example.com'],
|
||
|
subject: `DMARC Test: ${test.name}`,
|
||
|
text: 'Testing DMARC alignment',
|
||
|
envelope: {
|
||
|
from: test.mailFrom
|
||
|
},
|
||
|
dkim: {
|
||
|
domainName: test.dkimDomain,
|
||
|
keySelector: 'default',
|
||
|
privateKey: 'mock-key'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
await smtpClient.sendMail(email);
|
||
|
|
||
|
// Analyze alignment
|
||
|
const fromDomain = test.fromHeader.split('@')[1];
|
||
|
const mailFromDomain = test.mailFrom.split('@')[1];
|
||
|
const dkimDomain = test.dkimDomain;
|
||
|
|
||
|
// Check SPF alignment
|
||
|
const spfStrictAlign = fromDomain === mailFromDomain;
|
||
|
const spfRelaxedAlign = fromDomain === mailFromDomain ||
|
||
|
mailFromDomain?.endsWith(`.${fromDomain}`) ||
|
||
|
fromDomain?.endsWith(`.${mailFromDomain}`);
|
||
|
|
||
|
// Check DKIM alignment
|
||
|
const dkimStrictAlign = fromDomain === dkimDomain;
|
||
|
const dkimRelaxedAlign = fromDomain === dkimDomain ||
|
||
|
dkimDomain?.endsWith(`.${fromDomain}`) ||
|
||
|
fromDomain?.endsWith(`.${dkimDomain}`);
|
||
|
|
||
|
console.log(` SPF alignment: Strict=${spfStrictAlign}, Relaxed=${spfRelaxedAlign}`);
|
||
|
console.log(` DKIM alignment: Strict=${dkimStrictAlign}, Relaxed=${dkimRelaxedAlign}`);
|
||
|
console.log(` Expected result: ${test.expectedResult}`);
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CSEC-05: DMARC policy enforcement', async () => {
|
||
|
// Test different DMARC policies
|
||
|
const policies = [
|
||
|
{
|
||
|
policy: 'none',
|
||
|
description: 'Monitor only - no action taken',
|
||
|
action: 'Deliver normally, send reports'
|
||
|
},
|
||
|
{
|
||
|
policy: 'quarantine',
|
||
|
description: 'Quarantine failing messages',
|
||
|
action: 'Move to spam/junk folder'
|
||
|
},
|
||
|
{
|
||
|
policy: 'reject',
|
||
|
description: 'Reject failing messages',
|
||
|
action: 'Bounce the message'
|
||
|
}
|
||
|
];
|
||
|
|
||
|
console.log('\nDMARC Policy Actions:\n');
|
||
|
|
||
|
for (const p of policies) {
|
||
|
console.log(`Policy: p=${p.policy}`);
|
||
|
console.log(` Description: ${p.description}`);
|
||
|
console.log(` Action: ${p.action}`);
|
||
|
console.log('');
|
||
|
}
|
||
|
|
||
|
// Test percentage application
|
||
|
const percentageTests = [
|
||
|
{ pct: 100, description: 'Apply policy to all messages' },
|
||
|
{ pct: 50, description: 'Apply policy to 50% of messages' },
|
||
|
{ pct: 10, description: 'Apply policy to 10% of messages' },
|
||
|
{ pct: 0, description: 'Monitor only (effectively)' }
|
||
|
];
|
||
|
|
||
|
console.log('DMARC Percentage (pct) tag:\n');
|
||
|
|
||
|
for (const test of percentageTests) {
|
||
|
console.log(`pct=${test.pct}: ${test.description}`);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tap.test('CSEC-05: DMARC report generation', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
|
||
|
// Simulate DMARC report data
|
||
|
const reportData = {
|
||
|
reportMetadata: {
|
||
|
orgName: 'Example ISP',
|
||
|
email: 'dmarc-reports@example-isp.com',
|
||
|
reportId: '12345678',
|
||
|
dateRange: {
|
||
|
begin: new Date(Date.now() - 86400000).toISOString(),
|
||
|
end: new Date().toISOString()
|
||
|
}
|
||
|
},
|
||
|
policy: {
|
||
|
domain: 'example.com',
|
||
|
adkim: 'r',
|
||
|
aspf: 'r',
|
||
|
p: 'reject',
|
||
|
sp: 'reject',
|
||
|
pct: 100
|
||
|
},
|
||
|
records: [
|
||
|
{
|
||
|
sourceIp: '192.168.1.1',
|
||
|
count: 5,
|
||
|
disposition: 'none',
|
||
|
dkim: 'pass',
|
||
|
spf: 'pass'
|
||
|
},
|
||
|
{
|
||
|
sourceIp: '10.0.0.1',
|
||
|
count: 2,
|
||
|
disposition: 'reject',
|
||
|
dkim: 'fail',
|
||
|
spf: 'fail'
|
||
|
}
|
||
|
]
|
||
|
};
|
||
|
|
||
|
console.log('\nSample DMARC Aggregate Report Structure:');
|
||
|
console.log(JSON.stringify(reportData, null, 2));
|
||
|
|
||
|
// Send a DMARC report email
|
||
|
const email = new Email({
|
||
|
from: 'dmarc-reports@example-isp.com',
|
||
|
to: ['dmarc@example.com'],
|
||
|
subject: `Report Domain: example.com Submitter: example-isp.com Report-ID: ${reportData.reportMetadata.reportId}`,
|
||
|
text: 'DMARC Aggregate Report attached',
|
||
|
attachments: [{
|
||
|
filename: `example-isp.com!example.com!${Date.now()}!${Date.now() + 86400000}.xml.gz`,
|
||
|
content: Buffer.from('mock-compressed-xml-report'),
|
||
|
contentType: 'application/gzip'
|
||
|
}]
|
||
|
});
|
||
|
|
||
|
await smtpClient.sendMail(email);
|
||
|
console.log('\nDMARC report email sent successfully');
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CSEC-05: DMARC forensic reports', async () => {
|
||
|
// Test DMARC forensic report options
|
||
|
const forensicOptions = [
|
||
|
{
|
||
|
fo: '0',
|
||
|
description: 'Generate reports if all underlying mechanisms fail'
|
||
|
},
|
||
|
{
|
||
|
fo: '1',
|
||
|
description: 'Generate reports if any mechanism fails'
|
||
|
},
|
||
|
{
|
||
|
fo: 'd',
|
||
|
description: 'Generate reports if DKIM signature failed'
|
||
|
},
|
||
|
{
|
||
|
fo: 's',
|
||
|
description: 'Generate reports if SPF failed'
|
||
|
},
|
||
|
{
|
||
|
fo: '1:d:s',
|
||
|
description: 'Multiple options combined'
|
||
|
}
|
||
|
];
|
||
|
|
||
|
console.log('\nDMARC Forensic Report Options (fo tag):\n');
|
||
|
|
||
|
for (const option of forensicOptions) {
|
||
|
console.log(`fo=${option.fo}: ${option.description}`);
|
||
|
}
|
||
|
|
||
|
// Example forensic report structure
|
||
|
const forensicReport = {
|
||
|
feedbackType: 'auth-failure',
|
||
|
userAgent: 'Example-MTA/1.0',
|
||
|
version: 1,
|
||
|
originalMailFrom: 'sender@spoofed.com',
|
||
|
sourceIp: '192.168.1.100',
|
||
|
authResults: {
|
||
|
spf: {
|
||
|
domain: 'spoofed.com',
|
||
|
result: 'fail'
|
||
|
},
|
||
|
dkim: {
|
||
|
domain: 'example.com',
|
||
|
result: 'fail',
|
||
|
humanResult: 'signature verification failed'
|
||
|
},
|
||
|
dmarc: {
|
||
|
domain: 'example.com',
|
||
|
result: 'fail',
|
||
|
policy: 'reject'
|
||
|
}
|
||
|
},
|
||
|
originalHeaders: [
|
||
|
'From: sender@example.com',
|
||
|
'To: victim@target.com',
|
||
|
'Subject: Suspicious Email',
|
||
|
'Date: ' + new Date().toUTCString()
|
||
|
]
|
||
|
};
|
||
|
|
||
|
console.log('\nSample DMARC Forensic Report:');
|
||
|
console.log(JSON.stringify(forensicReport, null, 2));
|
||
|
});
|
||
|
|
||
|
tap.test('CSEC-05: DMARC subdomain policies', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
|
||
|
// Test subdomain policy inheritance
|
||
|
const subdomainTests = [
|
||
|
{
|
||
|
parentDomain: 'example.com',
|
||
|
parentPolicy: 'p=reject; sp=none',
|
||
|
subdomain: 'mail.example.com',
|
||
|
expectedPolicy: 'none'
|
||
|
},
|
||
|
{
|
||
|
parentDomain: 'example.com',
|
||
|
parentPolicy: 'p=reject', // No sp tag
|
||
|
subdomain: 'mail.example.com',
|
||
|
expectedPolicy: 'reject' // Inherits parent policy
|
||
|
},
|
||
|
{
|
||
|
parentDomain: 'example.com',
|
||
|
parentPolicy: 'p=quarantine; sp=reject',
|
||
|
subdomain: 'newsletter.example.com',
|
||
|
expectedPolicy: 'reject'
|
||
|
}
|
||
|
];
|
||
|
|
||
|
console.log('\nDMARC Subdomain Policy Tests:\n');
|
||
|
|
||
|
for (const test of subdomainTests) {
|
||
|
console.log(`Parent domain: ${test.parentDomain}`);
|
||
|
console.log(`Parent DMARC: v=DMARC1; ${test.parentPolicy}`);
|
||
|
console.log(`Subdomain: ${test.subdomain}`);
|
||
|
console.log(`Expected policy: ${test.expectedPolicy}`);
|
||
|
|
||
|
const email = new Email({
|
||
|
from: `sender@${test.subdomain}`,
|
||
|
to: ['recipient@example.com'],
|
||
|
subject: 'Subdomain Policy Test',
|
||
|
text: `Testing DMARC policy for ${test.subdomain}`
|
||
|
});
|
||
|
|
||
|
await smtpClient.sendMail(email);
|
||
|
console.log('');
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CSEC-05: DMARC deployment best practices', async () => {
|
||
|
// DMARC deployment phases
|
||
|
const deploymentPhases = [
|
||
|
{
|
||
|
phase: 1,
|
||
|
policy: 'p=none; rua=mailto:dmarc@example.com',
|
||
|
duration: '2-4 weeks',
|
||
|
description: 'Monitor only - collect data'
|
||
|
},
|
||
|
{
|
||
|
phase: 2,
|
||
|
policy: 'p=quarantine; pct=10; rua=mailto:dmarc@example.com',
|
||
|
duration: '1-2 weeks',
|
||
|
description: 'Quarantine 10% of failing messages'
|
||
|
},
|
||
|
{
|
||
|
phase: 3,
|
||
|
policy: 'p=quarantine; pct=50; rua=mailto:dmarc@example.com',
|
||
|
duration: '1-2 weeks',
|
||
|
description: 'Quarantine 50% of failing messages'
|
||
|
},
|
||
|
{
|
||
|
phase: 4,
|
||
|
policy: 'p=quarantine; pct=100; rua=mailto:dmarc@example.com',
|
||
|
duration: '2-4 weeks',
|
||
|
description: 'Quarantine all failing messages'
|
||
|
},
|
||
|
{
|
||
|
phase: 5,
|
||
|
policy: 'p=reject; rua=mailto:dmarc@example.com; ruf=mailto:forensics@example.com',
|
||
|
duration: 'Ongoing',
|
||
|
description: 'Reject all failing messages'
|
||
|
}
|
||
|
];
|
||
|
|
||
|
console.log('\nDMARC Deployment Best Practices:\n');
|
||
|
|
||
|
for (const phase of deploymentPhases) {
|
||
|
console.log(`Phase ${phase.phase}: ${phase.description}`);
|
||
|
console.log(` Record: v=DMARC1; ${phase.policy}`);
|
||
|
console.log(` Duration: ${phase.duration}`);
|
||
|
console.log('');
|
||
|
}
|
||
|
|
||
|
// Common mistakes
|
||
|
console.log('Common DMARC Mistakes to Avoid:\n');
|
||
|
const mistakes = [
|
||
|
'Jumping directly to p=reject without monitoring',
|
||
|
'Not setting up aggregate report collection (rua)',
|
||
|
'Ignoring subdomain policy (sp)',
|
||
|
'Not monitoring legitimate email sources before enforcement',
|
||
|
'Setting pct=100 too quickly',
|
||
|
'Not updating SPF/DKIM before DMARC'
|
||
|
];
|
||
|
|
||
|
mistakes.forEach((mistake, i) => {
|
||
|
console.log(`${i + 1}. ${mistake}`);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
tap.test('CSEC-05: DMARC and mailing lists', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
|
||
|
// Test mailing list scenario
|
||
|
console.log('\nDMARC Challenges with Mailing Lists:\n');
|
||
|
|
||
|
const originalEmail = new Email({
|
||
|
from: 'original@sender-domain.com',
|
||
|
to: ['mailinglist@list-server.com'],
|
||
|
subject: '[ListName] Original Subject',
|
||
|
text: 'Original message content',
|
||
|
headers: {
|
||
|
'List-Id': '<listname.list-server.com>',
|
||
|
'List-Post': '<mailto:mailinglist@list-server.com>',
|
||
|
'List-Unsubscribe': '<mailto:unsubscribe@list-server.com>'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
console.log('Original email:');
|
||
|
console.log(` From: ${originalEmail.from}`);
|
||
|
console.log(` To: ${originalEmail.to[0]}`);
|
||
|
|
||
|
// Mailing list forwards the email
|
||
|
const forwardedEmail = new Email({
|
||
|
from: 'original@sender-domain.com', // Kept original From
|
||
|
to: ['subscriber@recipient-domain.com'],
|
||
|
subject: '[ListName] Original Subject',
|
||
|
text: 'Original message content\n\n--\nMailing list footer',
|
||
|
envelope: {
|
||
|
from: 'bounces@list-server.com' // Changed MAIL FROM
|
||
|
},
|
||
|
headers: {
|
||
|
'List-Id': '<listname.list-server.com>',
|
||
|
'X-Original-From': 'original@sender-domain.com'
|
||
|
}
|
||
|
});
|
||
|
|
||
|
console.log('\nForwarded by mailing list:');
|
||
|
console.log(` From header: ${forwardedEmail.from} (unchanged)`);
|
||
|
console.log(` MAIL FROM: bounces@list-server.com (changed)`);
|
||
|
console.log(` Result: SPF will pass for list-server.com, but DMARC alignment fails`);
|
||
|
|
||
|
await smtpClient.sendMail(forwardedEmail);
|
||
|
|
||
|
console.log('\nSolutions for mailing lists:');
|
||
|
console.log('1. ARC (Authenticated Received Chain) - preserves authentication');
|
||
|
console.log('2. Conditional DMARC policies for known mailing lists');
|
||
|
console.log('3. From header rewriting (changes to list address)');
|
||
|
console.log('4. Encourage subscribers to whitelist the mailing list');
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CSEC-05: DMARC record lookup', async () => {
|
||
|
// Test real DMARC record lookups
|
||
|
const testDomains = ['paypal.com', 'ebay.com', 'amazon.com'];
|
||
|
|
||
|
console.log('\nReal DMARC Record Lookups:\n');
|
||
|
|
||
|
for (const domain of testDomains) {
|
||
|
const dmarcDomain = `_dmarc.${domain}`;
|
||
|
console.log(`Domain: ${domain}`);
|
||
|
|
||
|
try {
|
||
|
const txtRecords = await resolveTxt(dmarcDomain);
|
||
|
const dmarcRecords = txtRecords
|
||
|
.map(record => record.join(''))
|
||
|
.filter(record => record.startsWith('v=DMARC1'));
|
||
|
|
||
|
if (dmarcRecords.length > 0) {
|
||
|
const record = dmarcRecords[0];
|
||
|
console.log(` Record: ${record}`);
|
||
|
|
||
|
// Parse key elements
|
||
|
const policyMatch = record.match(/p=(\w+)/);
|
||
|
const ruaMatch = record.match(/rua=([^;]+)/);
|
||
|
const pctMatch = record.match(/pct=(\d+)/);
|
||
|
|
||
|
if (policyMatch) console.log(` Policy: ${policyMatch[1]}`);
|
||
|
if (ruaMatch) console.log(` Reports to: ${ruaMatch[1]}`);
|
||
|
if (pctMatch) console.log(` Percentage: ${pctMatch[1]}%`);
|
||
|
} else {
|
||
|
console.log(' No DMARC record found');
|
||
|
}
|
||
|
} catch (error) {
|
||
|
console.log(` Lookup failed: ${error.message}`);
|
||
|
}
|
||
|
console.log('');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tap.test('cleanup test SMTP server', async () => {
|
||
|
if (testServer) {
|
||
|
await testServer.stop();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
export default tap.start();
|