'use strict'; const {createHash}=require('node:crypto'); const {createConnectProxy}=require('./connect-inspection-proxy.cjs'); const {GateError}=require('./http-request-gate.cjs'); const {inspectFileTransfer,detectType}=require('../../packages/core/file-transfer.cjs'); const {createBrokerAudit}=require('./audit-gate.cjs'); const {createBrokerLifecycle}=require('./lifecycle-audit.cjs'); const {icapRequired}=require('../browser/enterprise-inspection.cjs'); const {inspectTextRepresentations}=require('../browser/inspect-text-representations.cjs'); const {snapshotHeaders,inspectHeaders}=require('../browser/header-inspection.cjs'); const {parseMultipart}=require('./multipart.cjs'); const {extractEncodedDocuments}=require('../../packages/core/encoded-documents.cjs'); const hash=bytes=>createHash('sha256').update(bytes).digest('hex'); /** Signed policy foundation for an isolated inspection broker. Trusted * callbacks supply scoped certificates, real local/enterprise scanners and * upstream transport. No classifier substitutes, enrollment or main wiring. * A policy update invalidates this snapshot: integration must replace the * listener with a newly authorized snapshot after protected publication. */ async function createManagedProxy({policyAuthority,snapshot=policyAuthority?.snapshot(),resolveCertificate,localInspector,enterpriseInspector,forward,persistAudit,auditCorrelation,deadlineMs=5000,maxClients=8,authMode='basic'}) { if(!snapshot||!policyAuthority||typeof policyAuthority.current!=='function'||!policyAuthority.current(snapshot.token)||policyAuthority.snapshot()!==snapshot||typeof resolveCertificate!=='function'||typeof forward!=='function')throw new GateError('managed-authority-invalid'); if(typeof persistAudit!=='function')throw new GateError('managed-audit-unavailable'); const core=await import('../../packages/core/policy.mjs'); const p=snapshot.policy,current=token=>token===snapshot.token&&policyAuthority.current(token); const check=signal=>{if(signal?.aborted)throw new GateError('aborted');if(!current(snapshot.token))throw new GateError('authority-expired');}; check(); const registeredIds=[...p.dlp.rules.map(rule=>rule.id),...p.dlp.semantic.categories.map(category=>category.id),...p.security.yara.rules.map(rule=>rule.id)]; const knownIds=new Set([...registeredIds,'private-key','aws-access-key','api-secret','content-blocklist','size-limit','uninspectable','normalization-limit','file-transfer-size-limit','file-transfer-type-blocked','file-transfer-type-not-allowed','file-transfer-unknown','file-transfer-extension-mismatch','file-transfer-mime-mismatch','inspection-incomplete']); const audit=createBrokerAudit({persist:persistAudit,current:()=>current(snapshot.token),policyVersion:p.version,allowedRuleIds:registeredIds,correlation:auditCorrelation}); const lifecycle=createBrokerLifecycle(audit); const destinationAllowed=url=>core.destinationAllowed(url,p)&&core.inspect(Buffer.from(url),p).decision==='allow'; const blocked=ruleId=>({decision:'block',ruleId}); const scanner=async(fn,bytes,signal,enterprise=false)=>{ const copy=Buffer.from(bytes),digest=hash(copy); try { check(signal);const result=await fn(copy,p,{signal});check(signal); if(hash(copy)!==digest)return blocked('inspection-incomplete'); if(result?.decision==='block')return blocked(knownIds.has(result.ruleId)?result.ruleId:'inspection-incomplete'); if(result?.decision!=='allow'||result.coverage?.complete!==true||enterprise&&(result.sha256!==digest||result.bytes!==copy.length))return blocked('inspection-incomplete'); return result; } finally {copy.fill(0);} }; const textScan=(bytes,signal)=>inspectTextRepresentations({bytes,policy:p,isCurrent:()=>current(snapshot.token), inspectLocal:typeof localInspector==='function'?value=>scanner(localInspector,value,signal):undefined, inspectEnterprise:icapRequired(p,'upload')&&typeof enterpriseInspector==='function'?value=>scanner(enterpriseInspector,value,signal,true):undefined}); const textValue=async(value,signal,budget)=>{const bytes=Buffer.from(value);try{return await inspectContent(bytes,signal,undefined,budget);}finally{bytes.fill(0);}}; const inspectContent=async(bytes,signal,metadata,budget,depth=0)=>{ check(signal);const transfer=inspectFileTransfer(bytes,p,'upload',metadata); if(transfer.decision!=='allow')return blocked(transfer.ruleId); const verdict=core.inspect(bytes,p); if(verdict.decision!=='allow'&&verdict.ruleId!=='uninspectable')return verdict; if(transfer.detectedType==='text'){ if(/^image\/svg\+xml(?:;|$)/i.test(metadata?.mime||'')||/)/i.test(bytes.toString('utf8')))return blocked('inspection-incomplete'); if(!budget||budget.bytes<1||budget.nodes<1||budget.parts<1)return blocked('inspection-incomplete'); const decoded=extractEncodedDocuments(bytes,{jsonRequired:/^(?:application\/(?:json|[a-z0-9.+-]+\+json))(?:;|$)/i.test(metadata?.mime||''),maxDecodedBytes:budget.bytes,maxNodes:budget.nodes,maxParts:budget.parts,maxDepth:32}); try{ budget.bytes-=decoded.decodedBytes;budget.nodes-=decoded.visitedNodes;budget.parts-=decoded.parts.length; if(decoded.complete!==true||budget.bytes<0||budget.nodes<0||budget.parts<0||depth>=4&&decoded.parts.length)return blocked('inspection-incomplete'); for(const part of decoded.parts){const result=await inspectContent(part.bytes,signal,{mime:part.mime},budget,depth+1);if(result.decision!=='allow')return result;} check(signal);return textScan(bytes,signal); }finally{decoded.dispose();} } if(typeof localInspector!=='function')return blocked('inspector-unavailable'); const local=await scanner(localInspector,bytes,signal);if(local.decision!=='allow')return local; if(icapRequired(p,'upload')){const enterprise=await scanner(enterpriseInspector,bytes,signal,true);if(enterprise.decision!=='allow')return enterprise;} check(signal);return {decision:'allow'}; }; return createConnectProxy({authority:snapshot.token,current,authMode,lifecycle,deadlineMs,maxClients,maxBytes:p.dlp.maxBytes,maxResponseBytes:Math.min(16777216,p.fileTransfers.download.maxBytes), resolveCertificate:async(binding,options)=>{ check();if(!destinationAllowed(`https://${binding.authority}/`))throw new GateError('destination-denied'); const result=await resolveCertificate(binding,options);check();return result; }, inspect:async({request,body,signal,auditContext})=>{ const classify=async()=>{ const budget={bytes:Math.min(p.dlp.maxBytes,16777216),nodes:20000,parts:64}; check();if(!destinationAllowed(request.url))return blocked('destination-denied'); if(icapRequired(p,'upload')&&typeof enterpriseInspector!=='function')return blocked('enterprise-unavailable'); const urlVerdict=await textValue(request.url,signal,budget);if(urlVerdict.decision!=='allow')return urlVerdict; const headers=snapshotHeaders(request.headers,p); const headerVerdict=await inspectHeaders(headers,{policy:p,inspect:core.inspect,isCurrent:()=>current(snapshot.token), inspectLocal:typeof localInspector==='function'?bytes=>scanner(localInspector,bytes,signal):undefined, inspectEnterprise:icapRequired(p,'upload')?bytes=>scanner(enterpriseInspector,bytes,signal,true):undefined}); if(headerVerdict.decision!=='allow')return headerVerdict; for(const value of headers.values){const verdict=await textValue(value,signal,budget);if(verdict.decision!=='allow')return verdict;} if(/^multipart\//i.test(request.headers['content-type']||'')){ const raw=core.inspect(body,p);if(raw.decision!=='allow'&&raw.ruleId!=='uninspectable')return raw; const parsed=parseMultipart(body,request.headers['content-type'],{maxBytes:p.dlp.maxBytes}); try { if(parsed.coverage?.complete!==true)return blocked('multipart-incomplete'); for(const part of parsed.parts){ for(const value of [part.name,part.filename,part.contentType].filter(value=>value!==undefined)){ const verdict=core.inspect(Buffer.from(value),p);if(verdict.decision!=='allow')return verdict; const representation=await textValue(value,signal,budget);if(representation.decision!=='allow')return representation; } const verdict=await inspectContent(part.body,signal,{filename:part.filename,mime:part.contentType},budget);if(verdict.decision!=='allow')return verdict; } check(signal);return {decision:'allow'}; } finally {parsed.dispose();} } return inspectContent(body,signal,{mime:request.headers['content-type']},budget); }; const facts={bytes:body.length,detectedType:detectType(body),host:new URL(request.url).hostname.replace(/^\[|\]$/g,'')}; let verdict; try {verdict=await classify();} catch { if(signal.aborted)throw new GateError('aborted'); await auditContext.decision(current(snapshot.token)?'fault':'block',{...facts,reason:current(snapshot.token)?'broker-inspection-fault':'broker-authority-expired'}); throw new GateError('inspection-failed'); } // This awaits a durable receipt while the original inspection job still // owns global admission. Outer cancellation cannot free pending audit. await auditContext.decision(verdict.decision==='allow'?'allow':'block',{...facts,...(verdict.decision!=='allow'&&knownIds.has(verdict.ruleId)?{matchedRuleId:verdict.ruleId}:{})}); check(signal);return verdict; }, forward:value=>{check();return forward(value);} }); } module.exports={createManagedProxy};