47 lines
4.7 KiB
JavaScript
47 lines
4.7 KiB
JavaScript
'use strict';
|
|
const {createHash}=require('node:crypto');
|
|
const {GateError}=require('./http-request-gate.cjs');
|
|
const {validateResponse}=require('./http-response-gate.cjs');
|
|
const {detectType,inspectFileTransfer}=require('../../packages/core/file-transfer.cjs');
|
|
const {scanDownloadEnterprise}=require('../browser/enterprise-inspection.cjs');
|
|
const digest=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
function filename(value,url){
|
|
if(value===undefined){try{return decodeURIComponent(new URL(url).pathname.split('/').pop());}catch{throw new GateError('invalid-upstream-response');}}
|
|
if(typeof value!=='string'||value.length>8192||!/^\s*(attachment|inline)(?:\s*;|\s*$)/i.test(value))throw new GateError('invalid-upstream-response');
|
|
const matches=[...value.matchAll(/;\s*filename(\*)?\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^;\s]+))/gi)];
|
|
if(matches.length>1)throw new GateError('invalid-upstream-response');if(!matches.length)return '';
|
|
const match=matches[0],text=match[2]!==undefined?match[2].replace(/\\(.)/g,'$1'):match[3];
|
|
if(match[1]){if(!/^UTF-8''/i.test(text))throw new GateError('invalid-upstream-response');try{return decodeURIComponent(text.slice(7));}catch{throw new GateError('invalid-upstream-response');}}return text;
|
|
}
|
|
/** A full buffered response is held until document/download analysis succeeds.
|
|
* Incoming HTML/API text is not classified as outgoing company data. Partial
|
|
* and compressed representations are unsupported, never counted as covered.
|
|
*/
|
|
function createDownloadResponseGate({forward,policy,current,inspector,enterpriseScanner,maxBytes=16777216,auditRequired=true}={}){
|
|
if(typeof auditRequired!=='boolean'||typeof forward!=='function'||typeof current!=='function'||!policy||!Number.isSafeInteger(maxBytes)||maxBytes<0||maxBytes>16777216)throw new GateError('invalid-configuration');
|
|
const check=signal=>{if(signal?.aborted)throw new GateError('aborted');if(current()!==true)throw new GateError('authority-expired');};
|
|
return async input=>{
|
|
check(input.signal);if(auditRequired&&typeof input.auditContext?.response!=='function')throw new GateError('audit-unavailable');const upstream=await forward(input);check(input.signal);
|
|
const response=validateResponse(upstream,maxBytes);let scanCopy;
|
|
const known=new Set([...(policy.dlp?.rules||[]).map(rule=>rule.id),...(policy.dlp?.semantic?.categories||[]).map(rule=>rule.id),...(policy.security?.yara?.rules||[]).map(rule=>rule.id)]);
|
|
const record=async(decision,reason,ruleId)=>{if(input.auditContext?.response)await input.auditContext.response(decision,{bytes:response.body.length,detectedType:detectType(response.body),sha256:digest(response.body),host:new URL(input.request.url).hostname.replace(/^\[|\]$/g,''),reason,...(known.has(ruleId)?{matchedRuleId:ruleId}:{})});else if(auditRequired)throw new GateError('audit-unavailable');check(input.signal);};
|
|
try{
|
|
if(response.statusCode===206)throw new GateError('invalid-upstream-response');
|
|
const disposition=response.headers['content-disposition'],type=detectType(response.body),mime=response.headers['content-type'];
|
|
const claimed=filename(disposition,input.request.url);
|
|
const document=type!=='text'||disposition!==undefined||/^(?:application\/(?:pdf|zip|octet-stream|msword|vnd\.)|image\/)/i.test(mime||'');
|
|
if(!document)return response;
|
|
if(/^image\/svg\+xml(?:;|$)/i.test(mime||'')){await record('block','broker-download-inspection-block');throw new GateError('download-inspection-blocked');}
|
|
if(/^\s*attachment(?:\s*;|\s*$)/i.test(disposition||'')&&policy.browser.allowDownloads!==true){await record('block','broker-download-policy-block');throw new GateError('download-policy-blocked');}
|
|
const verdict=inspectFileTransfer(response.body,policy,'download',{filename:claimed,mime});if(verdict.decision!=='allow'){await record('block','broker-download-policy-block',verdict.ruleId);throw new GateError('download-policy-blocked');}
|
|
input.markStage?.('scan');scanCopy=Buffer.from(response.body);const originalDigest=digest(scanCopy);
|
|
const result=await scanDownloadEnterprise(scanCopy,policy,inspector,enterpriseScanner);check(input.signal);
|
|
if(digest(scanCopy)!==originalDigest||result?.decision!=='allow'||result.coverage?.complete!==true||result.sha256!==originalDigest||result.bytes!==scanCopy.length){await record('block','broker-download-inspection-block',digest(scanCopy)===originalDigest?result?.ruleId:undefined);throw new GateError('download-inspection-blocked');}
|
|
input.markStage?.('forward');await record('allow','broker-download-approved');
|
|
return response;
|
|
}catch(error){response.body.fill(0);if(error instanceof GateError)throw error;throw new GateError('download-inspection-blocked');}
|
|
finally{scanCopy?.fill(0);}
|
|
};
|
|
}
|
|
module.exports={createDownloadResponseGate};
|