dcrouter/test/suite/smtpclient_security/test.csec-05.dmarc-policy.ts

572 lines
16 KiB
TypeScript
Raw Normal View History

2025-05-24 16:19:19 +00:00
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();