Files
safetynet/apps/browser/inspection.cjs
T

56 lines
4.8 KiB
JavaScript
Raw Normal View History

2026-10-13 10:33:31 +02:00
const {spawn}=require('node:child_process');
const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto');
function privateInspectorPaths(root,packaged=false,environment=process.env){
const python=path.resolve(root,packaged?'python/python.exe':'.runtime/windows/python/python.exe');
const script=path.resolve(root,packaged?'inspector/server.py':'apps/inspector/server.py');
try{
for(const [file,override]of [[python,environment.SAFETYNET_PYTHON],[script,environment.SAFETYNET_INSPECTOR_SCRIPT]]){
if(override&&path.resolve(override).toLowerCase()!==file.toLowerCase())return null;
if(!fs.statSync(file).isFile())return null;
// Do not resolve away a junction before checking ownership boundaries.
for(let item=file;;item=path.dirname(item)){
if(fs.lstatSync(item).isSymbolicLink())return null;
if(path.dirname(item)===item)break;
}
}
return {python,script};
}catch{return null;}
}
function startInspector(root,packaged=false,options={}){
2026-10-13 10:33:31 +02:00
const files=privateInspectorPaths(root,packaged);if(!files)return null;
const {python,script}=files;
const credential=crypto.randomBytes(32).toString('base64url');
const semanticRuntime=path.join(root,packaged?'semantic':'.runtime/windows/semantic');
// Native model loading remains wholly local; the Python adapter verifies pinned artifacts.
// Explicit deployment paths take precedence, and missing artifacts fail semantic inspection closed.
const environment={...process.env,INSPECTOR_TOKEN:credential,INSPECTOR_PORT:'0',INSPECTOR_PARENT_PID:String(process.pid)};
if(!environment.EMBEDDING_MODEL_DIR)environment.EMBEDDING_MODEL_DIR=path.join(semanticRuntime,'models');
if(!environment.EMBEDDING_WORKER_PATH)environment.EMBEDDING_WORKER_PATH=path.join(semanticRuntime,'searchi-gemma-worker.exe');
for(const key of ['ADMIN_TOKEN','OIDC_CLIENT_SECRET','AI_API_KEY','SIEM_HTTP_TOKEN','SIEM_KEY_FILE','SAFETYNET_PROXY_PASSWORD'])delete environment[key];
const child=spawn(python,[script],{windowsHide:true,stdio:['ignore','pipe','pipe'],env:environment});
let port,output='';const startup=new Promise((resolve,reject)=>{
const timer=setTimeout(()=>{child.kill();reject(Error('Local inspector startup timed out'));},30000);
child.on('error',()=>{clearTimeout(timer);reject(Error('Local inspector failed to start'));});
child.once('exit',()=>{clearTimeout(timer);reject(Error('Local inspector stopped'));});
child.stdout.on('data',chunk=>{output+=chunk.toString();if(output.length>4096){child.kill();return;}
if(output.includes('\n')){try{const value=JSON.parse(output.split('\n')[0]);if(!Number.isInteger(value.port)||value.port<1||value.port>65535)throw Error();if(process.platform==='win32'&&(value.containment?.active!==true||value.containment?.kind!=='windows-job-v1'||value.containment?.killsDescendants!==true||value.containment?.parserIsolation?.active!==true||value.containment?.parserIsolation?.kind!=='windows-appcontainer-v1'||value.containment?.parserIsolation?.networkCapabilities!==0||value.containment?.parserIsolation?.privatePipes!==true||value.containment?.parserIsolation?.readOnlyRuntime!==true))throw Error();port=value.port;clearTimeout(timer);resolve();}catch{child.kill();reject(Error('Local inspector startup rejected'));}}});
child.stderr.on('data',()=>{});
});startup.catch(()=>{});
const waitReady=async()=>{await startup;if(child.exitCode!==null||child.signalCode!==null)throw Error('Local content inspector stopped; restart the browser');};
const invoke=async(route,bytes,policy)=>{
await waitReady();
const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),30000);
try{
const response=await require('./loopback-http.cjs').postLoopback({port,route,token:credential,body:{data:Buffer.from(bytes).toString('base64'),policy},signal:controller.signal});
if(response.status!==200)throw Error('Local content inspection failed');const result=response.body;
if(!['allow','block'].includes(result.decision)||typeof result.coverage?.complete!=='boolean')throw Error('Inspector response rejected');
if(result.decision==='allow'&&!result.coverage.complete)throw Error('Incomplete content cannot be released');
const stages=result._diagnosticStages;delete result._diagnosticStages;if(policy?.diagnostics?.enabled===true&&policy.diagnostics.mode==='debug'&&typeof options.onStages==='function'){try{options.onStages(stages);}catch{}}return result;
2026-10-13 10:33:31 +02:00
}catch(error){if(error.name==='AbortError'){child.kill();throw Error('Content inspection exceeded its time budget');}throw error;}
finally{clearTimeout(timer);}
};
return {stop:()=>child.kill(),waitReady,inspect:(bytes,policy)=>invoke('/inspect',bytes,policy),scanDownload:(bytes,policy)=>invoke('/scan-download',bytes,policy)};
}
module.exports={startInspector,privateInspectorPaths};