Add policy-bound credential submission and native DLP audit proofs

This commit is contained in:
Karl committed 2026-10-13 21:00:41 +02:00
1 parent b991ee9da6
commit c8b6ff9cdd
45 files changed
+777 -98

No files matched your search

+4 -4
View File
@@ -8,12 +8,12 @@ async function capture(file,section){await evaluate(`document.getElementById('${
app.whenReady().then(async()=>{
window=new BrowserWindow({show:false,width:1500,height:1100,webPreferences:{nodeIntegration:false,contextIsolation:true,sandbox:true,offscreen:true,backgroundThrottling:false}});
const proofTraceIds=[],requestsById=new Map();session.defaultSession.webRequest.onCompleted({urls:[process.env.SAFETYNET_ADMIN_SMOKE_URL+'/api/*']},details=>{const row=requestsById.get(details.id);if(row){row.outcome='completed';row.statusCode=details.statusCode;}});session.defaultSession.webRequest.onErrorOccurred({urls:[process.env.SAFETYNET_ADMIN_SMOKE_URL+'/api/*']},details=>{const row=requestsById.get(details.id);if(row)row.outcome='failed';});session.defaultSession.webRequest.onBeforeSendHeaders({urls:[process.env.SAFETYNET_ADMIN_SMOKE_URL+'/api/*']},(details,callback)=>{const crypto=require('node:crypto'),traceId=crypto.randomBytes(16).toString('hex'),spanId=crypto.randomBytes(8).toString('hex');const path=new URL(details.url).pathname,route=path==='/api/admin/policy'?'/api/admin/policy':path==='/api/admin/policy/restore'?'/api/admin/policy/restore':/^\/api\/admin\/policy-reviews\/[^/]+\/approve$/.test(path)?'/api/admin/policy-reviews/:id/approve':/^\/api\/admin\/policy-history\/[^/]+\/restore$/.test(path)?'/api/admin/policy-history/:id/restore':'other-api';const row={traceId,parentSpanId:spanId,outcome:'pending',method:details.method,route};proofTraceIds.push(row);requestsById.set(details.id,row);callback({requestHeaders:{...details.requestHeaders,traceparent:'00-'+traceId+'-'+spanId+'-01'}});});
await identity('grant-author');assert.equal(await evaluate('typeof require'),'undefined');for(const scope of ['history:list','history:go','window:state','window:control','vault:status','vault:list','vault:fill','sessions:save','sessions:list','sessions:restore'])assert.deepEqual(await evaluate(`(()=>{const field=document.getElementById('agent-scope-${scope}');return {present:!!field,checked:field.checked,disabled:field.disabled};})()`),{present:true,checked:false,disabled:false});await evaluate(`document.getElementById('agent-enabled').checked=true;document.getElementById('agent-scope-sessions').checked=true;document.getElementById('agent-scope-read').checked=true;for(const scope of ['vault:status','vault:list','vault:fill','sessions:save','sessions:list','sessions:restore'])document.getElementById('agent-scope-'+scope).checked=true;document.getElementById('agent-hosts').value='*.company.example';document.getElementById('agent-identity-mode').value='individual';document.getElementById('agent-identity-mode').dispatchEvent(new Event('change'));document.getElementById('agent-grant-add').click();`);
await identity('grant-author');assert.equal(await evaluate('typeof require'),'undefined');for(const scope of ['history:list','history:go','window:state','window:control','vault:status','vault:list','vault:fill','sessions:save','sessions:list','sessions:restore','vault:prepare-submit'])assert.deepEqual(await evaluate(`(()=>{const field=document.getElementById('agent-scope-${scope}');return {present:!!field,checked:field.checked,disabled:field.disabled};})()`),{present:true,checked:false,disabled:false});assert.equal(await evaluate(`document.getElementById('vault-controlled-submit').checked`),false);await evaluate(`document.getElementById('agent-enabled').checked=true;document.getElementById('vault-controlled-submit').checked=true;document.getElementById('vault-submit-forms').value=JSON.stringify([{id:'company-login',actionUrl:'https://login.company.example/signin',usernameField:'username',passwordField:'password'}]);document.getElementById('agent-scope-sessions').checked=true;document.getElementById('agent-scope-read').checked=true;for(const scope of ['vault:status','vault:list','vault:fill','sessions:save','sessions:list','sessions:restore','vault:prepare-submit'])document.getElementById('agent-scope-'+scope).checked=true;document.getElementById('agent-hosts').value='*.company.example';document.getElementById('agent-identity-mode').value='individual';document.getElementById('agent-identity-mode').dispatchEvent(new Event('change'));document.getElementById('agent-grant-add').click();`);
assert.equal(await evaluate(`Array.from(document.querySelectorAll('#agent-grants-editor .checks input')).every(x=>!x.checked)`),true);assert.equal(await evaluate(`document.getElementById('agent-vault-help').textContent.includes('A person must unlock')&&document.getElementById('agent-vault-help').textContent.includes('cannot unlock it or export passwords')`),true);assert.equal(await evaluate(`document.getElementById('agent-grants-editor').textContent.includes('human unlock')`),true);assert.equal(await evaluate(`document.getElementById('agent-sessions-help').textContent.includes('no passwords, form values or page state')&&document.getElementById('agent-sessions-help').textContent.includes('never restores revoked agent authority')`),true);
await evaluate(`const box=document.querySelector('#agent-grants-editor>fieldset'),fields=box.querySelectorAll('.grid input,.grid textarea');fields[0].value='research-agent';fields[0].dispatchEvent(new Event('input'));fields[1].value='Research Agent';fields[1].dispatchEvent(new Event('input'));fields[2].checked=true;fields[2].dispatchEvent(new Event('change'));fields[3].value='2099-12-31T23:59:59Z';fields[3].dispatchEvent(new Event('input'));fields[4].value='*.company.example';fields[4].dispatchEvent(new Event('input'));for(const label of box.querySelectorAll('.checks label'))if(['sessions','read','vault:status','vault:list','vault:fill','sessions:save','sessions:list','sessions:restore'].includes(label.textContent)){const input=label.querySelector('input');input.checked=true;input.dispatchEvent(new Event('change'));}document.getElementById('publish').click()`);await waitFor(`document.getElementById('status').textContent.includes('prepared for independent approval')`);assert.equal(await evaluate(`document.getElementById('policy-version').textContent`),'v1');await selectReview();assert.equal(await evaluate(`document.getElementById('independent-review-approve').disabled`),true);
await evaluate(`const box=document.querySelector('#agent-grants-editor>fieldset'),fields=box.querySelectorAll('.grid input,.grid textarea');fields[0].value='research-agent';fields[0].dispatchEvent(new Event('input'));fields[1].value='Research Agent';fields[1].dispatchEvent(new Event('input'));fields[2].checked=true;fields[2].dispatchEvent(new Event('change'));fields[3].value='2099-12-31T23:59:59Z';fields[3].dispatchEvent(new Event('input'));fields[4].value='*.company.example';fields[4].dispatchEvent(new Event('input'));for(const label of box.querySelectorAll('.checks label'))if(['sessions','read','vault:status','vault:list','vault:fill','sessions:save','sessions:list','sessions:restore','vault:prepare-submit'].includes(label.textContent)){const input=label.querySelector('input');input.checked=true;input.dispatchEvent(new Event('change'));}document.getElementById('publish').click()`);await waitFor(`document.getElementById('status').textContent.includes('prepared for independent approval')`);assert.equal(await evaluate(`document.getElementById('policy-version').textContent`),'v1');await selectReview();assert.equal(await evaluate(`document.getElementById('independent-review-approve').disabled`),true);
assert.equal(await evaluate(`fetch('/api/admin/policy-reviews/'+selectedIndependentReview.id+'/approve',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({payloadHash:selectedIndependentReview.payloadHash,expectedVersion:selectedIndependentReview.expectedVersion,approve:true})}).then(r=>r.status)`),403);
await identity('grant-reviewer');await selectReview();const diff=await evaluate(`document.getElementById('independent-review-diff').textContent`);for(const text of ['/agents/grants','research-agent','2099','company.example','sessions','read','generation','vault:status','vault:list','vault:fill','sessions:save','sessions:list','sessions:restore'])assert.ok(diff.includes(text),text);await capture('artifacts/agent-grants-review-admin.png','independent-review-detail');await approve(2);
await identity('grant-auditor');for(const scope of ['vault:status','vault:list','vault:fill','sessions:save','sessions:list','sessions:restore'])assert.equal(await evaluate(`document.getElementById('agent-scope-${scope}').disabled`),true);assert.equal(await evaluate(`document.getElementById('agent-grant-add').disabled&&document.getElementById('agent-identity-mode').disabled`),true);assert.equal(await evaluate(`Array.from(document.querySelectorAll('#agent-grants-editor input, #agent-grants-editor textarea, #agent-grants-editor button')).every(x=>x.disabled)`),true);
await identity('grant-reviewer');await selectReview();const diff=await evaluate(`document.getElementById('independent-review-diff').textContent`);for(const text of ['/agents/grants','research-agent','2099','company.example','sessions','read','generation','controlledSubmission','company-login','vault:status','vault:list','vault:fill','sessions:save','sessions:list','sessions:restore','vault:prepare-submit'])assert.ok(diff.includes(text),text);await capture('artifacts/agent-grants-review-admin.png','independent-review-detail');await approve(2);
await capture('artifacts/credential-submission-admin-controls.png','password-vault');await identity('grant-auditor');for(const id of ['vault-controlled-submit','vault-submit-forms'])assert.equal(await evaluate(`document.getElementById('${id}').disabled`),true);for(const scope of ['vault:status','vault:list','vault:fill','sessions:save','sessions:list','sessions:restore','vault:prepare-submit'])assert.equal(await evaluate(`document.getElementById('agent-scope-${scope}').disabled`),true);assert.equal(await evaluate(`document.getElementById('agent-grant-add').disabled&&document.getElementById('agent-identity-mode').disabled`),true);assert.equal(await evaluate(`Array.from(document.querySelectorAll('#agent-grants-editor input, #agent-grants-editor textarea, #agent-grants-editor button')).every(x=>x.disabled)`),true);
await identity('grant-author');assert.equal(await evaluate(`document.querySelector('#agent-grants-editor .grid input').readOnly`),true);await evaluate(`Array.from(document.querySelectorAll('#agent-grants-editor button')).find(x=>x.textContent==='Invalidate previous credentials').click();document.getElementById('publish').click()`);await waitFor(`document.getElementById('status').textContent.includes('prepared for independent approval')`);assert.equal(await evaluate(`document.getElementById('policy-version').textContent`),'v2');await identity('grant-reviewer');await selectReview();assert.ok((await evaluate(`document.getElementById('independent-review-diff').textContent`)).includes('"generation":2'));await approve(3);
await identity('grant-author');await evaluate(`document.getElementById('brand-name').value='Unpublished preserved draft';document.getElementById('history-load').click()`);await waitFor(`document.querySelectorAll('#history-rows tr').length===3`);await evaluate(`document.querySelectorAll('#history-rows button')[1].click()`);await waitFor(`!document.getElementById('history-review').hidden`);assert.equal(await evaluate(`document.getElementById('brand-name').value`),'Unpublished preserved draft');assert.equal(await evaluate(`document.getElementById('policy-version').textContent`),'v3');assert.equal(await evaluate(`document.getElementById('history-restore').disabled`),true);const history=await evaluate(`document.getElementById('history-diff-list').textContent`);assert.ok(history.includes('/agents/grants'));assert.ok(history.includes('generation'));assert.ok((await evaluate(`document.getElementById('history-review-summary').textContent`)).includes('does not restore invalidated credentials'));await capture('artifacts/agent-grants-history-admin.png','history-review');await evaluate(`document.getElementById('history-approve').checked=true;document.getElementById('history-approve').dispatchEvent(new Event('change'));document.getElementById('history-restore').click()`);await waitFor(`document.getElementById('status').textContent.includes('prepared for independent approval')`);assert.equal(await evaluate(`document.getElementById('policy-version').textContent`),'v3');await selectReview();assert.equal(await evaluate(`document.getElementById('independent-review-approve').disabled`),true);await identity('grant-reviewer');await selectReview();assert.ok((await evaluate(`document.getElementById('independent-review-diff').textContent`)).includes('research-agent'));await approve(4);
for(let n=0;n<100&&proofTraceIds.some(row=>row.outcome==='pending');n++)await new Promise(r=>setTimeout(r,50));assert.equal(proofTraceIds.some(row=>row.outcome==='pending'),false,'API requests must settle before telemetry shutdown');
+3 -2
View File
@@ -16,6 +16,7 @@ try {
const config={publicUrl:'http://127.0.0.1',secure:false,requireIndependentPolicyApproval:true};server=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));config.publicUrl=`http://127.0.0.1:${server.address().port}`;
const env={...process.env,SAFETYNET_ADMIN_SMOKE_PROFILE:join(directory,'browser'),SAFETYNET_ADMIN_SMOKE_URL:config.publicUrl};delete env.ELECTRON_RUN_AS_NODE;
child=spawn(require('electron'),['scripts/agent-grants-history-admin-smoke.cjs'],{env,windowsHide:true,stdio:['ignore','pipe','pipe']});child.stdout.on('data',d=>{if(String(d).includes('GRANT_ADMIN_OK'))console.log('Grant admin actual UI passed');});child.stderr.on('data',d=>{if(String(d).includes('GRANT_ADMIN_FAILED'))console.error(String(d).trim());});const timer=setTimeout(()=>child.kill(),60000),code=await new Promise(r=>{child.once('exit',c=>r(c??1));child.once('error',()=>r(1));});clearTimeout(timer);assert.equal(code,0);
const p=verifyPolicy(store.envelope(),store.publicKey());assert.equal(p.version,4);assert.equal(p.agents.grants[0].generation,1);assert.equal(p.agents.identityMode,'individual');for(const scope of ['vault:status','vault:list','vault:fill','sessions:save','sessions:list','sessions:restore']){assert.ok(p.agents.scopes.includes(scope));assert.ok(p.agents.grants[0].scopes.includes(scope));}
const traces=JSON.parse(readFileSync('artifacts/agent-grants-admin-request-trace-ids.json','utf8'));await sdk.shutdown();await new Promise(r=>setTimeout(r,100));const exported=[],failedAttempts=traces.filter(item=>item.outcome==='failed');assert.ok(traces.some(item=>item.outcome==='completed'),'Completed API requests required');assert.equal(traces.some(item=>item.outcome!=='completed'&&['POST','PUT','PATCH','DELETE'].includes(item.method)),false,'Every policy mutation must complete and export a correlated span');for(const item of traces.filter(item=>item.outcome==='completed')){const r=await fetch(`http://127.0.0.1:4318/api/trace/${item.traceId}`);assert.equal(r.ok,true,'Completed API request trace must be available');if(r.ok){const trace=await r.json();assert.ok(trace.spans.some(s=>s.name==='server.request'&&s.parentSpanId===item.parentSpanId&&s.durationMs>=0));exported.push({traceId:item.traceId,parentSpanId:item.parentSpanId,trace});}}assert.ok(exported.length,'Actual request traces required');mkdirSync('artifacts',{recursive:true});writeFileSync('artifacts/agent-grants-history-admin-traces.json',JSON.stringify(exported,null,2));writeFileSync('artifacts/agent-grants-history-admin-proof.json',JSON.stringify({actualUi:true,vaultScopesDefaultOffAndReviewed:true,sessionScopesDefaultOffAndReviewed:true,separateAuthorReviewer:true,selfApprovalDenied:true,auditorReadonly:true,exactGrantDiff:true,rotationReviewed:true,historyDraftPreserved:true,restoreReviewed:true,finalVersion:p.version,completedRequests:traces.filter(item=>item.outcome==='completed').length,failedAttempts:failedAttempts.map(item=>({traceId:item.traceId,outcome:item.outcome,method:item.method,route:item.route})),traceIds:exported.map(x=>x.traceId)},null,2));console.log('Grant publication/history and exported traces verified');
const p=verifyPolicy(store.envelope(),store.publicKey());assert.equal(p.version,4);assert.equal(p.agents.grants[0].generation,1);assert.equal(p.agents.identityMode,'individual');for(const scope of ['vault:status','vault:list','vault:fill','sessions:save','sessions:list','sessions:restore','vault:prepare-submit']){assert.ok(p.agents.scopes.includes(scope));assert.ok(p.agents.grants[0].scopes.includes(scope));}
assert.equal(p.passwordManager.controlledSubmission.enabled,true);assert.equal(p.passwordManager.controlledSubmission.forms[0].actionUrl,'https://login.company.example/signin');
const traces=JSON.parse(readFileSync('artifacts/agent-grants-admin-request-trace-ids.json','utf8'));await sdk.shutdown();await new Promise(r=>setTimeout(r,100));const exported=[],failedAttempts=traces.filter(item=>item.outcome==='failed');assert.ok(traces.some(item=>item.outcome==='completed'),'Completed API requests required');assert.equal(traces.some(item=>item.outcome!=='completed'&&['POST','PUT','PATCH','DELETE'].includes(item.method)),false,'Every policy mutation must complete and export a correlated span');for(const item of traces.filter(item=>item.outcome==='completed')){const r=await fetch(`http://127.0.0.1:4318/api/trace/${item.traceId}`);assert.equal(r.ok,true,'Completed API request trace must be available');if(r.ok){const trace=await r.json();assert.ok(trace.spans.some(s=>s.name==='server.request'&&s.parentSpanId===item.parentSpanId&&s.durationMs>=0));exported.push({traceId:item.traceId,parentSpanId:item.parentSpanId,trace});}}assert.ok(exported.length,'Actual request traces required');mkdirSync('artifacts',{recursive:true});writeFileSync('artifacts/agent-grants-history-admin-traces.json',JSON.stringify(exported,null,2));writeFileSync('artifacts/agent-grants-history-admin-proof.json',JSON.stringify({actualUi:true,vaultScopesDefaultOffAndReviewed:true,sessionScopesDefaultOffAndReviewed:true,controlledCredentialPolicyReviewed:true,prepareSubmitScopeDefaultOffAndReviewed:true,separateAuthorReviewer:true,selfApprovalDenied:true,auditorReadonly:true,exactGrantDiff:true,rotationReviewed:true,historyDraftPreserved:true,restoreReviewed:true,finalVersion:p.version,completedRequests:traces.filter(item=>item.outcome==='completed').length,failedAttempts:failedAttempts.map(item=>({traceId:item.traceId,outcome:item.outcome,method:item.method,route:item.route})),traceIds:exported.map(x=>x.traceId)},null,2));console.log('Grant publication/history and exported traces verified');
} finally {child?.kill();if(server)await new Promise(r=>{server.closeAllConnections?.();server.close(r);});store?.db.close();const target=resolve(directory);if(!target.startsWith(resolve(tmpdir())+sep)||!basename(target).startsWith('safetynet-agent-grant-admin-'))throw Error('Unsafe cleanup');rmSync(target,{recursive:true,force:true,maxRetries:10,retryDelay:100});}
+27
View File
@@ -0,0 +1,27 @@
'use strict';
const {app,BrowserWindow,session}=require('electron'),fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict'),{X509Certificate}=require('node:crypto');
app.setPath('userData',process.env.SAFETYNET_SMOKE_USERDATA);app.disableHardwareAcceleration();let fixtureCredentialHeader=false;const headerFacts=[];
// Inject one synthetic header at the native fixture boundary; retain the actual
// main-process listener and its original cancellation/release callback.
app.whenReady().then(()=>{const requests=session.fromPartition('persist:safetynet-managed').webRequest,original=requests.onBeforeSendHeaders.bind(requests);requests.onBeforeSendHeaders=(filter,listener)=>original(filter,(details,callback)=>{if(details.url.endsWith('/public')){const entries=Object.entries(details.requestHeaders),one=entries.find(([name])=>name.toLowerCase()==='x-credential-one'),two=entries.find(([name])=>name.toLowerCase()==='x-credential-two'),known='fixtureControlledCredentialDelta0123456789';if(one&&two){headerFacts.push({names:entries.map(([name])=>name),fragmentOneIndex:entries.indexOf(one),fragmentTwoIndex:entries.indexOf(two),fullJoinMatch:entries.map(([,value])=>value).join('').includes(known),pairJoinMatch:(one[1]+two[1])===known,reversePairJoinMatch:(two[1]+one[1])===known});fs.writeFileSync(path.resolve('artifacts/controlled-login-header-facts.json'),JSON.stringify(headerFacts,null,2));}}if(fixtureCredentialHeader&&details.resourceType==='mainFrame'&&details.method==='POST')details.requestHeaders['X-Synthetic-Credential']='sk-syntheticControlledLoginCredential0123456789';listener(details,callback);});});require('../apps/browser/main.cjs');
const wait=ms=>new Promise(r=>setTimeout(r,ms)),base=process.env.SAFETYNET_SMOKE_SERVER,origin=process.env.SAFETYNET_SMOKE_ORIGIN,headers={authorization:`Bearer ${process.env.SAFETYNET_SMOKE_ADMIN}`,'content-type':'application/json'};
async function until(fn){for(let i=0;i<200;i++){const r=await fn();if(r)return r;await wait(100);}throw Error('Controlled login fixture timeout');}
app.whenReady().then(async()=>{const pages=[];let stage='startup';try{const win=await until(()=>{const w=BrowserWindow.getAllWindows()[0];return w&&!w.webContents.isLoading()&&w.webContents.getURL().endsWith('index.html')&&w;});const fingerprint=new X509Certificate(fs.readFileSync(path.join(__dirname,'../tests/fixtures/icap/cert.pem'))).fingerprint256;session.fromPartition('persist:safetynet-managed').setCertificateVerifyProc((req,callback)=>callback(req.hostname==='127.0.0.1'&&new X509Certificate(req.certificate.data).fingerprint256===fingerprint?0:-2));const ui=async(method,value)=>{const result=await win.webContents.executeJavaScript(`window.safetynet.${method}(${value===undefined?'':JSON.stringify(value)})`);if(!result.ok)throw Object.assign(Error('Native UI operation failed'),{operation:method,reason:/not managed|not ready|paused/i.test(result.error||'')?'not-ready':/context.*changed/i.test(result.error||'')?'context-changed':/BLOCKED_BY_CLIENT/i.test(result.error||'')?'native-request-blocked':/ERR_ABORTED/i.test(result.error||'')?'native-navigation-aborted':'ui-operation-refused'});return result.data;};const overview=async()=>(await fetch(base+'/api/admin/overview',{headers})).json();stage='enrollment';await ui('enroll',{server:base,code:process.env.SAFETYNET_SMOKE_CODE});const page=win.contentView.children.find(v=>v.webContents!==win.webContents).webContents;pages.push(page);await until(()=>!page.isLoading()&&page.getURL()===origin+'/login');const a=await ui('agentConnection','vault-a'),b=await ui('agentConnection','vault-b'),requestFile=path.join(app.getPath('userData'),'controlled-request.json');async function call(connection,action,args={}){fs.writeFileSync(requestFile,JSON.stringify({action,args}));const child=require('node:child_process').spawn(process.execPath,['scripts/agent-client.cjs','--connection',connection.credentialPath,'--request',requestFile],{windowsHide:true,stdio:['ignore','pipe','pipe']});let output='';child.stdout.on('data',v=>output+=v);child.stderr.on('data',()=>{});const exit=await new Promise(r=>{child.once('exit',r);child.once('error',()=>r(1));});return {exit,result:exit===0?JSON.parse(output).result:undefined};}const sessionId=(await call(a,'sessions')).result[0].id,master='Synthetic master password for controlled native proof',password='sk-syntheticControlledLoginCredential0123456789',username='public-native-user',alternatePassword='fixtureControlledCredentialDelta0123456789';stage='human unlock';await ui('vaultCreate',master);const saved=await ui('vaultSave',{origin,username,password,label:'Approved synthetic login'});const reset=async(mode='200')=>{await ui('navigate',origin+'/login/'+mode);await until(()=>!page.isLoading()&&page.getURL()===origin+'/login/'+mode);await ui('vaultUnlock',master);win.focus();page.focus();await wait(100);};const stats=()=>new Promise((resolve,reject)=>{const req=require('node:https').get(origin+'/stats',{ca:fs.readFileSync(path.join(__dirname,'../tests/fixtures/icap/cert.pem')),checkServerIdentity:(_host,cert)=>new X509Certificate(cert.raw).fingerprint256===fingerprint?undefined:Error('Synthetic certificate mismatch')},res=>{let body='';res.on('data',v=>body+=v);res.on('end',()=>resolve(JSON.parse(body)));});req.on('error',reject);});const inventory=async()=>{const r=await call(a,'vault:list',{sessionId});assert.equal(r.exit,0);return r.result[0].credentialRef;};const submit=async()=>{const terminal=new Promise((resolve,reject)=>{const timer=setTimeout(()=>{page.removeListener('did-stop-loading',done);reject(Error('Native form navigation terminal timeout'));},22000);const done=()=>{clearTimeout(timer);resolve();};page.once('did-stop-loading',done);});await page.executeJavaScript("document.querySelector('form').requestSubmit()");await terminal;await wait(200);};const prepare=async()=>{const ref=await inventory(),r=await call(a,'vault:prepare-submit',{sessionId,credentialRef:ref});assert.equal(r.exit,0,'Agent prepare failed');assert.deepEqual(r.result,{completed:true,prepared:true});return ref;};const count=async()=>{const result=await fetch(base+'/api/admin/overview',{headers});assert.equal(result.status,200);return (await result.json()).events.filter(e=>e.kind==='vault.submission-authorized').length;};
stage='ordinary fill remains DLP blocked';await reset();assert.equal((await call(a,'vault:fill',{sessionId,credentialRef:await inventory()})).exit,0);await submit();assert.equal((await stats()).submitted,0);
stage='agent prepare does not submit';await reset();let ref=await prepare();assert.equal((await stats()).submitted,0);assert.equal((await call(a,'vault:prepare-submit',{sessionId,credentialRef:ref})).exit,1);assert.equal((await call(b,'vault:prepare-submit',{sessionId,credentialRef:await inventory()})).exit,1);await submit();await until(()=>!page.isLoading()&&page.getURL()===origin+'/signin');assert.equal((await stats()).submitted,1);
stage='controlled302 safeGET';await reset('302');await prepare();await submit();await until(()=>!page.isLoading()&&page.getURL()===origin+'/final');assert.equal((await stats()).submitted,2);assert.equal((await stats()).finalGets,1);
stage='controlled307 body blocked';await reset('307');await prepare();await submit();assert.equal((await stats()).submitted,3);assert.equal((await stats()).replayed,0);stage='slow200 completes after lease expiry';await reset('slow');await prepare();await submit();await until(()=>!page.isLoading()&&page.getURL()===origin+'/signin');assert.equal((await stats()).submitted,4);
const negative=async(name,mutate)=>{stage=name;await reset();await prepare();await mutate();await submit();assert.equal((await stats()).submitted,4,name);};
await negative('confidential residual blocked',()=>page.executeJavaScript("const e=document.createElement('input');e.name='note';e.value='synthetic-company-confidential-value';document.querySelector('form').append(e)"));
await negative('local YARA residual blocked',()=>page.executeJavaScript("const e=document.createElement('input');e.name='note';e.value='EXTRA_YARA_RESIDUAL_SECRET';document.querySelector('form').append(e)"));await negative('ICAP residual blocked',()=>page.executeJavaScript("const e=document.createElement('input');e.name='note';e.value='EXTRA_ICAP_RESIDUAL_SECRET';document.querySelector('form').append(e)"));assert.equal((await stats()).icap.blocks,1);assert.equal((await stats()).icap.credentialObserved,false);await negative('duplicate password blocked',()=>page.executeJavaScript("const e=document.createElement('input');e.name='password';e.value=document.querySelector('[name=password]').value;document.querySelector('form').append(e)"));
await negative('changed action blocked',()=>page.executeJavaScript("document.querySelector('form').action='/other'"));
await negative('vault lock invalidates',()=>ui('vaultLock'));await negative('credential header blocked',async()=>{fixtureCredentialHeader=true;});fixtureCredentialHeader=false;
stage='foreground swap invalidates';await reset();await prepare();await ui('createTab',{url:origin+'/login/200'});const other=win.contentView.children.find(v=>v.webContents!==win.webContents&&v.webContents!==page).webContents;pages.push(other);await until(()=>!other.isLoading());await ui('activateTab',sessionId);await ui('vaultUnlock',master);await submit();assert.equal((await stats()).submitted,4);
const change=async modify=>{const p=(await overview()).policy;modify(p);p.version++;const proposed=await fetch(base+'/api/admin/policy',{method:'PUT',headers,body:JSON.stringify(p)});assert.equal(proposed.status,202);const review=await proposed.json();const approval=await fetch(base+`/api/admin/policy-reviews/${review.reviewId}/approve`,{method:'POST',headers:{cookie:`sn_session=${process.env.SAFETYNET_SMOKE_REVIEWER}`,origin:base,'content-type':'application/json'},body:JSON.stringify({payloadHash:review.payloadHash,expectedVersion:review.expectedVersion,approve:true})});assert.equal(approval.status,200);await until(async()=>{const s=await ui('state');return s.ready&&s.policyVersion===p.version;});};
stage='central disable invalidates';await reset();await prepare();await change(p=>p.passwordManager.controlledSubmission.enabled=false);await ui('vaultUnlock',master);await win.webContents.executeJavaScript("document.getElementById('passwords').click()");await until(()=>win.webContents.executeJavaScript("!document.getElementById('vault').hidden&&document.querySelectorAll('#vault-list .login-card').length>0"));assert.equal(await win.webContents.executeJavaScript("[...document.querySelectorAll('#vault-list button')].filter(b=>b.textContent==='Prepare approved login').every(b=>b.hidden)"),true);await win.webContents.executeJavaScript("document.getElementById('vault-close').click()");await until(()=>win.webContents.executeJavaScript("document.getElementById('vault').hidden"));await page.executeJavaScript(`document.querySelector('[name=username]').value=${JSON.stringify(username)};document.querySelector('[name=password]').value=${JSON.stringify(password)}`);await submit();assert.equal((await stats()).submitted,4);await change(p=>p.passwordManager.controlledSubmission.enabled=true);
stage='target revoke invalidates';await reset();await prepare();await change(p=>p.agents.grants.find(g=>g.id==='vault-a').enabled=false);assert.equal((await call(a,'vault:status',{sessionId})).exit,1);await reset();await page.executeJavaScript(`document.querySelector('[name=username]').value=${JSON.stringify(username)};document.querySelector('[name=password]').value=${JSON.stringify(password)}`);await submit();assert.equal((await stats()).submitted,4);
stage='unregistered credential URL escape blocked';await reset();await ui('vaultSave',{id:saved.id,origin,username,password:alternatePassword,label:'Approved synthetic login'});await ui('vaultPrepareSubmit',saved.id);const getTerminal=new Promise((resolve,reject)=>{const timer=setTimeout(()=>{page.removeListener('did-stop-loading',done);reject(Error('Native credential GET terminal timeout'));},15000);const done=()=>{clearTimeout(timer);resolve();};page.once('did-stop-loading',done);});await page.executeJavaScript(`location.href='/leak?value='+encodeURIComponent(${JSON.stringify(alternatePassword)})`);await getTerminal;await wait(200);assert.equal((await stats()).credentialUrlLeaks,0);
stage='unregistered split credential headers blocked';await reset();await ui('vaultPrepareSubmit',saved.id);const split=Math.floor(alternatePassword.length/2),headerResult=await page.executeJavaScript(`fetch('/public',{method:'POST',body:'note=public',headers:{'Content-Type':'application/x-www-form-urlencoded','X-Credential-One':${JSON.stringify(alternatePassword.slice(0,split))},'X-Credential-Two':${JSON.stringify(alternatePassword.slice(split))}}}).then(()=>true,()=>false)`);if(headerResult!==false){await wait(1500);const recent=(await overview()).events,last=recent.find(e=>e.kind==='upload.authorized'&&e.metadata.traceId);if(last){const escapedTrace=await(await fetch('http://127.0.0.1:4318/api/trace/'+last.metadata.traceId)).json();for(const sentinel of [master,password,username,alternatePassword])assert.ok(!JSON.stringify(escapedTrace).includes(sentinel));fs.writeFileSync(path.resolve('artifacts/controlled-login-escaped-header-trace.json'),JSON.stringify({traceId:last.metadata.traceId,spans:escapedTrace.spans.map(({name,operation,spanId,parentSpanId,durationMs})=>({name,operation,spanId,parentSpanId,durationMs}))},null,2));}}assert.equal(headerResult,false);assert.equal((await stats()).credentialHeaderLeaks,0);assert.equal((await stats()).publicPosts,0);assert.equal((await stats()).totalPosts,4);assert.equal((await stats()).submitted,4);await ui('vaultSave',{id:saved.id,origin,username,password,label:'Approved synthetic login'});
stage='human rendered panel handoff';await reset();await win.webContents.executeJavaScript("document.getElementById('passwords').click()");await until(()=>win.webContents.executeJavaScript("!document.getElementById('vault').hidden"));await until(()=>win.webContents.executeJavaScript("[...document.querySelectorAll('#vault-list button')].some(b=>b.textContent==='Prepare approved login'&&!b.hidden)"));await win.webContents.executeJavaScript("document.querySelectorAll('#vault-list .login-card p:nth-of-type(2)').forEach(e=>e.style.color='transparent')");fs.writeFileSync(path.resolve('artifacts/controlled-login-browser-vault.png'),(await win.webContents.capturePage()).toPNG());await win.webContents.executeJavaScript("document.getElementById('vault-master').value='synthetic-unused-master-input';document.getElementById('vault-password').value='synthetic-unused-save-input'");await ui('vaultPrepareSubmit',saved.id);assert.equal(await win.webContents.executeJavaScript("document.getElementById('vault').hidden&&document.getElementById('vault-master').value===''&&document.getElementById('vault-password').value===''"),true);assert.equal((await ui('vaultStatus')).locked,false);assert.equal((await stats()).submitted,4);await ui('vaultLock');
stage='audit and actual traces';const events=[];let after=0;for(let batch=0;batch<20;batch++){const response=await fetch(base+`/api/admin/events/export?after=${after}&limit=1000&format=ecs`,{headers});assert.equal(response.status,200);const rows=(await response.text()).trim().split('\n').filter(Boolean).map(line=>JSON.parse(line));for(const row of rows){for(const sentinel of [master,password,username,alternatePassword])assert.ok(!JSON.stringify(row).includes(sentinel));events.push({kind:row.event.action,metadata:{traceId:row.trace?.id,ruleId:row.rule?.id}});}const next=Number(response.headers.get('x-safetynet-next-cursor'));if(rows.length<1000)break;assert.ok(next>after);after=next;}assert.ok(events.some(e=>e.kind==='dlp.block'&&e.metadata.ruleId==='controlled-residual-yara'));for(const e of events)for(const sentinel of [master,password,username,alternatePassword])assert.ok(!JSON.stringify(e).includes(sentinel));await wait(1500);const completed=events.find(e=>e.kind==='vault.submission-completed'&&e.metadata.traceId);assert.ok(completed);const trace=await(await fetch('http://127.0.0.1:4318/api/trace/'+completed.metadata.traceId)).json(),required=['credential.claim','credential.parse','credential.match','credential.residual','credential.headers','credential.header-values','credential.authorize','credential.complete'];for(const operation of required)assert.ok(trace.spans.some(s=>s.name==='browser.credential'&&s.operation===operation),operation);const byId=new Map(trace.spans.map(s=>[s.spanId,s]));for(const operation of required){let span=trace.spans.find(s=>s.name==='browser.credential'&&s.operation===operation),found=false;const seen=new Set();while(span&&!seen.has(span.spanId)){if(span.name==='browser.request'){found=true;break;}seen.add(span.spanId);span=byId.get(span.parentSpanId);}assert.ok(found,'Missing request ancestor: '+operation);}assert.ok(trace.spans.some(s=>s.name==='browser.inspect.local'));assert.ok(trace.spans.some(s=>s.name==='browser.inspect.enterprise'));for(const operation of ['audit.serialize','audit.encrypt','audit.write','audit.fsync','audit.commit'])assert.ok(trace.spans.some(s=>s.name==='browser.security'&&s.operation===operation));for(const sentinel of [master,password,username,alternatePassword])assert.ok(!JSON.stringify(trace).includes(sentinel));const proofs=[{traceId:completed.metadata.traceId,commonLifecycleTrace:true,stages:trace.spans.map(({name,operation,spanId,parentSpanId,durationMs})=>({name,operation,spanId,parentSpanId,durationMs}))}];const terminal=events.filter(e=>e.kind==='vault.submission-completed');assert.ok(terminal.length>=3);const receiver=await stats(),receiverFacts={totalPosts:receiver.totalPosts,credentialPosts:receiver.submitted,publicPosts:receiver.publicPosts,replayedPosts:receiver.replayed,credentialUrlLeaks:receiver.credentialUrlLeaks,credentialHeaderLeaks:receiver.credentialHeaderLeaks};assert.equal(receiverFacts.totalPosts,4);assert.equal(receiverFacts.publicPosts,0);const scannerFacts=receiver.icap;assert.equal(scannerFacts.credentialObserved,false);const health=await(await fetch('http://127.0.0.1:4318/health')).json();assert.equal(health.rejectedWrites,0);const summary=await(await fetch('http://127.0.0.1:4318/api/summary')).json();fs.writeFileSync(path.resolve('artifacts/controlled-login-native-proof.json'),JSON.stringify({actualElectron:true,exactPinnedSyntheticHTTPS:true,ordinaryFillDLPBlocked:true,agentPrepareNeverSubmits:true,approvedPOST200:true,approvedPOST302SafeGET:true,redirect307BodyBlocked:true,residualBlocked:true,duplicatePasswordBlocked:true,changedActionBlocked:true,nativeCredentialHeaderBlocked:true,unregisteredCredentialUrlBlocked:true,unregisteredSplitHeadersBlocked:true,centralPrepareControlHidden:true,vaultLockInvalidates:true,foregroundSwapInvalidates:true,centralDisableInvalidates:true,targetRevokeInvalidates:true,referenceReplayDenied:true,humanUIFieldsCleared:true,handoffDoesNotLock:true,ordinaryAuditSecretFree:true,localYaraResidualBlocked:true,icapResidualBlocked:true,icapCredentialFree:true,slowTerminalAfterExpiry:true,receiverFacts,scannerFacts,headerFacts,summary,health:{storedBytes:health.storedBytes,rejectedWrites:health.rejectedWrites},proofs},null,2));console.log('Controlled login native proof passed');for(const p of pages)if(!p.isDestroyed())require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(p);app.exit(0);
}catch(error){fs.writeFileSync(path.resolve('artifacts/controlled-login-native-error.json'),JSON.stringify({stage,name:error.name,message:error.message,operation:error.operation,reason:error.reason,headerFacts},null,2));console.error('Controlled login native failed at '+stage+': '+error.name);for(const p of pages)if(!p.isDestroyed())require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(p);app.exit(1);}});
+25
View File
@@ -0,0 +1,25 @@
import {createServer} from 'node:https';
import {createRequire} from 'node:module';
import {mkdtempSync,mkdirSync,rmSync,readFileSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join,resolve,sep} from 'node:path';
import {spawn} from 'node:child_process';
import {randomBytes} from 'node:crypto';
import net from 'node:net';
import {createApp} from '../apps/server/app.mjs';
import {openStore,digest} from '../apps/server/store.mjs';
import {startTelemetry} from '../apps/server/telemetry.mjs';
const collector='http://127.0.0.1:4318';if(!(await fetch(collector+'/health')).ok)throw Error('Protected collector unavailable');
process.env.OTEL_EXPORTER_OTLP_ENDPOINT=collector;const telemetry=startTelemetry();
const temporary=mkdtempSync(join(tmpdir(),'safetynet-controlled-login-'));mkdirSync('artifacts',{recursive:true});
if(!resolve(temporary).startsWith(resolve(tmpdir())+sep)||!temporary.split(/[\\/]/).at(-1).startsWith('safetynet-controlled-login-'))throw Error('Unsafe temporary fixture path');
const icapFacts={scans:0,blocks:0,credentialObserved:false},icapSockets=new Set();const icap=net.createServer(socket=>{icapSockets.add(socket);socket.on('close',()=>icapSockets.delete(socket));socket.on('error',()=>{});let input=Buffer.alloc(0),done=false;socket.on('data',chunk=>{input=Buffer.concat([input,chunk]);if(input.length>262144){socket.destroy();return;}if(done||input.indexOf('\r\n\r\n')<0)return;const options=input.subarray(0,7).toString()==='OPTIONS';if(!options&&!input.subarray(-5).equals(Buffer.from('0\r\n\r\n')))return;done=true;if(options){input.fill(0);socket.end('ICAP/1.0 200 OK\r\nMethods: RESPMOD, REQMOD\r\nAllow: 204\r\nISTag: "controlled-fixture"\r\nEncapsulated: null-body=0\r\n\r\n');return;}icapFacts.scans++;if(input.includes(Buffer.from('sk-syntheticControlledLoginCredential0123456789'))||input.includes(Buffer.from('public-native-user'))||input.includes(Buffer.from('fixtureControlledCredentialDelta0123456789')))icapFacts.credentialObserved=true;const blocked=input.includes(Buffer.from('EXTRA_ICAP_RESIDUAL_SECRET'));if(blocked)icapFacts.blocks++;input.fill(0);socket.end(blocked?'ICAP/1.0 204 No Content\r\nISTag: "controlled-fixture"\r\nX-Infection-Found: Type=0; Threat=synthetic;\r\n\r\n':'ICAP/1.0 204 No Content\r\nISTag: "controlled-fixture"\r\n\r\n');});}).listen(0,'127.0.0.1');await new Promise(r=>icap.once('listening',r));
let mode='200',submitted=0,totalPosts=0,replayed=0,finalGets=0,credentialUrlLeaks=0,credentialHeaderLeaks=0,publicPosts=0;const sink=createServer({cert:readFileSync('tests/fixtures/icap/cert.pem'),key:readFileSync('tests/fixtures/icap/key.pem')},(req,res)=>{if(req.url.includes('fixtureControlledCredentialDelta0123456789'))credentialUrlLeaks++;const values=Object.values(req.headers).flat().map(value=>String(value)),known='fixtureControlledCredentialDelta0123456789';if(values.some(value=>value.includes(known))||values.some((a,i)=>values.some((b,j)=>i!==j&&a+b===known)))credentialHeaderLeaks++;if(req.url==='/stats'){res.setHeader('content-type','application/json');res.end(JSON.stringify({submitted,totalPosts,replayed,finalGets,credentialUrlLeaks,credentialHeaderLeaks,publicPosts,icap:icapFacts}));return;}if(req.method==='POST'){totalPosts++;if(req.url==='/public')publicPosts++;const chunks=[];let size=0;req.on('data',bytes=>{size+=bytes.length;if(size>65536){req.destroy();return;}chunks.push(bytes);});req.on('end',()=>{const body=Buffer.concat(chunks);const credentialPresent=body.includes(Buffer.from('sk-syntheticControlledLoginCredential0123456789'))||body.includes(Buffer.from('fixtureControlledCredentialDelta0123456789'));body.fill(0);for(const bytes of chunks)bytes.fill(0);if(req.url==='/replay'){replayed++;res.end('unexpected replay');return;}if(credentialPresent)submitted++;if(mode==='302'||mode==='307'){res.writeHead(Number(mode),{location:mode==='302'?'/final':'/replay'});res.end();return;}res.setHeader('content-type','text/html');if(mode==='slow'){setTimeout(()=>res.end('<!doctype html><title>Login accepted</title>Done'),17000);return;}res.end('<!doctype html><title>Login accepted</title>Done');});return;}if(req.url==='/final'){finalGets++;res.setHeader('content-type','text/html');res.end('<!doctype html><title>Login accepted</title>Done');return;}mode=req.url.endsWith('/302')?'302':req.url.endsWith('/307')?'307':req.url.endsWith('/slow')?'slow':'200';res.setHeader('content-type','text/html');res.end('<!doctype html><title>Approved login fixture</title><form method="post" action="/signin"><input name="username" autocomplete="username"><input name="password" type="password" autocomplete="current-password"><button id="submit">Sign in</button></form>');}).listen(0,'127.0.0.1');await new Promise(r=>sink.once('listening',r));const origin=`https://127.0.0.1:${sink.address().port}`,store=openStore(join(temporary,'db')),code=randomBytes(32).toString('base64url'),adminToken=randomBytes(32).toString('base64url');
const p=store.get('policy');p.browser.homeUrl=origin+'/login';p.passwordManager={enabled:true,allowSaving:true,allowAutofill:true,idleLockSeconds:300,controlledSubmission:{enabled:true,forms:[{id:'approved-login',actionUrl:origin+'/signin',usernameField:'username',passwordField:'password'}]}};p.dlp.rules.push({id:'fixture-credential',label:'Synthetic controlled credential',action:'block',kind:'literal',value:'sk-syntheticControlledLoginCredential0123456789'});p.dlp.rules.push({id:'fixture-confidential',label:'Synthetic residual secret',action:'block',kind:'literal',value:'synthetic-company-confidential-value'});
p.security.yara.enabled=true;p.security.yara.rules=[{id:'controlled-residual-yara',name:'Synthetic residual YARA',source:'rule controlled_residual_yara { strings: $a = "EXTRA_YARA_RESIDUAL_SECRET" condition: $a }'}];p.enterprise.icap={...p.enterprise.icap,enabled:true,url:`icap://127.0.0.1:${icap.address().port}/scan`,scanUploads:true,scanDownloads:false,scanClipboard:false,scanAI:false};
p.agents={...p.agents,enabled:true,identityMode:'individual',scopes:['sessions','vault:status','vault:list','vault:fill','vault:prepare-submit'],allowedHosts:['127.0.0.1','127.0.0.2'],grants:[{id:'vault-a',name:'Vault A',enabled:true,expiresAt:new Date(Date.now()+3600000).toISOString(),scopes:['sessions','vault:status','vault:list','vault:fill','vault:prepare-submit'],allowedHosts:['127.0.0.1'],generation:1},{id:'vault-b',name:'Vault B',enabled:true,expiresAt:new Date(Date.now()+3600000).toISOString(),scopes:['vault:status','vault:list'],allowedHosts:['127.0.0.1'],generation:1}]};
p.diagnostics={...p.diagnostics,enabled:true,mode:'debug',endpoint:collector,sampleRate:1,debugSampleRate:1,flushIntervalMs:250};store.put('policy',p);store.db.prepare('INSERT INTO enrollments VALUES(?,?,?)').run(digest(code),Date.now()+600000,'vault-fixture');
const config={secure:false,publicUrl:'http://127.0.0.1',adminToken,requirePQManagement:true,requireIndependentPolicyApproval:true};const management=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>management.once('listening',r));config.publicUrl=`http://127.0.0.1:${management.address().port}`;
const reviewer=randomBytes(32).toString('base64url');store.db.prepare('INSERT INTO sessions VALUES(?,?,?)').run(digest(reviewer),Date.now()+600000,JSON.stringify({sub:'independent-vault-reviewer',role:'admin'}));
const child=spawn(createRequire(import.meta.url)('electron'),['scripts/controlled-login-smoke.cjs'],{windowsHide:true,stdio:['ignore','pipe','pipe'],env:{...process.env,SAFETYNET_SMOKE_SERVER:config.publicUrl,SAFETYNET_SMOKE_CODE:code,SAFETYNET_SMOKE_ADMIN:adminToken,SAFETYNET_SMOKE_REVIEWER:reviewer,SAFETYNET_SMOKE_USERDATA:join(temporary,'browser'),SAFETYNET_SMOKE_ARTIFACTS:resolve('artifacts'),SAFETYNET_SMOKE_ORIGIN:origin}});const redact=chunk=>chunk.toString('utf8').replaceAll('sk-syntheticControlledLoginCredential0123456789','[synthetic-credential]').replaceAll('fixtureControlledCredentialDelta0123456789','[synthetic-credential]');child.stdout.on('data',chunk=>process.stdout.write(redact(chunk)));child.stderr.on('data',chunk=>process.stderr.write(redact(chunk)));const deadline=setTimeout(()=>child.kill(),180000);const exit=await new Promise(r=>{child.once('exit',v=>r(v??1));child.once('error',()=>r(1));});clearTimeout(deadline);
for(const socket of icapSockets)socket.destroy();await new Promise(r=>icap.close(r));for(const server of [management,sink])server.closeAllConnections();await Promise.all([new Promise(r=>management.close(r)),new Promise(r=>sink.close(r))]);store.db.close();await telemetry?.shutdown();rmSync(temporary,{recursive:true,force:true});if(exit===0&&(submitted!==4||replayed!==0||finalGets!==1||icapFacts.scans<4||icapFacts.blocks!==1||icapFacts.credentialObserved||credentialUrlLeaks||credentialHeaderLeaks||publicPosts||totalPosts!==4))throw Error('Unexpected controlled login receiver counts');process.exit(exit);
+10
View File
@@ -0,0 +1,10 @@
'use strict';
const {app,BrowserWindow,session,webContents}=require('electron'),fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict'),{X509Certificate}=require('node:crypto');
app.setPath('userData',process.env.SAFETYNET_SMOKE_USERDATA);app.disableHardwareAcceleration();
const facts=[],origin=process.env.SAFETYNET_SMOKE_ORIGIN,base=process.env.SAFETYNET_SMOKE_SERVER;
const route=url=>url===origin+'/login'?'login':url===origin+'/submit'?'submit':url===origin+'/final'?'final':url?'other':'empty';
const frame=f=>{try{return f?{processId:f.processId,routingId:f.routingId,urlKind:route(f.url)}:null;}catch{return {destroyed:true};}};
app.whenReady().then(()=>{const ses=session.fromPartition('persist:safetynet-managed');for(const name of ['onBeforeRequest','onBeforeSendHeaders']){const original=ses.webRequest[name].bind(ses.webRequest);ses.webRequest[name]=function(filter,listener){if(typeof filter==='function'){listener=filter;filter=undefined;}return original(filter,(details,callback)=>{const wc=details.webContents??(details.webContentsId?webContents.fromId(details.webContentsId):null);if(details.method==='POST'&&details.resourceType==='mainFrame')facts.push({phase:name,time:performance.now(),requestId:details.id,urlKind:route(details.url),webContentsId:details.webContentsId??null,requestFrame:frame(details.frame),mainFrame:frame(wc?.mainFrame),currentUrlKind:route(wc?.getURL())});return listener(details,response=>{if(details.method==='POST'&&details.resourceType==='mainFrame')facts.push({phase:name+'.release',time:performance.now(),requestId:details.id,urlKind:route(details.url),cancel:response?.cancel===true});callback(response);});});};} });
require('../apps/browser/main.cjs');
const wait=ms=>new Promise(r=>setTimeout(r,ms));async function until(fn){for(let i=0;i<200;i++){const value=await fn();if(value)return value;await wait(100);}throw Error('Native credential facts timeout');}
app.whenReady().then(async()=>{let page,stage='startup';try{const win=await until(()=>{const w=BrowserWindow.getAllWindows()[0];return w&&!w.webContents.isLoading()&&w.webContents.getURL().endsWith('index.html')&&w;});const fingerprint=new X509Certificate(fs.readFileSync(path.join(__dirname,'../tests/fixtures/icap/cert.pem'))).fingerprint256;session.fromPartition('persist:safetynet-managed').setCertificateVerifyProc((req,callback)=>callback(req.hostname==='127.0.0.1'&&new X509Certificate(req.certificate.data).fingerprint256===fingerprint?0:-2));const ui=async(method,value)=>{const result=await win.webContents.executeJavaScript(`window.safetynet.${method}(${value===undefined?'':JSON.stringify(value)})`);assert.equal(result.ok,true,method);return result.data;};stage='enrollment';await ui('enroll',{server:base,code:process.env.SAFETYNET_SMOKE_CODE});page=win.contentView.children.find(v=>v.webContents!==win.webContents).webContents;await until(()=>!page.isLoading()&&page.getURL()===origin+'/login');for(const name of ['will-navigate','did-start-navigation','will-redirect','did-redirect-navigation','did-frame-navigate','did-navigate'])page.on(name,(event,...args)=>{const info=typeof args[0]==='object'?args[0]:{};facts.push({phase:name,time:performance.now(),urlKind:route(info.url??args[0]??event.url),mainFrame:frame(page.mainFrame),currentUrlKind:route(page.getURL())});});stage='public form submit';await page.executeJavaScript(`document.querySelector('[name=username]').value='fixture-public-user';document.querySelector('[name=password]').value='fixture-public-value';document.querySelector('form').requestSubmit();`);await until(()=>!page.isLoading()&&page.getURL()===origin+'/final');assert.equal(facts.filter(f=>f.phase==='onBeforeRequest').length,2);assert.equal(facts.filter(f=>f.phase==='onBeforeSendHeaders').length,2);assert.equal(facts.some(f=>f.cancel),false);await wait(1000);const headers={authorization:'Bearer '+process.env.SAFETYNET_SMOKE_ADMIN};const overview=await(await fetch(base+'/api/admin/overview',{headers})).json();const event=overview.events.find(e=>e.kind==='upload.authorized'&&e.metadata.traceId);assert.ok(event);const trace=await(await fetch('http://127.0.0.1:4318/api/trace/'+event.metadata.traceId)).json();const requestSpan=trace.spans.find(s=>s.name==='browser.request');assert.ok(requestSpan);assert.ok(trace.spans.some(s=>s.parentSpanId===requestSpan.spanId&&s.name==='browser.security'));const summary=await(await fetch('http://127.0.0.1:4318/api/summary')).json();const health=await(await fetch('http://127.0.0.1:4318/health')).json();assert.equal(health.rejectedWrites,0);const proof={actualMainGuardsPreserved:true,exactPinnedSyntheticHTTPS:true,publicSyntheticBodyOnly:true,electron:process.versions.electron,chromium:process.versions.chrome,facts,summary,traceId:event.metadata.traceId,spans:trace.spans.map(({name,operation,spanId,parentSpanId,durationMs})=>({name,operation,spanId,parentSpanId,durationMs})),health:{storedBytes:health.storedBytes,rejectedWrites:health.rejectedWrites}};fs.writeFileSync(path.resolve('artifacts/credential-native-facts-proof.json'),JSON.stringify(proof,null,2));console.log('Native credential lifecycle facts passed');require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(page);app.exit(0);}catch(error){fs.writeFileSync(path.resolve('artifacts/credential-native-facts-error.json'),JSON.stringify({stage,name:error.name,message:error.message,facts},null,2));console.error('Credential lifecycle facts failed at '+stage+': '+error.name);if(page&&!page.isDestroyed())require('../apps/browser/page-clipboard-guard.cjs').disposePageClipboardGuard(page);app.exit(1);}});
+22
View File
@@ -0,0 +1,22 @@
import {createServer} from 'node:https';
import {createRequire} from 'node:module';
import {mkdtempSync,mkdirSync,rmSync,readFileSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join,resolve,sep} from 'node:path';
import {spawn} from 'node:child_process';
import {randomBytes} from 'node:crypto';
import {createApp} from '../apps/server/app.mjs';
import {openStore,digest} from '../apps/server/store.mjs';
import {startTelemetry} from '../apps/server/telemetry.mjs';
const collector='http://127.0.0.1:4318';if(!(await fetch(collector+'/health')).ok)throw Error('Protected collector unavailable');
process.env.OTEL_EXPORTER_OTLP_ENDPOINT=collector;const telemetry=startTelemetry();
const temporary=mkdtempSync(join(tmpdir(),'safetynet-credential-facts-'));mkdirSync('artifacts',{recursive:true});
if(!resolve(temporary).startsWith(resolve(tmpdir())+sep)||!temporary.split(/[\\/]/).at(-1).startsWith('safetynet-credential-facts-'))throw Error('Unsafe temporary fixture path');
let submitted=0;const sink=createServer({cert:readFileSync('tests/fixtures/icap/cert.pem'),key:readFileSync('tests/fixtures/icap/key.pem')},(req,res)=>{if(req.method==='POST'){submitted++;req.resume();if(req.url==='/submit'){res.writeHead(307,{location:'/final'});res.end();return;}res.setHeader('content-type','text/html');res.end('<!doctype html><title>Public response</title>Done');return;}res.setHeader('content-type','text/html');res.end('<!doctype html><title>Public login fixture</title><form method="post" action="/submit"><input name="username" autocomplete="username"><input name="password" type="password" autocomplete="current-password"><button>Sign in</button></form>');}).listen(0,'127.0.0.1');await new Promise(r=>sink.once('listening',r));const origin=`https://127.0.0.1:${sink.address().port}`,store=openStore(join(temporary,'db')),code=randomBytes(32).toString('base64url'),adminToken=randomBytes(32).toString('base64url');
const p=store.get('policy');p.browser.homeUrl=origin+'/login';p.passwordManager={enabled:true,allowSaving:true,allowAutofill:true,idleLockSeconds:300};
p.agents={...p.agents,enabled:true,identityMode:'individual',scopes:['sessions','vault:status','vault:list','vault:fill'],allowedHosts:['127.0.0.1','127.0.0.2'],grants:[{id:'vault-a',name:'Vault A',enabled:true,expiresAt:new Date(Date.now()+3600000).toISOString(),scopes:['sessions','vault:status','vault:list','vault:fill'],allowedHosts:['127.0.0.1'],generation:1},{id:'vault-b',name:'Vault B',enabled:true,expiresAt:new Date(Date.now()+3600000).toISOString(),scopes:['vault:status','vault:list'],allowedHosts:['127.0.0.1'],generation:1}]};
p.diagnostics={...p.diagnostics,enabled:true,mode:'debug',endpoint:collector,sampleRate:1,debugSampleRate:1,flushIntervalMs:250};store.put('policy',p);store.db.prepare('INSERT INTO enrollments VALUES(?,?,?)').run(digest(code),Date.now()+600000,'vault-fixture');
const config={secure:false,publicUrl:'http://127.0.0.1',adminToken,requirePQManagement:true,requireIndependentPolicyApproval:true};const management=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>management.once('listening',r));config.publicUrl=`http://127.0.0.1:${management.address().port}`;
const reviewer=randomBytes(32).toString('base64url');store.db.prepare('INSERT INTO sessions VALUES(?,?,?)').run(digest(reviewer),Date.now()+600000,JSON.stringify({sub:'independent-vault-reviewer',role:'admin'}));
const child=spawn(createRequire(import.meta.url)('electron'),['scripts/credential-native-facts-smoke.cjs'],{windowsHide:true,stdio:'inherit',env:{...process.env,SAFETYNET_SMOKE_SERVER:config.publicUrl,SAFETYNET_SMOKE_CODE:code,SAFETYNET_SMOKE_ADMIN:adminToken,SAFETYNET_SMOKE_REVIEWER:reviewer,SAFETYNET_SMOKE_USERDATA:join(temporary,'browser'),SAFETYNET_SMOKE_ARTIFACTS:resolve('artifacts'),SAFETYNET_SMOKE_ORIGIN:origin}});const deadline=setTimeout(()=>child.kill(),180000);const exit=await new Promise(r=>{child.once('exit',v=>r(v??1));child.once('error',()=>r(1));});clearTimeout(deadline);
for(const server of [management,sink])server.closeAllConnections();await Promise.all([new Promise(r=>management.close(r)),new Promise(r=>sink.close(r))]);store.db.close();await telemetry?.shutdown();rmSync(temporary,{recursive:true,force:true});if(exit===0&&submitted!==2)throw Error('Credential lifecycle fixture expected initial and redirected POST');process.exit(exit);
+1 -1
View File
@@ -14,7 +14,7 @@ assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize
for(const file of ['apps/browser/upload-lease.cjs','apps/browser/file-upload-gate.cjs','apps/browser/request-source.cjs'])assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)),await fs.readFile(file),`Packaged upload binding source is stale: ${file}`);
assert.deepEqual(await fs.readFile(path.join(root,'resources/inspector/upload_lease.py')),await fs.readFile('apps/inspector/upload_lease.py'),'Packaged Windows upload lease helper is stale');
assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize('apps/browser/header-inspection.cjs')),await fs.readFile('apps/browser/header-inspection.cjs'),'Packaged outgoing header DLP source is stale');
for(const file of ['apps/browser/session-snapshot.cjs','apps/browser/agent-session-adapters.cjs','apps/browser/session-restore.cjs','apps/browser/vault-agent-controls.cjs','apps/browser/vault.cjs','apps/browser/window-controls.cjs','apps/browser/history-snapshot.cjs','apps/browser/human-interaction.cjs','packages/core/siem.mjs','packages/core/evidence.mjs','packages/core/evidence-payload.cjs','packages/core/interaction-facts.cjs','apps/browser/evidence-capture.cjs','apps/browser/evidence-delivery.cjs','packages/core/request-transfer-facts.cjs','packages/core/transfer-facts.cjs','packages/core/inspection-stages.cjs','apps/browser/history-navigation.cjs','apps/browser/agent-transport-client.cjs','apps/browser/agent-client.cjs','apps/browser/library-organization.cjs','packages/core/library.mjs','packages/core/branding.mjs','packages/core/file-transfer.cjs','packages/core/file-transfer.mjs'])assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)),await fs.readFile(file),`Packaged new security/branding source is stale: ${file}`);
for(const file of ['apps/browser/credential-submission.cjs','packages/core/credential-policy.mjs','apps/browser/session-snapshot.cjs','apps/browser/agent-session-adapters.cjs','apps/browser/session-restore.cjs','apps/browser/vault-agent-controls.cjs','apps/browser/vault.cjs','apps/browser/window-controls.cjs','apps/browser/history-snapshot.cjs','apps/browser/human-interaction.cjs','packages/core/siem.mjs','packages/core/evidence.mjs','packages/core/evidence-payload.cjs','packages/core/interaction-facts.cjs','apps/browser/evidence-capture.cjs','apps/browser/evidence-delivery.cjs','packages/core/request-transfer-facts.cjs','packages/core/transfer-facts.cjs','packages/core/inspection-stages.cjs','apps/browser/history-navigation.cjs','apps/browser/agent-transport-client.cjs','apps/browser/agent-client.cjs','apps/browser/library-organization.cjs','packages/core/library.mjs','packages/core/branding.mjs','packages/core/file-transfer.cjs','packages/core/file-transfer.mjs'])assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)),await fs.readFile(file),`Packaged new security/branding source is stale: ${file}`);
for(const file of ['apps/browser/agent-grants.cjs','apps/browser/policy-assignment.cjs','packages/core/policy-groups.mjs','apps/browser/tab-registry.cjs','apps/browser/agent-adapters.cjs','apps/browser/agent-control.cjs','apps/browser/password-fill.cjs','apps/browser/diagnostics.cjs','packages/core/diagnostics.mjs','packages/core/security.mjs'])assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)),await fs.readFile(file),`Packaged device/session policy source is stale: ${file}`);
for(const file of ['apps/browser/main.cjs','apps/browser/preload.cjs','apps/browser/site-preload.cjs','apps/browser/clipboard-deny.cjs','apps/browser/clipboard-guard.cjs','apps/browser/clipboard-dom.cjs','apps/browser/page-clipboard-guard.cjs','apps/browser/startup-security.cjs','apps/browser/visual-capture.cjs','apps/browser/renderer.js','apps/browser/index.html','apps/browser/style.css','packages/core/clipboard.mjs']){
assert.deepEqual(extractFile(path.join(root,'resources/app.asar'),path.normalize(file)),await fs.readFile(file),`Packaged source is stale: ${file}`);