update
This commit is contained in:
@ -1,16 +1,20 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
||||
import { createTestSmtpClient } 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;
|
||||
let testServer: ITestServer;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestSmtpServer();
|
||||
testServer = await startTestServer({
|
||||
port: 2565,
|
||||
tlsEnabled: false,
|
||||
authRequired: false
|
||||
});
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
@ -32,11 +36,6 @@ tap.test('CSEC-05: DMARC record parsing', async () => {
|
||||
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'
|
||||
}
|
||||
];
|
||||
|
||||
@ -50,29 +49,14 @@ tap.test('CSEC-05: DMARC record parsing', async () => {
|
||||
// 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(`Tags found: ${tags.length}`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CSEC-05: DMARC alignment testing', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
const smtpClient = createTestSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
@ -80,89 +64,35 @@ tap.test('CSEC-05: DMARC alignment testing', async () => {
|
||||
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',
|
||||
name: 'Different domain',
|
||||
fromHeader: 'sender@otherdomain.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'
|
||||
}
|
||||
text: 'Testing DMARC alignment'
|
||||
});
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result.success).toBeTruthy();
|
||||
|
||||
// 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(` Email sent successfully`);
|
||||
console.log(` Expected result: ${test.expectedResult}`);
|
||||
}
|
||||
|
||||
@ -197,211 +127,6 @@ tap.test('CSEC-05: DMARC policy enforcement', async () => {
|
||||
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 () => {
|
||||
@ -410,31 +135,16 @@ tap.test('CSEC-05: DMARC deployment best practices', async () => {
|
||||
{
|
||||
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',
|
||||
policy: 'p=reject; rua=mailto:dmarc@example.com',
|
||||
description: 'Reject all failing messages'
|
||||
}
|
||||
];
|
||||
@ -444,90 +154,13 @@ tap.test('CSEC-05: DMARC deployment best practices', async () => {
|
||||
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'];
|
||||
const testDomains = ['paypal.com'];
|
||||
|
||||
console.log('\nReal DMARC Record Lookups:\n');
|
||||
|
||||
@ -543,16 +176,11 @@ tap.test('CSEC-05: DMARC record lookup', async () => {
|
||||
|
||||
if (dmarcRecords.length > 0) {
|
||||
const record = dmarcRecords[0];
|
||||
console.log(` Record: ${record}`);
|
||||
console.log(` Record found: ${record.substring(0, 50)}...`);
|
||||
|
||||
// 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');
|
||||
}
|
||||
@ -565,8 +193,8 @@ tap.test('CSEC-05: DMARC record lookup', async () => {
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
await stopTestServer(testServer);
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
tap.start();
|
Reference in New Issue
Block a user