Implement managed browser security, individual agents and observability
This commit is contained in:
1 parent
ff482ebe23
commit
ea448de2d4
172 files changed
+5404
-347
No files matched your search
+31
-11
@@ -1,19 +1,36 @@
|
||||
import { createServer } from 'node:http';
|
||||
import { createRequire } from 'node:module';
|
||||
import { mkdtempSync,rmSync,mkdirSync } from 'node:fs';
|
||||
import { mkdtempSync,rmSync,mkdirSync,writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join,resolve } from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { spawn,execFileSync } from 'node:child_process';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { createApp } from '../apps/server/app.mjs';
|
||||
import {startTelemetry} from '../apps/server/telemetry.mjs';
|
||||
import { openStore,digest } from '../apps/server/store.mjs';
|
||||
const require=createRequire(import.meta.url),electron=require('electron');
|
||||
if(process.env.SAFETYNET_TEST_OTLP_ENDPOINT)process.env.OTEL_EXPORTER_OTLP_ENDPOINT=process.env.SAFETYNET_TEST_OTLP_ENDPOINT;
|
||||
const smokeTelemetry=startTelemetry();
|
||||
const temporary=mkdtempSync(join(tmpdir(),'safetynet-smoke-'));mkdirSync('artifacts',{recursive:true});
|
||||
let posts=0,proxyRequests=0,secondaryRequests=0;
|
||||
const evidenceKeyFile=join(temporary,'evidence.key');writeFileSync(evidenceKeyFile,randomBytes(32),{mode:0o600});
|
||||
if(process.platform==='win32')execFileSync(join(process.env.SystemRoot,'System32/WindowsPowerShell/v1.0/powershell.exe'),['-NoProfile','-NonInteractive','-Command',"$ErrorActionPreference='Stop';$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User;$acl=New-Object System.Security.AccessControl.FileSecurity;$acl.SetOwner($sid);$acl.SetAccessRuleProtection($true,$false);$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($sid,'FullControl','Allow')));Set-Acl -LiteralPath $env:TEST_EVIDENCE_KEY -AclObject $acl"],{windowsHide:true,env:{SystemRoot:process.env.SystemRoot,TEST_EVIDENCE_KEY:evidenceKeyFile},stdio:'pipe'});
|
||||
let posts=0,proxyRequests=0,secondaryRequests=0,headerRequests=0,fileUploadRequests=0,fileUploadExact=false;
|
||||
const publicUploadBytes=Buffer.from("Public managed native upload fixture\n");
|
||||
const publicUploadFile=join(temporary,"public-upload.txt"),secretUploadFile=join(temporary,"secret-upload.txt");
|
||||
writeFileSync(publicUploadFile,publicUploadBytes);writeFileSync(secretUploadFile,"Secret Roadmap\n");
|
||||
const visualLogin='<!doctype html><meta charset="utf-8"><title>Visual identity fixture</title><style>body{font-family:Arial;background:#fff;color:#202124;margin:0}form{margin:60px auto;padding:32px;border:2px solid #3267cb;border-radius:12px;width:340px;height:330px;box-sizing:content-box}h1{color:#3267cb;font-size:27px}input{display:block;margin:20px 0;padding:12px;border:1px solid #aaa;width:260px}button{background:#3267cb;color:white;padding:12px}</style><form method="post" action="/sink"><h1>SafetyNet Identity Lab</h1><h2>Sign in</h2><p>Use your laboratory account</p><input name="username" type="email" autocomplete="username"><input type="password" autocomplete="current-password"><button type="submit">Continue</button></form>';
|
||||
const receiver=(req,res)=>{
|
||||
res.setHeader('Access-Control-Allow-Origin','*');
|
||||
if(req.url==='/count'){res.setHeader('content-type','application/json');res.end(JSON.stringify({posts,proxyRequests,secondaryRequests}));return;}
|
||||
if(req.url==='/count'){res.setHeader('content-type','application/json');res.end(JSON.stringify({posts,proxyRequests,secondaryRequests,headerRequests,fileUploadRequests,fileUploadExact}));return;}
|
||||
// Dedicated count-only endpoint: header values never enter fixture logs or artifacts.
|
||||
if(req.url==='/file-upload-sink'){
|
||||
fileUploadRequests++;const chunks=[];let size=0;
|
||||
req.on('data',chunk=>{size+=chunk.length;if(size>1048576){req.destroy();return;}chunks.push(chunk);});
|
||||
req.on('end',()=>{const body=Buffer.concat(chunks),boundary=/boundary=(?:"([^"]+)"|([^;]+))/.exec(req.headers['content-type']||'');
|
||||
if(boundary){const start=body.indexOf(Buffer.from('\r\n\r\n')),end=body.indexOf(Buffer.from('\r\n--'+(boundary[1]||boundary[2])),start+4);fileUploadExact=start>=0&&end>=0&&body.subarray(start+4,end).equals(publicUploadBytes);}
|
||||
body.fill(0);for(const chunk of chunks)chunk.fill(0);res.end('file received');});return;
|
||||
}
|
||||
if(req.url==='/header-sink'){headerRequests++;req.resume();res.end('header received');return;}
|
||||
if(req.url==='/visual-login'){res.setHeader('content-type','text/html');res.end(visualLogin);return;}
|
||||
if(req.url==='/download-safe'||req.url==='/download-blocked'){
|
||||
res.setHeader('Content-Type','application/octet-stream');res.setHeader('Content-Disposition',`attachment; filename="${req.url==='/download-safe'?'safe.bin':'blocked.bin'}"`);
|
||||
@@ -21,7 +38,7 @@ const receiver=(req,res)=>{
|
||||
}
|
||||
if(req.url.startsWith('http://proxy-check.invalid/'))proxyRequests++;
|
||||
if(req.method==='POST'){posts++;req.resume();res.end('received');return;}
|
||||
res.setHeader('content-type','text/html');res.end('<!doctype html><title>SafetyNet enforcement fixture</title><h1>Managed browsing is active</h1><p>This controlled receiver verifies that blocked payloads never arrive.</p><input id="public-input" type="text"><input id="private-input" type="password" value="never-export-this"><a id="safe-download" href="/download-safe">Download safe sample</a><a id="blocked-download" href="/download-blocked">Download blocked sample</a>');
|
||||
res.setHeader('content-type','text/html');res.end('<!doctype html><title>SafetyNet enforcement fixture</title><h1>Managed browsing is active</h1><p>This controlled receiver verifies that blocked payloads never arrive.</p><input id="public-input" type="text"><select id="public-select"><option value="one">First</option><option value="two">Second</option><option value="locked" disabled>Locked</option></select><input id="public-checkbox" type="checkbox"><input id="private-input" type="password" value="never-export-this"><a id="safe-download" href="/download-safe">Download safe sample</a><a id="blocked-download" href="/download-blocked">Download blocked sample</a>');
|
||||
};
|
||||
const sink=createServer(receiver).listen(0,'127.0.0.1');await new Promise(r=>sink.on('listening',r));
|
||||
const secondary=createServer((req,res)=>{secondaryRequests++;res.setHeader('Access-Control-Allow-Origin','*');res.end('secondary');}).listen(0,'127.0.0.2');await new Promise(r=>secondary.on('listening',r));
|
||||
@@ -41,14 +58,17 @@ const inspectorPort=await new Promise((resolve,reject)=>{
|
||||
});
|
||||
inspectorProcess.stderr.on('data',()=>{});
|
||||
});
|
||||
const p=store.get('policy');p.browser.homeUrl=`http://127.0.0.1:${sink.address().port}`;p.brand={name:'Acme Secure Browser',color:'#245fbc'};p.dlp.rules=[{id:'secret-roadmap',label:'Roadmap',kind:'literal',value:'Secret Roadmap',action:'block'}];store.put('policy',p);store.db.prepare('INSERT INTO enrollments VALUES(?,?,?)').run(digest(code),Date.now()+600000,'smoke-user');
|
||||
p.browser.allowDownloads=true;p.security.yara={enabled:true,rules:[{id:'test-marker',name:'Controlled malicious marker',source:'rule Block_Test { strings: $a = "SAFETYNET_BLOCK_TEST" condition: $a }'}],maxScanBytes:1048576};p.agents={enabled:true,scopes:['sessions','navigate','read','click','type','application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group'],allowedHosts:['127.0.0.1'],allowSensitiveRead:false};store.put('policy',p);
|
||||
const config={publicUrl:'http://127.0.0.1',secure:false,adminToken,inspectorUrl:`http://127.0.0.1:${inspectorPort}`,inspectorToken};const management=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>management.on('listening',r));config.publicUrl=`http://127.0.0.1:${management.address().port}`;
|
||||
const child=spawn(electron,['scripts/electron-smoke.cjs'],{stdio:'inherit',windowsHide:true,env:{...process.env,SAFETYNET_SMOKE_SERVER:config.publicUrl,SAFETYNET_SMOKE_CODE:code,SAFETYNET_SMOKE_SINK:p.browser.homeUrl,SAFETYNET_SMOKE_SECONDARY:`http://127.0.0.2:${secondary.address().port}`,SAFETYNET_SMOKE_USERDATA:join(temporary,'browser'),SAFETYNET_SMOKE_ARTIFACTS:resolve('artifacts'),SAFETYNET_SMOKE_ADMIN:adminToken}});
|
||||
const p=store.get('policy');p.browser.homeUrl=`http://127.0.0.1:${sink.address().port}`;p.brand={name:'Acme Secure Browser',color:'#245fbc'};p.dlp.rules=[{id:'secret-roadmap',label:'Roadmap',kind:'literal',value:'Secret Roadmap',action:'block'}];if(process.env.SAFETYNET_TEST_OTLP_ENDPOINT)p.diagnostics={...p.diagnostics,enabled:true,mode:'debug',endpoint:process.env.SAFETYNET_TEST_OTLP_ENDPOINT,sampleRate:1,debugSampleRate:1,flushIntervalMs:250};store.put('policy',p);store.db.prepare('INSERT INTO enrollments VALUES(?,?,?)').run(digest(code),Date.now()+600000,'smoke-user');
|
||||
p.evidence={enabled:true,captureDownloads:true};p.browser.allowDownloads=true;p.security.yara={enabled:true,rules:[{id:'test-marker',name:'Controlled malicious marker',source:'rule Block_Test { strings: $a = "SAFETYNET_BLOCK_TEST" condition: $a }'}],maxScanBytes:1048576};p.agents={enabled:true,scopes:['sessions','navigate','back','forward','reload','read','inspect','scroll','click','type','select','check','application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group'],allowedHosts:['127.0.0.1'],allowSensitiveRead:false};if(process.env.SAFETYNET_TEST_OTLP_ENDPOINT)p.diagnostics={...p.diagnostics,enabled:true,mode:'debug',endpoint:process.env.SAFETYNET_TEST_OTLP_ENDPOINT,sampleRate:1,debugSampleRate:1,flushIntervalMs:250};store.put('policy',p);
|
||||
const config={publicUrl:'http://127.0.0.1',secure:false,adminToken,evidenceKeyFile,requirePQManagement:true,inspectorUrl:`http://127.0.0.1:${inspectorPort}`,inspectorToken};const management=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>management.on('listening',r));config.publicUrl=`http://127.0.0.1:${management.address().port}`;
|
||||
const evidenceWire=[];management.on('request',(req,res)=>{if(!req.url.startsWith('/api/device/channel/evidence'))return;const chunks=[];req.on('data',chunk=>chunks.push(chunk));req.on('end',()=>{const text=Buffer.concat(chunks).toString('utf8');evidenceWire.push({route:req.url,containsMetadata:['safe.bin','blocked.bin','sourceUrl','sha256'].some(value=>text.includes(value))});});});
|
||||
|
||||
const child=spawn(electron,['scripts/electron-smoke.cjs'],{stdio:'inherit',windowsHide:true,env:{...process.env,SAFETYNET_SMOKE_SERVER:config.publicUrl,SAFETYNET_SMOKE_CODE:code,SAFETYNET_SMOKE_SINK:p.browser.homeUrl,SAFETYNET_SMOKE_SECONDARY:`http://127.0.0.2:${secondary.address().port}`,SAFETYNET_SMOKE_USERDATA:join(temporary,'browser'),SAFETYNET_SMOKE_ARTIFACTS:resolve('artifacts'),SAFETYNET_SMOKE_ADMIN:adminToken,SAFETYNET_SMOKE_PUBLIC_UPLOAD:publicUploadFile,SAFETYNET_SMOKE_SECRET_UPLOAD:secretUploadFile}});
|
||||
const timeout=setTimeout(()=>child.kill(),240000);
|
||||
const exitCode=await new Promise(resolve=>{child.on('exit',code=>resolve(code??1));child.on('error',()=>resolve(1));});clearTimeout(timeout);
|
||||
for(const server of [management,sink,secondary])server.closeAllConnections();
|
||||
await Promise.all([new Promise(r=>management.close(r)),new Promise(r=>sink.close(r)),new Promise(r=>secondary.close(r))]);store.db.close();
|
||||
await Promise.all([new Promise(r=>management.close(r)),new Promise(r=>sink.close(r)),new Promise(r=>secondary.close(r))]);if(exitCode===0){if(!evidenceWire.some(r=>r.route==='/api/device/channel/evidence')||evidenceWire.some(r=>r.containsMetadata))throw Error('Protected evidence wire proof failed');writeFileSync(resolve('artifacts/evidence-pq-wire-proof.json'),JSON.stringify({nativeBrowser:true,encryptedSubmissions:evidenceWire.filter(r=>r.route==='/api/device/channel/evidence').length,metadataAbsentFromWire:true,plaintextEndpointDisabled:true},null,2));}
|
||||
store.db.close();
|
||||
inspectorProcess.kill();
|
||||
// Temporary paths originate from mkdtemp under the OS temp directory.
|
||||
rmSync(temporary,{recursive:true,force:true});process.exit(exitCode);
|
||||
rmSync(temporary,{recursive:true,force:true});await smokeTelemetry?.shutdown();process.exit(exitCode);
|
||||
Reference in new issue
Block a user