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,40 @@
|
||||
'use strict';
|
||||
const {BrokerAuditEventSchema}=require('../../packages/core/broker-audit.cjs');
|
||||
// Parent-only adapter. persistQueue must synchronously encrypt, write, fsync and
|
||||
// commit the complete queue, return undefined on success and throw on failure.
|
||||
// An OTLP send, boolean result or asynchronous enqueue is insufficient.
|
||||
// Bind current() to the authenticated child and the accepted policy generation.
|
||||
function createBrokerAuditPersistence({current,policyVersion,getQueue,persistQueue,onFailure}={}){
|
||||
if(![current,getQueue,persistQueue,onFailure].every(fn=>typeof fn==='function')||!Number.isSafeInteger(policyVersion)||policyVersion<1)throw Error('broker-audit-parent-invalid');
|
||||
let failed=false;
|
||||
const fail=()=>{const first=!failed;failed=true;if(first)try{onFailure();}catch{}throw Error('broker-audit-parent-unavailable');};
|
||||
const active=()=>{try{return !failed&¤t()===true;}catch{return false;}};
|
||||
return Object.freeze({
|
||||
persist(input){
|
||||
if(!active())return fail();
|
||||
let event,queue;
|
||||
try{
|
||||
let nodes=0;
|
||||
const plain=(value,depth=0)=>{
|
||||
if(++nodes>128||depth>6)return false;
|
||||
if(!value||typeof value!=='object')return true;
|
||||
if(![Object.prototype,null].includes(Object.getPrototypeOf(value)))return false;
|
||||
return Object.values(Object.getOwnPropertyDescriptors(value)).every(item=>'value'in item&&plain(item.value,depth+1));
|
||||
};
|
||||
if(!input||typeof input!=='object'||!plain(input))return fail();
|
||||
event=BrokerAuditEventSchema.parse(input);queue=getQueue();
|
||||
if(event.policyVersion!==policyVersion||!Array.isArray(queue)||queue.length>=10000||queue.some(item=>item.eventId===event.eventId))return fail();
|
||||
Object.freeze(event.request);if(event.file)Object.freeze(event.file);if(event.broker)Object.freeze(event.broker);Object.freeze(event);
|
||||
queue.push(event);
|
||||
const result=persistQueue();
|
||||
// Fail closed for an accidentally asynchronous writer; suppress its rejected
|
||||
// promise without issuing a receipt or retrying uncertain persistence.
|
||||
if(result&&typeof result.then==='function'){Promise.resolve(result).catch(()=>{});return fail();}
|
||||
if(result!==undefined)return fail();
|
||||
if(!active())return fail();
|
||||
return Object.freeze({persisted:true,eventId:event.eventId});
|
||||
}catch{return fail();}
|
||||
}
|
||||
});
|
||||
}
|
||||
module.exports={createBrokerAuditPersistence};
|
||||
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
const {X509Certificate}=require('node:crypto');
|
||||
const {isIP}=require('node:net');
|
||||
const fail=()=>{throw Error('broker-certificate-pin-rejected');};
|
||||
function authorityFacts(value){
|
||||
if(typeof value!=='string'||value.length>320||/[^\x21-\x7e]|[@/\\?#%]/.test(value))fail();
|
||||
const match=/^(\[[^\]]+\]|[^:]+):([1-9][0-9]{0,4})$/.exec(value);if(!match||Number(match[2])>65535)fail();
|
||||
let url;try{url=new URL('https://'+value+'/');}catch{fail();}
|
||||
const hostname=url.hostname.startsWith('[')?url.hostname.slice(1,-1):url.hostname;
|
||||
if(!isIP(hostname)&&hostname.split('.').some(label=>!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))fail();
|
||||
if(`${isIP(hostname)===6?'['+hostname+']':hostname}:${Number(match[2])}`!==value)fail();
|
||||
return Object.freeze({hostname,authority:value});
|
||||
}
|
||||
/** For one managed Electron session only. Electron's verifier has NO port and
|
||||
* caches results: CONNECT/SNI/inner-Host gates must still bind the port. Revoke
|
||||
* must also close existing connections/stop the old broker before replacement.
|
||||
* Management sessions retain the platform verifier and receive no such pins.
|
||||
*/
|
||||
function createBrokerCertificatePins({generation,policyVersion,current,authorizedAuthority,maxPins=128}={}){
|
||||
if(typeof generation!=='string'||!generation||!Number.isSafeInteger(policyVersion)||policyVersion<1||typeof current!=='function'||typeof authorizedAuthority!=='function'||!Number.isInteger(maxPins)||maxPins<1||maxPins>1024)fail();
|
||||
let revoked=false;const pins=new Map();
|
||||
const active=()=>{try{return !revoked&¤t()===true;}catch{return false;}};
|
||||
const authorized=authority=>{try{return authorizedAuthority(authority)===true;}catch{return false;}};
|
||||
return Object.freeze({
|
||||
accept(input){
|
||||
if(!active()||!input||input.generation!==generation||input.policyVersion!==policyVersion||typeof input.cert!=='string'||Buffer.byteLength(input.cert)>16384||!/^-----BEGIN CERTIFICATE-----\r?\n[A-Za-z0-9+/=\r\n]+\r?\n-----END CERTIFICATE-----\s*$/.test(input.cert))fail();
|
||||
const facts=authorityFacts(input.authority);if(!authorized(facts.authority))fail();
|
||||
let certificate;try{certificate=new X509Certificate(input.cert);}catch{fail();}
|
||||
const now=Date.now(),until=Date.parse(certificate.validTo),from=Date.parse(certificate.validFrom);
|
||||
const matches=isIP(facts.hostname)?certificate.checkIP(facts.hostname):certificate.checkHost(facts.hostname,{subject:'never',wildcards:false});
|
||||
if(certificate.ca||!certificate.subjectAltName||certificate.subjectAltName.includes(',')||!matches||!certificate.verify(certificate.publicKey)||!Number.isFinite(until)||!Number.isFinite(from)||from>now||until<=now||until>now+3601000||input.fingerprint256!==certificate.fingerprint256)fail();
|
||||
for(const [key,pin]of pins)if(pin.expiresAt<=now)pins.delete(key);
|
||||
const key=facts.authority;
|
||||
if(!pins.has(key)&&pins.size>=maxPins)fail();
|
||||
if(!active()||!authorized(key))fail();
|
||||
pins.set(key,Object.freeze({...facts,fingerprint:certificate.fingerprint256,expiresAt:until}));return true;
|
||||
},
|
||||
verify(request,callback){
|
||||
let result=-3;
|
||||
try{
|
||||
// Electron exposes Certificate.data/fingerprint, not Node's fingerprint256.
|
||||
// Derive our fingerprint from the actual presented PEM, never a claim.
|
||||
const data=request?.certificate?.data,host=request?.hostname;
|
||||
if(typeof data!=='string'||Buffer.byteLength(data)>16384)throw Error('certificate-data-unavailable');
|
||||
const fingerprint=new X509Certificate(data).fingerprint256;
|
||||
const match=[...pins.values()].find(pin=>pin.hostname===host&&pin.fingerprint===fingerprint);
|
||||
if(match)result=active()&&match.expiresAt>Date.now()&&authorized(match.authority)?0:-2;
|
||||
}catch{result=-3;}
|
||||
callback(result);
|
||||
},
|
||||
revoke(){revoked=true;},
|
||||
status(){return Object.freeze({active:active(),pins:pins.size});}
|
||||
});
|
||||
}
|
||||
module.exports={createBrokerCertificatePins,authorityFacts};
|
||||
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
const path=require('node:path');
|
||||
const {spawn}=require('node:child_process');
|
||||
const {EventEmitter}=require('node:events');
|
||||
const {createBrokerSupervisor}=require('./broker-supervisor.cjs');
|
||||
const {createFramedChannel}=require('../network-broker/framed-channel.cjs');
|
||||
// Privileged browser construction only. Installation verification covers the
|
||||
// fixed native client as well as the signed broker payload. No renderer-supplied
|
||||
// command, path, pipe name, environment or identity enters this transport.
|
||||
function createServiceBrokerSupervisor({installationRoot,verifyInstallation,verifyPackage,persistAudit,acceptCertificate,onQuarantine,launch=spawn,deadlineMs,shutdownMs,traceOperation}={}){
|
||||
if(!path.isAbsolute(installationRoot||'')||typeof verifyInstallation!=='function'||typeof launch!=='function'||typeof verifyPackage!=='function')throw Error('broker-supervisor-invalid');
|
||||
const root=path.resolve(installationRoot),resourceRoot=path.join(root,'resources','broker');
|
||||
function connect(){
|
||||
const env={};for(const name of ['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','LOCALAPPDATA','APPDATA'])if(process.env[name])env[name]=process.env[name];
|
||||
// --client-stdio authenticates the retained actual browser parent and the
|
||||
// connected service pipe handle in native code, before forwarding frames.
|
||||
const child=launch(path.join(root,'safetynet-service-host.exe'),['--client-stdio'],{cwd:root,env,windowsHide:true,shell:false,stdio:['pipe','pipe','ignore']});
|
||||
if(!child||!child.stdin||!child.stdout||typeof child.kill!=='function')throw Error('broker-supervisor-unavailable');
|
||||
const transport=new EventEmitter();
|
||||
const channel=createFramedChannel({readable:child.stdout,writable:child.stdin,inboundFirstLimit:65536,outboundFirstLimit:2097152});
|
||||
Object.defineProperties(transport,{exitCode:{get:()=>child.exitCode},signalCode:{get:()=>child.signalCode}});
|
||||
channel.on('message',message=>transport.emit('message',message));
|
||||
channel.once('disconnect',()=>transport.emit('disconnect'));
|
||||
child.once('error',()=>{channel.disconnect();transport.emit('error',Error('broker-supervisor-unavailable'));});
|
||||
child.once('exit',(code,signal)=>{channel.disconnect();transport.emit('exit',code,signal);});
|
||||
transport.send=(message,callback)=>channel.send(message,callback);
|
||||
// Only the retained child handle is terminated. The service owns the broker
|
||||
// Job; losing this connection causes its generation to close independently.
|
||||
transport.kill=signal=>{channel.disconnect();return child.kill(signal);};
|
||||
return transport;
|
||||
}
|
||||
return createBrokerSupervisor({resourceRoot,verifyPackage:async location=>await verifyInstallation(root)===true&&await verifyPackage(location)===true,persistAudit,acceptCertificate,onQuarantine,spawn:connect,deadlineMs,shutdownMs,traceOperation});
|
||||
}
|
||||
module.exports={createServiceBrokerSupervisor};
|
||||
@@ -0,0 +1,111 @@
|
||||
'use strict';
|
||||
const {fork}=require('node:child_process');
|
||||
const path=require('node:path');
|
||||
const {randomUUID,X509Certificate}=require('node:crypto');
|
||||
const UUID=/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
||||
const ENTRY='apps/network-broker/production-entry.cjs';
|
||||
const STARTUP_CODES=new Set(['broker-bootstrap-invalid','broker-route-unsupported','broker-startup-inspector','broker-startup-certificate','broker-startup-transport','broker-startup-managed','broker-startup-failed']);
|
||||
// Parent-only: resourceRoot and verifyPackage come from the trusted embedded
|
||||
// installer contract. Never accept executable/module paths from renderer IPC.
|
||||
function createBrokerSupervisor({resourceRoot,verifyPackage,persistAudit,acceptCertificate,onQuarantine,spawn=fork,deadlineMs=15000,shutdownMs=5000,traceOperation=(_name,_attributes,run)=>run()}={}){
|
||||
if(!path.isAbsolute(resourceRoot||'')||![verifyPackage,persistAudit,acceptCertificate,onQuarantine,spawn].every(f=>typeof f==='function')||!Number.isInteger(deadlineMs)||deadlineMs<1||deadlineMs>60000)throw Error('broker-supervisor-invalid');
|
||||
if(!Number.isInteger(shutdownMs)||shutdownMs<1||shutdownMs>30000)throw Error('broker-supervisor-invalid');
|
||||
if(typeof traceOperation!=='function')throw Error('broker-supervisor-invalid');
|
||||
// Instrumentation may neither skip a protected operation nor replace its
|
||||
// result, retry it, or invoke it twice. Only fixed stage names are exported.
|
||||
async function stage(name,operation){
|
||||
let original,called=false;
|
||||
const once=()=>{if(called)throw Error('broker-diagnostics-invalid');called=true;try{original=Promise.resolve(operation());}catch(error){original=Promise.reject(error);}original.catch(()=>{});return original;};
|
||||
try{await traceOperation(name,{component:'network'},once);}catch(error){if(called)throw error;return once();}
|
||||
if(!called)once();return original;
|
||||
}
|
||||
let child,current,failed=false,closed=false,starting=false,timer,readyResolve,readyReject,lastSequence=0,stopping,pendingCertificates=0,startupCode;
|
||||
const stopChild=()=>{
|
||||
clearTimeout(timer);if(stopping)return stopping;
|
||||
const previous=child;if(!previous)return Promise.resolve();
|
||||
previous.removeAllListeners('message');
|
||||
stopping=stage('browser.broker.shutdown',()=>new Promise((resolve,reject)=>{
|
||||
let done=false;
|
||||
const finish=()=>{if(done)return;done=true;clearTimeout(limit);child=undefined;resolve();};
|
||||
const limit=setTimeout(()=>{if(done)return;done=true;failed=true;try{onQuarantine('broker-shutdown-unconfirmed');}catch{}reject(Error('broker-shutdown-unconfirmed'));},shutdownMs);
|
||||
previous.once('exit',finish);
|
||||
// ChildProcess retains ownership state; no numeric PID/taskkill operation
|
||||
// is performed after exit, avoiding a reused-PID tree kill. Native job-based
|
||||
// descendant containment remains an installer/launcher prerequisite.
|
||||
if(previous.exitCode!==null&&previous.exitCode!==undefined||previous.signalCode)finish();
|
||||
else try{previous.kill('SIGKILL');}catch{}
|
||||
}));
|
||||
// Fault callers do not await shutdown, but close() receives its real outcome.
|
||||
stopping.catch(()=>{});return stopping;
|
||||
};
|
||||
const fault=()=>{if(failed||closed)return;failed=true;stopChild();try{onQuarantine('broker-supervisor-unavailable');}catch{}readyReject?.(Error('broker-supervisor-unavailable'));};
|
||||
const send=value=>{if(!child||failed||closed)throw Error('broker-supervisor-unavailable');child.send(value,error=>{if(error)fault();});};
|
||||
const context=()=>({version:1,generation:current.generation,policyVersion:current.policyVersion});
|
||||
const exact=(m,keys)=>Object.keys(m).sort().join(',')===keys.sort().join(',');
|
||||
async function message(m,owner,generation){
|
||||
if(owner!==child||generation!==current.generation)return;
|
||||
try{
|
||||
const bytes=Buffer.byteLength(JSON.stringify(m));
|
||||
if(bytes>65536||!m||typeof m!=='object'||Array.isArray(m)||m.version!==1||m.generation!==current.generation||m.policyVersion!==current.policyVersion)throw Error();
|
||||
const common=['type','version','generation','policyVersion'];
|
||||
if(m.type==='startup-fault'){
|
||||
if(current.ready||!exact(m,[...common,'code'])||!STARTUP_CODES.has(m.code))throw Error();
|
||||
// Bounded local pre-enrollment state only; no raw errors or unauthorised
|
||||
// telemetry exporter. This never grants readiness or permits a retry.
|
||||
startupCode=m.code;fault();return;
|
||||
}
|
||||
if(m.type==='ready'){
|
||||
if(m.sequence!==0||m.authMode!=='basic'||current.ready||!exact(m,[...common,'port','credentials','authMode','sequence'])||!Number.isInteger(m.port)||m.port!==current.port||!m.credentials||!exact(m.credentials,['username','password'])||!/^safetynet-[a-f0-9]{32}$/.test(m.credentials.username)||!/^[A-Za-z0-9_-]{43}$/.test(m.credentials.password))throw Error();
|
||||
current.ready=true;clearTimeout(timer);readyResolve(Object.freeze({generation:current.generation,policyVersion:current.policyVersion,port:m.port,credentials:Object.freeze({...m.credentials})}));return;
|
||||
}
|
||||
if(!UUID.test(m.requestId)||!Number.isSafeInteger(m.sequence)||m.sequence!==lastSequence+1)throw Error();lastSequence=m.sequence;
|
||||
if(m.type==='audit'&&exact(m,[...common,'requestId','sequence','event'])){
|
||||
const receipt=persistAudit(m.event);
|
||||
if(!receipt||receipt.persisted!==true||receipt.eventId!==m.event.eventId||receipt.then)throw Error();
|
||||
if(owner!==child||generation!==current.generation)throw Error();
|
||||
send({type:'audit-receipt',...context(),requestId:m.requestId,sequence:m.sequence,receipt});return;
|
||||
}
|
||||
if(m.type==='certificate'&&exact(m,[...common,'requestId','sequence','authority','cert'])){
|
||||
if(pendingCertificates>=16)throw Error();
|
||||
if(typeof m.authority!=='string'||m.authority.length>300||typeof m.cert!=='string'||m.cert.length>16384||!/^-----BEGIN CERTIFICATE-----/.test(m.cert))throw Error();
|
||||
const certificate=new X509Certificate(m.cert);if(certificate.ca)throw Error();
|
||||
pendingCertificates++;
|
||||
const certificateDeadline=setTimeout(fault,shutdownMs);
|
||||
let accepted;
|
||||
// Admission remains occupied until the ORIGINAL callback settles even if
|
||||
// the deadline quarantines. A timed-out operation cannot free capacity.
|
||||
try{accepted=await acceptCertificate(Object.freeze({...context(),authority:m.authority,cert:m.cert,fingerprint256:certificate.fingerprint256}));}
|
||||
finally{clearTimeout(certificateDeadline);pendingCertificates--;}
|
||||
if(accepted!==true||owner!==child||generation!==current.generation)throw Error();
|
||||
send({type:'certificate-receipt',...context(),requestId:m.requestId,sequence:m.sequence,accepted:true});return;
|
||||
}
|
||||
throw Error();
|
||||
}catch{fault();}
|
||||
}
|
||||
return Object.freeze({
|
||||
async start({policyVersion,port,config}={}){
|
||||
if(failed||closed||child||starting||!Number.isSafeInteger(policyVersion)||policyVersion<1||!Number.isInteger(port)||port<1||port>65535)throw Error('broker-supervisor-invalid');
|
||||
starting=true;
|
||||
try{
|
||||
if(await stage('browser.broker.verify',()=>verifyPackage(resourceRoot))!==true)throw Error();
|
||||
if(failed||closed)throw Error();
|
||||
current={generation:randomUUID(),policyVersion,port,ready:false};lastSequence=0;
|
||||
const envelope={type:'start',...context(),config};if(Buffer.byteLength(JSON.stringify(envelope))>2097152)throw Error();
|
||||
// Allowlisted Windows runtime essentials only; inherited Node injection,
|
||||
// proxy credentials and developer environment never reach the child.
|
||||
const env={};for(const key of ['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','LOCALAPPDATA','APPDATA'])if(process.env[key])env[key]=process.env[key];
|
||||
const pending=new Promise((resolve,reject)=>{readyResolve=resolve;readyReject=reject;});
|
||||
const owner=spawn(path.join(resourceRoot,ENTRY),[],{execPath:path.join(resourceRoot,'node.exe'),cwd:resourceRoot,env,execArgv:[],windowsHide:true,stdio:['ignore','ignore','ignore','ipc'],serialization:'json'});
|
||||
child=owner;const generation=current.generation;
|
||||
owner.on('message',m=>{void message(m,owner,generation);});
|
||||
for(const event of ['error','exit','disconnect'])owner.once(event,()=>{if(owner===child)fault();});
|
||||
timer=setTimeout(fault,deadlineMs);send(envelope);return await stage('browser.broker.start',()=>pending);
|
||||
}catch{fault();throw Error('broker-supervisor-unavailable');}
|
||||
},
|
||||
async close(){closed=true;readyReject?.(Error('broker-supervisor-closed'));await stopChild();},
|
||||
// Replacement is deliberately stop-and-new-supervisor: no automatic retry
|
||||
// and no stale ready state or credentials survive a policy generation.
|
||||
status(){return Object.freeze({ready:!!current?.ready&&!failed&&!closed,failed,closed,generation:current?.generation,startupCode});}
|
||||
});
|
||||
}
|
||||
module.exports={createBrokerSupervisor};
|
||||
@@ -10,6 +10,8 @@ const {trace,ROOT_CONTEXT,SpanStatusCode,TraceFlags}=require('@opentelemetry/api
|
||||
const names=new Set(['browser.window','browser.network','browser.ipc','browser.agent','browser.management','browser.navigation','browser.dns','browser.request','browser.request.headers','browser.inspect.local','browser.inspect.enterprise','browser.download','browser.clipboard','browser.visual','browser.password.fill','browser.ai','browser.policy','browser.startup','browser.parser','browser.semantic','browser.yara','browser.icap','browser.tab','browser.security','browser.evidence']);
|
||||
const operations=new Set(['audit.deliver','evidence.observer.install','evidence.observer.drain','evidence.observer.dispose','policy.verify','policy.bookmarks','policy.agent-reconcile','policy.page-configure','policy.audit','evidence.capture','evidence.authorize','evidence.challenge','evidence.encrypt','evidence.send','evidence.ack','agent.request','agent.challenge','agent.decrypt','agent.transport','agent.authorize','agent.audit','agent.dispatch','agent.response','agent.project','agent.encrypt','library:organize-plan','library:organize-apply','application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group','state','authorize-setting','settings-update','window-control','chrome-layout','share-url','bookmarks-mutate','enroll','sso','navigate','back','forward','reload','ai','inspect-file','agent-connection','ai-panel','vault-panel','vault-status','vault-create','vault-unlock','vault-lock','vault-list','vault-save','vault-delete','vault-fill','vault-generate','tab-create','tab-activate','tab-close','tab-group','sessions','read','inspect','scroll','click','type','select','check','screenshot','copy','cut','paste','scan','download','sync','create','activate','close','group','policy','request','dns','parse','embed','yara','icap']);
|
||||
for(const operation of ['visual.probe','visual.capture','visual.inspect.local','visual.inspect.enterprise','visual.audit','visual.validate','visual.project','visual.release'])operations.add(operation);
|
||||
for(const operation of ['broker.request','broker.framing','broker.buffer','broker.inspect','broker.authorize','broker.forward'])operations.add(operation);
|
||||
for(const name of ['browser.broker.verify','browser.broker.start','browser.broker.shutdown'])names.add(name);
|
||||
const components=new Set(['browser','ipc','agent','management','navigation','network','dns','dlp','inspector','parser','semantic','yara','icap','download','clipboard','visual','vault','ai','policy','tabs']);
|
||||
for(const operation of ['text.local','text.enterprise'])operations.add(operation);
|
||||
for(const operation of ['history:list','history:go','window:state','window:control'])operations.add(operation);
|
||||
|
||||
+34
-14
@@ -1,20 +1,40 @@
|
||||
const {app}=require('electron');
|
||||
const {spawn}=require('node:child_process');
|
||||
const path=require('node:path');
|
||||
function capabilityCrypto(operation,input){
|
||||
const root=path.join(__dirname,'../..');
|
||||
const python=app.isPackaged?path.join(process.resourcesPath,'python/python.exe'):path.join(root,'.runtime/windows/python/python.exe');
|
||||
const script=app.isPackaged?path.join(process.resourcesPath,'inspector/dpapi.py'):path.join(root,'apps/inspector/dpapi.py');
|
||||
if(!['protect','unprotect'].includes(operation)||Buffer.byteLength(input)>32768)return Promise.reject(Error('Capability rejected'));
|
||||
function createCapabilityCrypto({packaged=false,resourcesPath,root=path.join(__dirname,'../..'),launch=spawn,environment=process.env}={}){
|
||||
const python=packaged?path.join(resourcesPath,'python/python.exe'):path.join(root,'.runtime/windows/python/python.exe');
|
||||
const script=packaged?path.join(resourcesPath,'inspector/dpapi.py'):path.join(root,'apps/inspector/dpapi.py');
|
||||
const active=new Set();
|
||||
return function convert(operation,input){
|
||||
const inputLimit=operation==='protect'?32768:65536;
|
||||
if(!['protect','unprotect'].includes(operation)||!(typeof input==='string'||Buffer.isBuffer(input))||!Buffer.byteLength(input)||Buffer.byteLength(input)>inputLimit)return Promise.reject(Error('Capability rejected'));
|
||||
if(active.size>=8)return Promise.reject(Error('Windows capability protection failed'));
|
||||
return new Promise((resolve,reject)=>{
|
||||
const environment={...process.env};for(const key of ['ADMIN_TOKEN','OIDC_CLIENT_SECRET','AI_API_KEY','INSPECTOR_TOKEN','SIEM_HTTP_TOKEN','SAFETYNET_PROXY_PASSWORD'])delete environment[key];
|
||||
const child=spawn(python,[script,operation],{windowsHide:true,stdio:['pipe','pipe','pipe'],env:environment});let output='',settled=false;
|
||||
const timer=setTimeout(()=>{child.kill();fail();},5000);
|
||||
function fail(){if(!settled){settled=true;clearTimeout(timer);reject(Error('Windows capability protection failed'));}}
|
||||
child.on('error',fail);child.stdin.on('error',fail);child.stderr.on('data',()=>{});
|
||||
child.stdout.on('data',data=>{output+=data;if(output.length>65536){child.kill();fail();}});
|
||||
child.on('close',code=>{if(code!==0||!output)return fail();if(!settled){settled=true;clearTimeout(timer);resolve(output);}});
|
||||
child.stdin.end(input);
|
||||
// Python startup hooks and inherited secrets must not enter a capability
|
||||
// helper. Isolated mode also disables user-site and Python environment paths.
|
||||
const env={};for(const key of ['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','LOCALAPPDATA','APPDATA'])if(environment[key])env[key]=environment[key];
|
||||
let child,timer,settled=false,terminal=false,bytes=0;
|
||||
const output=Buffer.alloc(operation==='protect'?65536:32768),slot={};active.add(slot);
|
||||
function fail(){
|
||||
if(settled)return;settled=true;clearTimeout(timer);output.fill(0);
|
||||
// A rejected operation does not free the slot until actual child close.
|
||||
if(child&&!terminal){try{child.kill();}catch{}child.stdin?.destroy?.();child.stdout?.destroy?.();child.stderr?.destroy?.();}
|
||||
reject(Error('Windows capability protection failed'));
|
||||
}
|
||||
try{
|
||||
child=launch(python,['-I','-B',script,operation],{windowsHide:true,shell:false,stdio:['pipe','pipe','pipe'],env});
|
||||
child.once('close',code=>{
|
||||
terminal=true;active.delete(slot);
|
||||
if(settled)return;if(code!==0||!bytes){fail();return;}
|
||||
settled=true;clearTimeout(timer);const result=output.subarray(0,bytes).toString('utf8');output.fill(0);resolve(result);
|
||||
});
|
||||
child.on('error',fail);child.stdin.on('error',fail);child.stdout.on('error',fail);child.stderr.on('error',fail);child.stderr.on('data',()=>{});
|
||||
child.stdout.on('data',data=>{if(settled)return;if(!Buffer.isBuffer(data)||bytes+data.length>output.length){fail();return;}data.copy(output,bytes);bytes+=data.length;});
|
||||
timer=setTimeout(fail,5000);child.stdin.end(input);
|
||||
}catch{if(!child)active.delete(slot);fail();}
|
||||
});
|
||||
};
|
||||
}
|
||||
module.exports={capabilityCrypto};
|
||||
let sharedConverter;
|
||||
function capabilityCrypto(operation,input){sharedConverter??=createCapabilityCrypto({packaged:app.isPackaged,resourcesPath:process.resourcesPath});return sharedConverter(operation,input);}
|
||||
module.exports={capabilityCrypto,createCapabilityCrypto};
|
||||
@@ -102,6 +102,7 @@ let message='Enroll this browser to receive company policy.',syncTimer,localInsp
|
||||
const recentRequests=new Map();
|
||||
const agentFile=()=>path.join(app.getPath('userData'),'agent-connection.json');
|
||||
const agentGrants=require('./agent-grants.cjs');
|
||||
function protectAgentCapability(value){return diagnosticSpan('browser.credential',{operation:'credential.issue',component:'agent',bytes:Buffer.byteLength(value)},()=>require('./dpapi.cjs').capabilityCrypto('protect',value));}
|
||||
const agentActorStorage=new (require('node:async_hooks').AsyncLocalStorage)();
|
||||
let agentServiceMode;
|
||||
function effectiveAgentPolicy(){if(!policy)return policy;const {grants}=agentGrants.reconcileGenerationFloor(policy.agents.grants||[],device?.agentGenerationFloor||{});return {...policy,agents:{...policy.agents,grants}};}
|
||||
@@ -121,7 +122,7 @@ async function pairIndividualAgent(principalId){
|
||||
const grant=approvedAgentGrants().find(g=>g.id===principalId),initial=policy,service=agentService;
|
||||
if(!grant||!service||agentServiceMode!=='individual')throw Error('Agent grant unavailable');
|
||||
const connection=service.issuePrincipal(grant.id,grant.generation,grant.expiresAt);
|
||||
const credentialPath=await agentGrants.persistConnection({filePath:agentGrants.descriptorPath(app.getPath('userData'),grant.id),connection,protect:value=>require('./dpapi.cjs').capabilityCrypto('protect',value),isCurrent:()=>ready&&policy===initial&&agentService===service&&featureAllowed(managedSettings(),'agents')&&actorGrantAllowed({principalId:grant.id,generation:grant.generation},grant.scopes[0])});
|
||||
const credentialPath=await agentGrants.persistConnection({filePath:agentGrants.descriptorPath(app.getPath('userData'),grant.id),connection,protect:protectAgentCapability,isCurrent:()=>ready&&policy===initial&&agentService===service&&featureAllowed(managedSettings(),'agents')&&actorGrantAllowed({principalId:grant.id,generation:grant.generation},grant.scopes[0])});
|
||||
return {address:service.address,credentialPath,principalId:grant.id,generation:grant.generation};
|
||||
}
|
||||
|
||||
@@ -457,7 +458,7 @@ async function ensureAgentAPI(){
|
||||
if(!checkURL(url))return {allowed:false};
|
||||
return {allowed:true,role:'agent',scopes:[scope]};
|
||||
},audit:auditAgent,
|
||||
persistCredential:connection=>agentGrants.persistConnection({filePath:agentFile(),connection,protect:value=>require('./dpapi.cjs').capabilityCrypto('protect',value),isCurrent:()=>ready&&policy?.agents.identityMode==='legacy'&&policy.agents.enabled})
|
||||
persistCredential:connection=>agentGrants.persistConnection({filePath:agentFile(),connection,protect:protectAgentCapability,isCurrent:()=>ready&&policy?.agents.identityMode==='legacy'&&policy.agents.enabled})
|
||||
});emit();
|
||||
}finally{agentStarting=false;}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user