Add packaged DLP broker and protected Windows service foundation
This commit is contained in:
1 parent
95d23f4e0b
commit
7b5057901a
140 files changed
+11215
-60
No files matched your search
@@ -0,0 +1,119 @@
|
||||
'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||'')||/<svg(?:\s|>)/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};
|
||||
Reference in new issue
Block a user