Add packaged DLP broker and protected Windows service foundation

This commit is contained in:
Karl committed 2026-10-14 14:19:22 +02:00
1 parent 95d23f4e0b
commit 7b5057901a
140 files changed
+11215 -60

No files matched your search

+156
View File
@@ -0,0 +1,156 @@
// Explicit Windows integration: run with the verified bundled Node executable
// and SAFETYNET_NATIVE_BROKER_INSPECTION=1. Default unit suites skip this gate.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import {createRequire} from 'node:module';
import {createHash,generateKeyPairSync} from 'node:crypto';
import {execFileSync} from 'node:child_process';
import http from 'node:http';
import https from 'node:https';
import tls from 'node:tls';
import {once} from 'node:events';
import {defaultPolicy,signPolicy,inspect} from '../packages/core/policy.mjs';
import {verifyBrokerRuntime} from '../scripts/provision-broker-runtime.mjs';
const require=createRequire(import.meta.url),{startInspector}=require('../apps/browser/inspection.cjs');
const {createManagedProxy}=require('../apps/network-broker/managed-proxy.cjs');
const {createPolicyAuthority}=require('../apps/network-broker/policy-authority.cjs');
const {createHttpsForwarder}=require('../apps/network-broker/https-forwarder.cjs');
const {createSyntheticAudit}=require('./fixtures/broker-durable-audit.cjs');
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
function pdf(text){
const stream=`BT /F1 12 Tf 72 720 Td <${Buffer.from(text).toString('hex')}> Tj ET`;
const objects=['<< /Type /Catalog /Pages 2 0 R >>','<< /Type /Pages /Kids [3 0 R] /Count 1 >>','<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>','<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',`<< /Length ${Buffer.byteLength(stream)} >>\nstream\n${stream}\nendstream`];
let output='%PDF-1.4\n';const offsets=[0];
objects.forEach((object,index)=>{offsets.push(Buffer.byteLength(output));output+=`${index+1} 0 obj\n${object}\nendobj\n`;});
const xref=Buffer.byteLength(output);output+=`xref\n0 6\n0000000000 65535 f \n${offsets.slice(1).map(offset=>String(offset).padStart(10,'0')+' 00000 n \n').join('')}trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
return Buffer.from(output);
}
function inspectorChildren(){
const script=`@(Get-CimInstance Win32_Process -Filter "ParentProcessId = ${process.pid}" | Where-Object { $_.Name -eq 'python.exe' -and $_.CommandLine -like '*apps*inspector*server.py*' } | Select-Object -ExpandProperty ProcessId) | ConvertTo-Json -Compress`;
const output=execFileSync('powershell.exe',['-NoProfile','-NonInteractive','-Command',script],{encoding:'utf8',windowsHide:true,timeout:10000}).trim();
return output?[].concat(JSON.parse(output)):[];
}
test('bundled Windows Node managed TLS broker uses real contained document inspection before dial',{
skip:process.platform!=='win32'||process.env.SAFETYNET_NATIVE_BROKER_INSPECTION!=='1',timeout:120000
},async t=>{
const root=path.resolve('.'),proofDirectory=path.join(root,'artifacts/broker-native-inspection');await fs.mkdir(proofDirectory,{recursive:true});
const proof={startedAt:new Date().toISOString(),outcome:'running',diagnosticsEnabled:false,scans:[],office:[]};
let inspector,proxy,target;const started=performance.now();
try{
const runtime=await verifyBrokerRuntime(path.join(root,'.runtime/windows/broker'));
assert.equal(path.resolve(process.execPath).toLowerCase(),path.join(root,'.runtime/windows/broker/node.exe').toLowerCase(),'Run this gate with the bundled broker Node executable');
proof.runtime={version:runtime.version,executableSha256:runtime.files.find(file=>file.path==='node.exe').sha256};
inspector=startInspector(root,false);assert.ok(inspector,'Contained inspector runtime unavailable');await inspector.waitReady();
const children=inspectorChildren();assert.equal(children.length,1,'Expected one owned contained inspector parent');proof.inspectorOwnedPids=children;
// startInspector only resolves readiness after validating Job Object plus
// zero-network-capability AppContainer/private-pipe/read-only containment.
proof.containmentStartupValidated=true;
const key=await fs.readFile(new URL('./fixtures/icap/key.pem',import.meta.url)),cert=await fs.readFile(new URL('./fixtures/icap/cert.pem',import.meta.url));
const received=[];let connections=0;
target=https.createServer({key,cert},async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);const body=Buffer.concat(chunks);received.push({sha256:hash(body),bytes:body.length});res.end('public receiver acknowledgment');});
target.on('connection',()=>connections++);target.listen(0,'127.0.0.1');await once(target,'listening');
const port=target.address().port,destination=`localhost:${port}`;
const keys=generateKeyPairSync('ed25519'),policy=defaultPolicy();policy.assignment={deviceId:'native-broker-device',groups:[]};
policy.dlp.rules=[{id:'synthetic-confidential-document',label:'Synthetic confidential',kind:'literal',value:'SYNTHETIC INTERNAL BLUEPRINT',action:'block'}];
assert.equal(policy.diagnostics.enabled,false);
const authority=await createPolicyAuthority({deviceId:'native-broker-device',publicKey:keys.publicKey.export({type:'spki',format:'pem'})});
authority.accept(signPolicy(policy,keys.privateKey.export({type:'pkcs8',format:'pem'})));
const snapshot=authority.snapshot();
const forward=createHttpsForwarder({authority:snapshot.token,current:authority.current,resolveRoute:async value=>({approved:value.hostname==='localhost'&&value.port===port,address:'127.0.0.1',port:value.port}),ca:cert,deadlineMs:45000,maxResponseBytes:Math.min(16777216,policy.fileTransfers.download.maxBytes)});
const durableAudit=createSyntheticAudit(t);
proxy=await createManagedProxy({policyAuthority:authority,authMode:'bearer',persistAudit:durableAudit.persist,resolveCertificate:binding=>binding.authority===destination?{authority:destination,key,cert}:undefined,
localInspector:async(bytes,signedPolicy)=>{const before=performance.now(),result=await inspector.inspect(bytes,signedPolicy);proof.scans.push({durationMs:performance.now()-before,decision:result.decision,complete:result.coverage.complete,contentType:result.contentType,matchedSyntheticRule:result.ruleId==='synthetic-confidential-document'});return result;},forward,deadlineMs:45000});
const proxyPort=await proxy.listen();
const send=(body,mime='application/pdf')=>new Promise((resolve,reject)=>{
const request=http.request({host:'127.0.0.1',port:proxyPort,method:'CONNECT',path:destination,headers:{host:destination,'proxy-authorization':`Bearer ${proxy.capability}`}});
request.setTimeout(55000,()=>request.destroy(new Error('native-test-deadline')));request.once('error',reject);
request.once('connect',(response,socket)=>{
if(response.statusCode!==200){socket.destroy();resolve(response.statusCode);return;}
const secure=tls.connect({socket,servername:'localhost',ca:cert,rejectUnauthorized:true});let reply='';
secure.setTimeout(55000,()=>secure.destroy(new Error('native-test-deadline')));secure.once('error',reject);
secure.once('secureConnect',()=>secure.write(Buffer.concat([Buffer.from(`POST /upload HTTP/1.1\r\nHost: ${destination}\r\nContent-Type: ${mime}\r\nContent-Length: ${body.length}\r\nConnection: close\r\n\r\n`),body])));
secure.on('data',chunk=>reply+=chunk.toString());secure.once('end',()=>{secure.destroy();resolve(Number(/^HTTP\/1.1 (\d+)/.exec(reply)?.[1]));});
});request.end();
});
const secret=pdf(policy.dlp.rules[0].value),publicDocument=pdf('Public example report');
assert.equal(inspect(secret,policy).decision,'allow','Encoded PDF must require genuine parsing, not raw literal detection');
assert.equal(await send(secret),403);assert.equal(connections,0);assert.equal(received.length,0);
proof.secret={status:403,upstreamConnections:connections,receivedBytes:0};
assert.equal(await send(publicDocument),200);assert.deepEqual(received,[{sha256:hash(publicDocument),bytes:publicDocument.length}]);
assert.ok(proof.scans.some(scan=>scan.decision==='block'&&scan.contentType==='pdf'&&scan.matchedSyntheticRule));
assert.ok(proof.scans.some(scan=>scan.decision==='allow'&&scan.complete&&scan.contentType==='pdf'));
proof.public={status:200,upstreamConnections:connections,...received[0]};
const officeRoot=path.join(root,'artifacts/office-coverage-fixtures');
// Only synthetic fixtures are modified by stdlib ZIP/XML; no classifier,
// document parser or OCR result is substituted in the actual broker gate.
const mutate=[
'import sys,zipfile,pathlib,xml.etree.ElementTree as ET',
'root=pathlib.Path(sys.argv[1]); marker=sys.argv[2]',
"for source,destination,member in [('public-generated.docx','secret-metadata.docx','docProps/core.xml'),('public-generated.xlsx','secret-hidden.xlsx','xl/worksheets/sheet2.xml')]:",
' with zipfile.ZipFile(root/source) as archive:',
' records=[(entry,archive.read(entry)) for entry in archive.infolist()]',
" if source.endswith('.xlsx'):",
" workbook=ET.fromstring(archive.read('xl/workbook.xml'))",
" sheets=[node for node in workbook.iter() if node.tag.endswith('}sheet')]",
" assert len(sheets)==2 and sheets[1].attrib.get('state')=='hidden'",
" with zipfile.ZipFile(root/destination,'w') as output:",
' changed=False',
' for entry,data in records:',
' if entry.filename==member:',
' document=ET.fromstring(data)',
" if source.endswith('.docx'):",
" title=document.find('{http://purl.org/dc/elements/1.1/}title')",
" if title is None: title=ET.SubElement(document,'{http://purl.org/dc/elements/1.1/}title')",
' title.text=marker',
' else:',
" namespace='{http://schemas.openxmlformats.org/spreadsheetml/2006/main}'",
" sheet=document.find(namespace+'sheetData')",
" if sheet is None: raise RuntimeError('Synthetic sheet data absent')",
" row=ET.SubElement(sheet,namespace+'row',{'r':'1'})",
" cell=ET.SubElement(row,namespace+'c',{'r':'A1','t':'inlineStr'})",
" value=ET.SubElement(ET.SubElement(cell,namespace+'is'),namespace+'t'); value.text=marker",
" data=ET.tostring(document,encoding='utf-8',xml_declaration=True);changed=True",
' output.writestr(entry,data)',
" if not changed: raise RuntimeError('Synthetic fixture target part absent')",
''
].join('\n');
execFileSync(path.join(root,'.runtime/windows/python/python.exe'),['-c',mutate,officeRoot,policy.dlp.rules[0].value],{windowsHide:true,stdio:'pipe',timeout:10000});
const expectedPublic=[received[0]];
for(const [label,name,mime] of [
['docx','public-generated.docx','application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
['xlsx','public-generated.xlsx','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']]){
const bytes=await fs.readFile(path.join(officeRoot,name)),firstScan=proof.scans.length;
assert.equal(await send(bytes,mime),200);
const observed=proof.scans.slice(firstScan);assert.ok(observed.some(scan=>scan.contentType==='office'&&scan.decision==='allow'&&scan.complete));
expectedPublic.push({sha256:hash(bytes),bytes:bytes.length});proof.office.push({case:label+'-public',status:200,complete:true,sha256:hash(bytes),bytes:bytes.length});
}
for(const [label,name,mime] of [
['docx-metadata','secret-metadata.docx','application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
['xlsx-hidden-sheet','secret-hidden.xlsx','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']]){
const before=connections,firstScan=proof.scans.length,bytes=await fs.readFile(path.join(officeRoot,name));
assert.equal(await send(bytes,mime),403);assert.equal(connections,before);
assert.ok(proof.scans.slice(firstScan).some(scan=>scan.contentType==='office'&&scan.decision==='block'&&scan.matchedSyntheticRule));
proof.office.push({case:label,status:403,matchedSyntheticRule:true,newUpstreamConnections:0});
}
const complex=await fs.readFile(path.join(officeRoot,'public-generated.pptx')),beforeComplex=connections,firstComplexScan=proof.scans.length;
assert.equal(await send(complex,'application/vnd.openxmlformats-officedocument.presentationml.presentation'),403);assert.equal(connections,beforeComplex);
assert.ok(proof.scans.slice(firstComplexScan).some(scan=>scan.contentType==='office'&&scan.decision==='block'&&!scan.complete));
proof.office.push({case:'unsupported-complex-pptx',status:403,complete:false,newUpstreamConnections:0});
assert.deepEqual(received,expectedPublic);
const auditEvents=durableAudit.events();
assert.equal(auditEvents.filter(event=>event.kind==='upload.authorized').length,3);
assert.equal(auditEvents.filter(event=>event.kind==='upload.block'&&event.ruleId==='synthetic-confidential-document').length,3);
assert.equal(auditEvents.length,7);
proof.audit={persistedEvents:auditEvents.length,authorizedEvents:3,matchedSecretBlocks:3,syntheticMetadataFsynced:true};
proof.outcome='passed';
}catch(error){proof.outcome='failed';proof.errorType=error.name;throw error;}
finally{
if(proxy)await proxy.close();if(target){target.closeAllConnections();await new Promise(resolve=>target.close(resolve));}
if(inspector){inspector.stop();const deadline=Date.now()+10000;while(inspectorChildren().length&&Date.now()<deadline)await new Promise(resolve=>setTimeout(resolve,100));proof.ownedInspectorParentsRemaining=inspectorChildren().length;}
proof.durationMs=performance.now()-started;await fs.writeFile(path.join(proofDirectory,`proof-${proof.startedAt.replaceAll(':','-')}.json`),JSON.stringify(proof,null,2)+'\n');
await fs.writeFile(path.join(proofDirectory,`office-proof-${proof.startedAt.replaceAll(':','-')}.json`),JSON.stringify({startedAt:proof.startedAt,outcome:proof.outcome,diagnosticsEnabled:false,runtime:proof.runtime,office:proof.office,scans:proof.scans.filter(scan=>scan.contentType==='office'),audit:proof.audit,ownedInspectorParentsRemaining:proof.ownedInspectorParentsRemaining},null,2)+'\n');
assert.equal(proof.ownedInspectorParentsRemaining??0,0,'Owned inspector parent did not terminate');
}
});