'use strict'; const {createHash}=require('node:crypto'); const path=require('node:path'); const TYPES=['pe','elf','macho','pdf','zip','ole','png','jpeg','gif','webp','gzip','text','unknown']; const EXT={exe:'pe',dll:'pe',sys:'pe',elf:'elf',pdf:'pdf',zip:'zip',docx:'zip',xlsx:'zip',pptx:'zip',docm:'zip',xlsm:'zip',xlsb:'zip',pptm:'zip',dotm:'zip',xlam:'zip',ppam:'zip',doc:'ole',xls:'ole',ppt:'ole',png:'png',jpg:'jpeg',jpeg:'jpeg',gif:'gif',webp:'webp',gz:'gzip',gzip:'gzip',txt:'text',csv:'text',json:'text',html:'text',js:'text',css:'text',xml:'text',md:'text'}; const MIME={'application/msword':'ole','application/vnd.ms-excel':'ole','application/vnd.ms-powerpoint':'ole','application/vnd.ms-word.document.macroenabled.12':'zip','application/vnd.ms-excel.sheet.macroenabled.12':'zip','application/vnd.ms-powerpoint.presentation.macroenabled.12':'zip','application/pdf':'pdf','application/zip':'zip','application/x-zip-compressed':'zip','application/vnd.openxmlformats-officedocument.wordprocessingml.document':'zip','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':'zip','application/vnd.openxmlformats-officedocument.presentationml.presentation':'zip','image/png':'png','image/jpeg':'jpeg','image/gif':'gif','image/webp':'webp','application/gzip':'gzip','application/x-gzip':'gzip','application/json':'text','application/xml':'text','application/javascript':'text','application/x-msdownload':'pe','application/vnd.microsoft.portable-executable':'pe'}; // eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input. function safeFilename(value){const name=path.win32.basename(path.posix.basename(String(value||'download.bin'))).replace(/[<>:"/\\|?*\x00-\x1f\x7f]/g,'_').replace(/[. ]+$/g,'').slice(0,180);return !name||/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name)?'download.bin':name;} function detectType(value){ const b=Buffer.from(value);const starts=(hex)=>b.length>=hex.length/2&&b.subarray(0,hex.length/2).equals(Buffer.from(hex,'hex')); // MZ is blocked as PE-family even if the header is corrupt or deliberately truncated. if(starts('4d5a'))return 'pe';if(starts('7f454c46'))return 'elf'; if(['feedface','cefaedfe','feedfacf','cffaedfe','cafebabe','bebafeca','cafebabf','bfbafeca'].some(starts))return 'macho'; if(starts('d0cf11e0a1b11ae1'))return 'ole';if(starts('255044462d'))return 'pdf';if(['504b0304','504b0506','504b0708'].some(starts))return 'zip'; if(starts('89504e470d0a1a0a'))return 'png';if(starts('ffd8ff'))return 'jpeg'; if(b.subarray(0,6).toString('ascii')==='GIF87a'||b.subarray(0,6).toString('ascii')==='GIF89a')return 'gif'; if(b.length>=12&&b.subarray(0,4).toString('ascii')==='RIFF'&&b.subarray(8,12).toString('ascii')==='WEBP')return 'webp';if(starts('1f8b'))return 'gzip'; // Decode the entire bounded payload. Prefix-only heuristics miss binary tails. // eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input. try{const text=new TextDecoder('utf-8',{fatal:true}).decode(b);if(!/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(text))return 'text';}catch{} return 'unknown'; } function inspectFileTransfer(bytes,policy,direction,metadata={}){ if(!['upload','download'].includes(direction))throw Error('Unsupported transfer direction'); const settings=policy.fileTransfers?.[direction]||{};const name=safeFilename(metadata.filename),mime=String(metadata.mime||'').split(';',1)[0].trim().toLowerCase().slice(0,160); const detectedType=detectType(bytes),extensionType=EXT[path.extname(name).slice(1).toLowerCase()]||null,mimeType=mime.startsWith('text/')?'text':MIME[mime]||null; const facts={filename:name,mimeclaimed:mime,detectedType,size:bytes.length,sha256:createHash('sha256').update(bytes).digest('hex'),extensionMismatch:!!extensionType&&extensionType!==detectedType,mimeMismatch:!!mimeType&&mimeType!==detectedType}; let ruleId=null;if(bytes.length>(settings.maxBytes??20*1024*1024))ruleId='file-transfer-size-limit';else if(settings.blockedTypes?.includes(detectedType))ruleId='file-transfer-type-blocked';else if(settings.allowTypes?.length&&!settings.allowTypes.includes(detectedType))ruleId='file-transfer-type-not-allowed';else if(settings.blockUnknown&&detectedType==='unknown')ruleId='file-transfer-unknown';else if(settings.blockExtensionMismatch&&facts.extensionMismatch)ruleId='file-transfer-extension-mismatch';else if(settings.blockMimeMismatch&&facts.mimeMismatch)ruleId='file-transfer-mime-mismatch'; return {decision:ruleId?'block':'allow',ruleId,...facts}; } module.exports={TYPES,safeFilename,detectType,inspectFileTransfer};