Add policy-bound credential submission and native DLP audit proofs
This commit is contained in:
1 parent
b991ee9da6
commit
c8b6ff9cdd
45 files changed
+777
-98
No files matched your search
@@ -16,3 +16,11 @@ test('new history and window capability controls require explicit opt-in and exp
|
||||
test('vault agent capabilities start unchecked and describe human-unlocked exact-origin access',()=>{const source=readFileSync(new URL('../apps/server/public/admin.js',import.meta.url),'utf8'),scopes=vm.runInNewContext(source.match(/const agentScopes=(\[[^;]+\]);/)[1]);for(const scope of ['vault:status','vault:list','vault:fill']){assert.ok(scopes.includes(scope));const tag=html.match(new RegExp(`<input[^>]*id="agent-scope-${scope}"[^>]*>`))[0];assert.equal(/\bchecked\b/.test(tag),false);}for(const text of ['A person must unlock the local vault','agents cannot unlock it or export passwords',"credential's exact origin"])assert.ok(html.includes(text));const editor=readFileSync(new URL('../apps/server/public/agent-grants-admin.js',import.meta.url),'utf8');for(const text of ['human unlock','Agents cannot unlock the vault or export passwords'])assert.ok(editor.includes(text));for(const unsupported of ['vault:unlock','vault:export','vault:mutate'])assert.ok(!scopes.includes(unsupported));});
|
||||
|
||||
test('session persistence capabilities are explicit opt-in with no credential state restoration',()=>{const source=readFileSync(new URL('../apps/server/public/admin.js',import.meta.url),'utf8'),scopes=vm.runInNewContext(source.match(/const agentScopes=(\[[^;]+\]);/)[1]);for(const scope of ['sessions:save','sessions:list','sessions:restore']){assert.ok(scopes.includes(scope));const tag=html.match(new RegExp(`<input[^>]*id="agent-scope-${scope}"[^>]*>`))[0];assert.equal(/\bchecked\b/.test(tag),false);}for(const text of ['Saving, listing and restoring sessions require separate capabilities','no passwords, form values or page state','never restores revoked agent authority'])assert.ok(html.includes(text));const editor=readFileSync(new URL('../apps/server/public/agent-grants-admin.js',import.meta.url),'utf8');assert.ok(editor.includes('Restoration rechecks current policy'));});
|
||||
|
||||
test('controlled credential submission and agent preparation start off and roundtrip reviewed metadata',()=>{
|
||||
const source=readFileSync(new URL('../apps/server/public/admin.js',import.meta.url),'utf8'),scopes=vm.runInNewContext(source.match(/const agentScopes=(\[[^;]+\]);/)[1]);assert.ok(scopes.includes('vault:prepare-submit'));
|
||||
for(const id of ['vault-controlled-submit','agent-scope-vault:prepare-submit']){const tag=html.match(new RegExp(`<input[^>]*id="${id}"[^>]*>`))[0];assert.equal(/\bchecked\b/.test(tag),false);}
|
||||
for(const text of ['Ordinary autofill does not authorize submission','every other field remains subject to DLP','never submits it or exports passwords','Never enter usernames, passwords, tokens'])assert.ok(html.includes(text));
|
||||
const fields=new Map(),get=id=>{if(!fields.has(id))fields.set(id,{value:'',checked:false});return fields.get(id);};const p={passwordManager:{enabled:true,allowSaving:true,allowAutofill:true,idleLockSeconds:300,controlledSubmission:{enabled:true,forms:[{id:'login',actionUrl:'https://login.example/signin',usernameField:'username',passwordField:'password'}]}}},context=vm.createContext({$:get,p});
|
||||
const fillLine=source.split('\n').find(line=>line.includes("$('vault-enabled').checked=p.passwordManager.enabled"));vm.runInContext(fillLine,context);assert.equal(get('vault-controlled-submit').checked,true);const draftLine=source.split('\n').find(line=>line.includes("const forms=JSON.parse($('vault-submit-forms')"));vm.runInContext(draftLine,context);assert.deepEqual(JSON.parse(JSON.stringify(p.passwordManager.controlledSubmission)),{enabled:true,forms:[{id:'login',actionUrl:'https://login.example/signin',usernameField:'username',passwordField:'password'}]});assert.equal(p.passwordManager.allowAutofill,true);get('vault-submit-forms').value='{}';assert.throws(()=>vm.runInContext(draftLine,vm.createContext({$:get,p})));assert.ok(source.includes("'password-vault'"));
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {randomUUID} from 'node:crypto';
|
||||
import {mkdtempSync,rmSync} from 'node:fs';
|
||||
import {tmpdir} from 'node:os';
|
||||
import {join,resolve,sep,basename} from 'node:path';
|
||||
import {openStore,digest} from '../apps/server/store.mjs';
|
||||
import {createApp} from '../apps/server/app.mjs';
|
||||
import {toECS,toCEF} from '../packages/core/siem.mjs';
|
||||
const kinds=['vault.submission-prepared','vault.submission-authorized','vault.submission-denied','vault.submission-completed','vault.submission.failure'];
|
||||
async function fixture(t){const dir=mkdtempSync(join(tmpdir(),'safetynet-controlled-login-audit-')),store=openStore(dir),token='synthetic-controlled-login-device-token';store.db.prepare('INSERT INTO devices(id,name,token_hash) VALUES(?,?,?)').run('device','Synthetic',digest(token));for(const role of ['analyst','auditor','operator'])store.db.prepare('INSERT INTO sessions VALUES(?,?,?)').run(digest('synthetic-'+role),Date.now()+600000,JSON.stringify({sub:role,role}));store.db.prepare('INSERT INTO sessions VALUES(?,?,?)').run(digest('synthetic-employee'),Date.now()+600000,JSON.stringify({sub:'employee',role:'employee',permissions:['audit.read']}));const server=createApp(store,{publicUrl:'http://127.0.0.1',secure:false}).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));t.after(async()=>{server.closeAllConnections();await new Promise(r=>server.close(r));store.db.close();const target=resolve(dir);assert.ok(target.startsWith(resolve(tmpdir())+sep)&&basename(target).startsWith('safetynet-controlled-login-audit-'));rmSync(target,{recursive:true,force:true});});const base=`http://127.0.0.1:${server.address().port}`;return {store,base,send:(body,credential=token)=>fetch(base+'/api/device/events',{method:'POST',headers:{authorization:`Bearer ${credential}`,'content-type':'application/json'},body:JSON.stringify({eventId:randomUUID(),policyVersion:1,...body})})};}
|
||||
test('authenticated controlled login lifecycle audit is deduplicated and rejects credential-bearing metadata',async t=>{const f=await fixture(t);const traceId='a'.repeat(32),spanId='b'.repeat(16);for(const kind of kinds){const eventId=randomUUID();assert.equal((await f.send({eventId,kind,traceId,spanId})).status,200);assert.equal((await f.send({eventId,kind,traceId,spanId})).status,200);}assert.equal((await f.send({kind:kinds[0]},'wrong-token')).status,401);for(const kind of kinds)for(const key of ['host','ruleId','username','password','formId','url','actionUrl','body','credentialRef','token','reason'])assert.equal((await f.send({kind,[key]:'PRIVATE_LOGIN_SENTINEL'})).status,400,kind+':'+key);for(const extra of [{file:{bytes:1,detectedType:'text',sha256:'c'.repeat(64)}},{request:{bytes:1,detectedType:'text'}},{agent:{action:'vault:prepare-submit',scope:'vault:prepare-submit',outcome:'completed'}},{interaction:{action:'submit',outcome:'observed',sessionId:randomUUID()}}])assert.equal((await f.send({kind:kinds[1],...extra})).status,400);const rows=f.store.db.prepare('SELECT * FROM events').all();assert.equal(rows.length,kinds.length);assert.equal(f.store.auditIntegrity.verify().valid,true);for(const row of rows){const m=JSON.parse(row.metadata);assert.equal(m.traceId,traceId);assert.equal(m.spanId,spanId);assert.ok(!row.metadata.includes('PRIVATE_LOGIN_SENTINEL'));}});
|
||||
test('security analysts can export lifecycle ECS and CEF without credentials; employee self-claimed permissions cannot',async t=>{const f=await fixture(t);for(const kind of kinds)assert.equal((await f.send({kind,traceId:'a'.repeat(32),spanId:'b'.repeat(16)})).status,200);for(const role of ['analyst','auditor','operator'])for(const format of ['ecs','cef']){const res=await fetch(f.base+'/api/admin/events/export?format='+format,{headers:{cookie:'sn_session=synthetic-'+role}});assert.equal(res.status,200);const body=await res.text();for(const kind of kinds)assert.ok(body.includes(kind));for(const value of ['PRIVATE_LOGIN_SENTINEL','synthetic-controlled-login-device-token','passwordField','credentialRef'])assert.ok(!body.includes(value));if(format==='ecs'){const rows=body.trim().split('\n').map(s=>JSON.parse(s));assert.equal(rows.length,kinds.length);assert.equal(rows[0].trace.id,'a'.repeat(32));assert.ok(rows[0].safetynet.audit.hash);}}assert.ok([401,403].includes((await fetch(f.base+'/api/admin/events/export',{headers:{cookie:'sn_session=synthetic-employee'}})).status));assert.equal((await fetch(f.base+'/api/admin/events/export')).status,401);f.store.db.prepare('UPDATE devices SET revoked=1 WHERE id=?').run('device');assert.equal((await f.send({kind:kinds[0]})).status,401);});
|
||||
test('denied credential submission exports denial rather than successful authorization',()=>{for(const kind of kinds){const event={id:1,time:new Date().toISOString(),kind,device_id:'device',metadata:{policyVersion:1,traceId:'a'.repeat(32)}};const ecs=toECS(event),cef=toCEF(event);assert.equal(ecs.event.action,kind);assert.ok(cef.includes('act='+kind));if(kind===kinds[2]){assert.deepEqual(ecs.event.type,['denied']);assert.equal(ecs.safetynet.decision,'block');assert.ok(cef.includes('|7|'));}}});
|
||||
|
||||
test('terminal HTTP lifecycle exports distinguish completion from transaction failure',()=>{for(const kind of ['vault.submission-completed','vault.submission.failure']){const event={id:5,time:new Date().toISOString(),kind,device_id:'device',metadata:{policyVersion:2,traceId:'c'.repeat(32),spanId:'d'.repeat(16)},audit:{hash:'e'.repeat(64),sequence:5}};const ecs=toECS(event),cef=toCEF(event);assert.equal(ecs.event.action,kind);assert.equal(ecs.event.category[0],'web');assert.equal(ecs.event.outcome,kind.endsWith('.failure')?'failure':'success');assert.deepEqual(ecs.event.type,[kind.endsWith('.failure')?'error':'allowed']);assert.equal(ecs.trace.id,event.metadata.traceId);assert.ok(cef.includes('|3|'));assert.ok(cef.includes('act='+kind));assert.ok(cef.includes('cs4Label=TraceId'));assert.equal(ecs.safetynet.agent,undefined);assert.equal(ecs.destination,undefined);assert.equal(ecs.rule,undefined);assert.ok(!JSON.stringify(ecs).includes('authenticated'));}});
|
||||
@@ -0,0 +1,5 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import {ControlledSubmissionSchema,credentialActionUrlAllowed} from '../packages/core/credential-policy.mjs';
|
||||
const rule=()=>({id:'company-login',actionUrl:'https://login.example/signin',usernameField:'username',passwordField:'password'});
|
||||
test('controlled submission defaults off and enabled configuration needs explicit forms',()=>{assert.deepEqual(ControlledSubmissionSchema.parse({}),{enabled:false,forms:[]});assert.throws(()=>ControlledSubmissionSchema.parse({enabled:true}));assert.equal(ControlledSubmissionSchema.parse({enabled:true,forms:[rule()]}).forms.length,1);});
|
||||
test('credential rules require exact HTTPS target and bounded metadata only',()=>{for(const url of ['http://login.example/signin','https://u:[email protected]/signin','https://login.example/signin?x=y','https://login.example/signin#fragment','https://login.example/signin?','https://login.example/signin#','https://LOGIN.example/signin','https://login.example','https://login.example/sign in'])assert.equal(credentialActionUrlAllowed(url),false,url);for(const extra of ['password','username','token','formValues'])assert.throws(()=>ControlledSubmissionSchema.parse({forms:[{...rule(),[extra]:'private'}]}));for(const value of ['','a'.repeat(129),'user name','üser','x\ny'])assert.throws(()=>ControlledSubmissionSchema.parse({forms:[{...rule(),usernameField:value}]}));});
|
||||
test('duplicate rule identities or target signatures and overlapping fields are rejected',()=>{for(const forms of [[rule(),{...rule(),actionUrl:'https://other.example/'}],[rule(),{...rule(),id:'other'}],[{...rule(),passwordField:'username'}],Array.from({length:33},(_,n)=>({...rule(),id:'rule'+n,actionUrl:'https://login.example/'+n}))])assert.throws(()=>ControlledSubmissionSchema.parse({forms}));assert.equal(ControlledSubmissionSchema.parse({forms:[rule(),{...rule(),id:'other',actionUrl:'https://other.example/'}]}).forms.length,2);});
|
||||
@@ -0,0 +1,5 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import {createRequire} from 'node:module';import {createServer} from 'node:http';import {DiagnosticsSchema} from '../packages/core/diagnostics.mjs';
|
||||
const require=createRequire(import.meta.url),{createDiagnostics}=require('../apps/browser/diagnostics.cjs'),{createCredentialSubmissionBroker}=require('../apps/browser/credential-submission.cjs');
|
||||
async function fixture(t){const payloads=[],server=createServer((req,res)=>{const chunks=[];req.on('data',c=>chunks.push(c));req.on('end',()=>{payloads.push(JSON.parse(Buffer.concat(chunks).toString()));res.setHeader('content-type','application/json');res.end('{}');});}).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));const diagnostics=createDiagnostics();diagnostics.configure(DiagnosticsSchema.parse({enabled:true,mode:'debug',endpoint:`http://127.0.0.1:${server.address().port}`,sampleRate:1,debugSampleRate:1,flushIntervalMs:10000}));const broker=createCredentialSubmissionBroker({isCurrent:()=>true,traceOperation:(operation,fn)=>diagnostics.spanSync('browser.credential',{operation},fn)});t.after(async()=>{broker.close();await diagnostics.shutdown();server.closeAllConnections();await new Promise(r=>server.close(r));});const frame={},wc={id:7,mainFrame:frame},vault={status:()=>({locked:false}),accessGeneration:()=>1},context={policy:{passwordManager:{controlledSubmission:{enabled:true,forms:[{id:'secret-form',actionUrl:'https://example.com/session',usernameField:'user',passwordField:'pass'}]}}},token:{},webContents:wc,mainFrame:frame,sourceURL:'https://example.com/login',actor:{id:'private-actor'}},body=Buffer.from('user=private-user&pass=PrivateVault123%21¬e=public'),details={id:1,url:'https://example.com/session',method:'POST',resourceType:'mainFrame',webContentsId:7,frame,initiatorOrigin:'https://example.com',uploadData:[{bytes:body}],requestHeaders:{'Content-Type':'application/x-www-form-urlencoded'}};const stage=(operation,fn)=>diagnostics.spanSync('browser.credential',{operation,token:'private-token',password:'PrivateVault123!',url:details.url,formId:'secret-form'},fn);return {payloads,diagnostics,broker,context,details,body,vault,stage,spans:()=>payloads.flatMap(p=>p.resourceSpans||[]).flatMap(p=>p.scopeSpans||[]).flatMap(p=>p.spans||[])};}
|
||||
test('real OTLP credential phase spans correlate and never export secrets or form descriptors',async t=>{const f=await fixture(t);await f.diagnostics.span('browser.agent',{operation:'vault:prepare-submit'},async()=>{f.stage('credential.issue',()=>f.broker.issue({context:f.context,formId:'secret-form',username:'private-user',password:'PrivateVault123!',vault:f.vault,vaultGeneration:1}));const claim=f.stage('credential.claim',()=>f.broker.claim(f.details,f.body,f.context));try{f.stage('credential.headers',()=>f.broker.validateHeaders(f.details,f.context));f.stage('credential.authorize',()=>f.broker.finalize(1,f.context,f.details));f.broker.redirect(1);f.stage('credential.redirect',()=>f.broker.validateRedirect({...f.details,method:'GET',url:'https://example.com/home',uploadData:[],requestHeaders:{}},f.context));}finally{claim.residual.fill(0);}});await f.diagnostics.flush();const spans=f.spans(),parent=spans.find(s=>s.name==='browser.agent');for(const operation of ['credential.issue','credential.claim','credential.headers','credential.authorize','credential.redirect']){const s=spans.find(s=>s.attributes.some(a=>a.key==='safetynet.operation'&&a.value.stringValue===operation));assert.ok(s,operation);assert.equal(s.name,'browser.credential');assert.equal(s.parentSpanId,parent.spanId);assert.equal(s.traceId,parent.traceId);assert.ok(BigInt(s.endTimeUnixNano)>=BigInt(s.startTimeUnixNano));}const text=JSON.stringify(f.payloads);for(const secret of ['private-user','PrivateVault123','private-actor','private-token','secret-form','example.com'])assert.equal(text.includes(secret),false,secret);for(const operation of ['credential.authority','credential.native','credential.parse','credential.match','credential.residual','credential.header-values']){const children=spans.filter(s=>s.attributes.some(a=>a.key==='safetynet.operation'&&a.value.stringValue===operation));assert.ok(children.length,operation);for(const child of children){assert.equal(child.traceId,parent.traceId);assert.ok(child.parentSpanId);assert.ok(spans.some(s=>s.spanId===child.parentSpanId));}}assert.equal(f.diagnostics.status().lifetimeFailedExports,0);});
|
||||
test('admin diagnostics disable preserves one-use credential enforcement without exports',async t=>{const f=await fixture(t);f.diagnostics.configure({enabled:false});f.stage('credential.issue',()=>f.broker.issue({context:f.context,formId:'secret-form',username:'private-user',password:'PrivateVault123!',vault:f.vault,vaultGeneration:1}));const claim=f.stage('credential.claim',()=>f.broker.claim(f.details,f.body,f.context));claim.residual.fill(0);f.stage('credential.headers',()=>f.broker.validateHeaders(f.details,f.context));assert.equal(f.stage('credential.authorize',()=>f.broker.finalize(1,f.context,f.details)).authorized,true);assert.throws(()=>f.stage('credential.authorize',()=>f.broker.finalize(1,f.context,f.details)));await f.diagnostics.flush();assert.equal(f.spans().length,0);});
|
||||
@@ -0,0 +1,30 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import {createRequire} from 'node:module';const require=createRequire(import.meta.url),{createCredentialSubmissionBroker}=require('../apps/browser/credential-submission.cjs');
|
||||
function fixture(extra={}){let time=1000,current=true,generation=1;const frame={},wc={id:7,mainFrame:frame},vault={status:()=>({locked:false}),accessGeneration:()=>generation},policy={passwordManager:{controlledSubmission:{enabled:true,forms:[{id:'login',actionUrl:'https://example.com/session',usernameField:'user',passwordField:'pass'}]}}},context={policy,token:{},webContents:wc,mainFrame:frame,sourceURL:'https://example.com/login',actor:{id:'agent1'}};const broker=createCredentialSubmissionBroker({isCurrent:()=>current,now:()=>time,...extra}),body=Buffer.from('user=alice&pass=Strong%21Vault123¬e=public'),details={id:1,url:'https://example.com/session',method:'POST',resourceType:'mainFrame',webContentsId:7,frame,initiatorOrigin:'https://example.com',uploadData:[{bytes:body}],requestHeaders:{'Content-Type':'application/x-www-form-urlencoded','Accept':'text/html'}};const issue=()=>broker.issue({context,formId:'login',username:'alice',password:'Strong!Vault123',vault,vaultGeneration:1});return {broker,body,details,context,issue,expire:()=>time+=15001,revoke:()=>current=false,rotate:()=>generation++};}
|
||||
test('exact native submission residual inspects all other fields without credentials',()=>{const f=fixture();f.issue();const result=f.broker.claim(f.details,f.body,f.context);assert.equal(result.residual.includes(Buffer.from('alice')),false);assert.equal(result.residual.includes(Buffer.from('Strong')),false);assert.match(result.residual.toString(),/note=public/);assert.match(result.residual.toString(),/note\npublic/);assert.match(result.residual.toString(),/user\npass\nnote/);assert.throws(()=>f.broker.finalize(1,f.context,f.details));f.broker.validateHeaders(f.details,f.context);assert.equal(f.broker.finalize(1,f.context,f.details).authorized,true);assert.throws(()=>f.broker.finalize(1,f.context,f.details));f.broker.complete(1);assert.equal(f.broker.claim({...f.details,id:2},f.body,f.context),null);});
|
||||
test('unrelated GET retains one-use prepared lease',()=>{const f=fixture();f.issue();assert.equal(f.broker.claim({...f.details,method:'GET',resourceType:'image'},Buffer.alloc(0),f.context),null);assert.ok(f.broker.claim(f.details,f.body,f.context));});
|
||||
test('duplicate and malformed fields consume failed lease and remain blocked',()=>{for(const text of ['user=alice&pass=Strong%21Vault123&pass=x','user=alice&pass=Strong%ZZVault123','user=alice&pass=wrong','user=alice&pass=Strong%21Vault123¬e=Strong%21Vault123']){const f=fixture();f.issue();const bytes=Buffer.from(text),details={...f.details,uploadData:[{bytes}]};assert.throws(()=>f.broker.claim(details,bytes,f.context));assert.throws(()=>f.broker.claim(details,bytes,f.context));}});
|
||||
test('request native frame, origin, policy and vault races fail closed',()=>{for(const change of [f=>f.details.frame={},f=>f.details.initiatorOrigin='https://evil.example',f=>f.context={...f.context,policy:{...f.context.policy}},f=>f.revoke(),f=>f.rotate()]){const f=fixture();f.issue();change(f);assert.throws(()=>f.broker.claim(f.details,f.body,f.context));}});
|
||||
test('header content type, encoding, body mutation and embedded credentials fail closed',()=>{for(const change of [f=>f.details.requestHeaders['Content-Type']='application/json',f=>f.details.requestHeaders['Content-Encoding']='gzip',f=>f.details.requestHeaders['X-Secret']='prefix-Strong!Vault123-suffix',f=>f.details.uploadData=[{bytes:Buffer.from('modified')}],f=>f.revoke()]){const f=fixture();f.issue();f.broker.claim(f.details,f.body,f.context);change(f);assert.throws(()=>f.broker.validateHeaders(f.details,f.context));assert.throws(()=>f.broker.finalize(1,f.context,f.details));}});
|
||||
test('expiration, disable and redirect never grant replay exemption',()=>{const f=fixture();f.issue();f.broker.claim(f.details,f.body,f.context);f.broker.validateHeaders(f.details,f.context);f.broker.finalize(1,f.context,f.details);f.broker.redirect(1);assert.throws(()=>f.broker.claim(f.details,f.body,f.context));const g=fixture();g.issue();g.expire();assert.equal(g.broker.claim(g.details,g.body,g.context),null);const h=fixture();h.context.policy.passwordManager.controlledSubmission.enabled=false;assert.throws(()=>h.issue());});
|
||||
test('extra confidential fields remain intact in residual for normal DLP',()=>{const f=fixture();f.issue();const bytes=Buffer.from('user=alice&pass=Strong%21Vault123¬e=CORPORATE_SECRET&part1=CORPO&part2=RATE_SECRET');const r=f.broker.claim({...f.details,uploadData:[{bytes}]},bytes,f.context);assert.match(r.residual.toString(),/CORPORATE_SECRET/);assert.match(r.residual.toString(),/CORPORATE_SECRETCORPORATE_SECRET/);});
|
||||
test('central disable and authority change immediately before release invalidate authorization',()=>{for(const change of [f=>f.context.policy.passwordManager.controlledSubmission.enabled=false,f=>f.revoke(),f=>f.rotate()]){const f=fixture();f.issue();f.broker.claim(f.details,f.body,f.context);f.broker.validateHeaders(f.details,f.context);change(f);assert.throws(()=>f.broker.finalize(1,f.context,f.details));assert.throws(()=>f.broker.finalize(1,f.context,f.details));}});
|
||||
function redirectFixture(){const f=fixture();f.issue();f.broker.claim(f.details,f.body,f.context);f.broker.validateHeaders(f.details,f.context);f.broker.finalize(1,f.context,f.details);f.broker.redirect(1);f.details={...f.details,method:'GET',url:'https://example.com/home',uploadData:[],requestHeaders:{Accept:'text/html'}};return f;}
|
||||
test('302-style bodyless GET passes ordinary inspection without credential exemption',()=>{const f=redirectFixture();assert.equal(f.broker.has(1),false);assert.equal(f.broker.isBlocked(1),false);assert.equal(f.broker.validateRedirect(f.details,f.context),true);assert.equal(f.broker.claim(f.details,Buffer.alloc(0),f.context),null);assert.throws(()=>f.broker.finalize(1,f.context,f.details));f.broker.complete(1);assert.equal(f.broker.validateRedirect(f.details,f.context),false);});
|
||||
test('307-style POST replay and redirected credential URL/header remain denied',()=>{for(const change of [f=>f.details.method='POST',f=>f.details.uploadData=[{bytes:f.body}],f=>f.details.url+='?secret=Strong%21Vault123',f=>f.details.requestHeaders['X-Value']='Strong!Vault123',f=>f.details.requestHeaders.accept='duplicate']){const f=redirectFixture();change(f);assert.throws(()=>f.broker.validateRedirect(f.details,f.context));assert.throws(()=>f.broker.claim(f.details,Buffer.alloc(0),f.context));}});
|
||||
test('ordinary redirects do not become blocked and stale tombstones expire boundedly',()=>{const f=fixture();f.broker.redirect(999);assert.equal(f.broker.isBlocked(999),false);const g=redirectFixture();g.expire();assert.equal(g.broker.isBlocked(1),true);g.broker.complete(1);assert.equal(g.broker.isBlocked(1),false);});
|
||||
test('duplicate header case and printable ASCII field names follow strict parsing',()=>{const f=fixture();f.issue();f.broker.claim(f.details,f.body,f.context);f.details.requestHeaders['content-type']='application/x-www-form-urlencoded';assert.throws(()=>f.broker.validateHeaders(f.details,f.context));const g=fixture();g.context.policy.passwordManager.controlledSubmission.forms[0].usernameField='user[email]';g.context.policy.passwordManager.controlledSubmission.forms[0].passwordField='user[password]';g.issue();const bytes=Buffer.from('user%5Bemail%5D=alice&user%5Bpassword%5D=Strong%21Vault123');assert.ok(g.broker.claim({...g.details,uploadData:[{bytes}]},bytes,g.context));});
|
||||
test('public background POST and XHR preserve prepared lease; credential XHR consumes and denies',()=>{const f=fixture();f.issue();const noise=Buffer.from('note=public');assert.equal(f.broker.claim({...f.details,id:2,url:'https://example.com/metrics',resourceType:'xhr',uploadData:[{bytes:noise}]},noise,f.context),null);assert.ok(f.broker.claim(f.details,f.body,f.context));const g=fixture();g.issue();assert.throws(()=>g.broker.claim({...g.details,resourceType:'xhr'},g.body,g.context));assert.equal(g.broker.isBlocked(1),true);assert.equal(g.broker.claim({...g.details,id:3},g.body,g.context),null);});
|
||||
test('redirect checks invoke redirect authority phase and expiry timer wipes without traffic',()=>{const phases=[],f=fixture();let tick,cleared=false,time=1000;const broker=createCredentialSubmissionBroker({isCurrent:(_c,{phase})=>{phases.push(phase);return true;},now:()=>time,setIntervalImpl:callback=>{tick=callback;return {unref(){}};},clearIntervalImpl:()=>cleared=true});const issue=()=>broker.issue({context:f.context,formId:'login',username:'alice',password:'Strong!Vault123',vault:{status:()=>({locked:false}),accessGeneration:()=>1},vaultGeneration:1});issue();broker.claim(f.details,f.body,f.context);broker.validateHeaders(f.details,f.context);broker.finalize(1,f.context,f.details);broker.redirect(1);broker.validateRedirect({...f.details,method:'GET',url:'https://example.com/home',uploadData:[],requestHeaders:{}},f.context);assert.equal(phases.at(-1),'redirect');time+=15001;tick();assert.equal(broker.isBlocked(1),true);broker.close();assert.equal(cleared,true);assert.equal(broker.isBlocked(1),false);});
|
||||
test('narrow creator cancel refuses cross-actor cancellation and blocks its pending request',()=>{const f=fixture();f.issue();f.broker.cancel({...f.context,actor:{id:'other'}});assert.ok(f.broker.claim(f.details,f.body,f.context));f.broker.cancel({...f.context,webContents:{id:7}});assert.equal(f.broker.has(1),true);f.broker.cancel(f.context);assert.equal(f.broker.has(1),false);assert.equal(f.broker.isBlocked(1),true);assert.throws(()=>f.broker.validateHeaders(f.details,f.context));const g=fixture();g.issue();g.broker.cancel(g.context);assert.equal(g.broker.claim(g.details,g.body,g.context),null);});
|
||||
test('known credentials split across other fields cannot use the credential exemption',()=>{const f=fixture();f.issue();const bytes=Buffer.from('user=alice&pass=Strong%21Vault123&part1=Strong%21&part2=Vault123');assert.throws(()=>f.broker.claim({...f.details,uploadData:[{bytes}]},bytes,f.context));});
|
||||
|
||||
test('fixed internal diagnostic stages never replay or alter protected checks',()=>{for(const mode of ['before','after','twice']){const stages=[];const f=fixture({traceOperation:(operation,fn)=>{stages.push(operation);if(mode==='before')throw Error('Diagnostic failed');const result=fn();if(mode==='after')throw Error('Diagnostic failed');fn();return result;}});f.issue();const claim=f.broker.claim(f.details,f.body,f.context);assert.ok(claim);f.broker.validateHeaders(f.details,f.context);assert.equal(f.broker.finalize(1,f.context,f.details).authorized,true);for(const operation of ['credential.authority','credential.native','credential.parse','credential.match','credential.residual','credential.header-values'])assert.ok(stages.includes(operation),operation);assert.deepEqual(f.broker.complete(1),{released:true});assert.deepEqual(f.broker.complete(1),{released:false});}});
|
||||
test('terminal released fact distinguishes no-release and finalized redirect completion',()=>{const f=fixture();f.issue();f.broker.claim(f.details,f.body,f.context);assert.deepEqual(f.broker.complete(1),{released:false});const g=redirectFixture();assert.deepEqual(g.broker.complete(1),{released:true});assert.deepEqual(g.broker.complete(1),{released:false});});
|
||||
test('credential split across headers remains denied in original and redirected requests',()=>{for(const redirect of [false,true]){const f=redirect?redirectFixture():fixture();if(!redirect){f.issue();f.broker.claim(f.details,f.body,f.context);}f.details.requestHeaders['X-Part1']='Strong!';f.details.requestHeaders['X-Part2']='Vault123';assert.throws(()=>redirect?f.broker.validateRedirect(f.details,f.context):f.broker.validateHeaders(f.details,f.context));}});
|
||||
test('issued watch preserves public GET but consumes credential URL/header leak without exemption',()=>{for(const mode of ['query','header','split']){const f=fixture();f.issue();const d={...f.details,id:2,method:'GET',resourceType:'image',url:'https://example.com/image',uploadData:[],requestHeaders:{Accept:'image/png'}};assert.equal(f.broker.guardIssued(d,f.context),false);if(mode==='query')d.url+='?x=Strong%21Vault123';if(mode==='header')d.requestHeaders['X-Value']='Strong!Vault123';if(mode==='split'){d.requestHeaders['X-Part1']='Strong!';d.requestHeaders['X-Part2']='Vault123';}assert.throws(()=>f.broker.guardIssued(d,f.context));assert.equal(f.broker.isBlocked(2),true);assert.equal(f.broker.claim({...f.details,id:3},f.body,f.context),null);}});
|
||||
test('issued watch rechecks live authority and invokes watch phase',()=>{let phase;const f=fixture({isCurrent:(_context,arg)=>{phase=arg.phase;return true;}});f.issue();assert.equal(f.broker.guardIssued({...f.details,id:2,method:'GET',url:'https://example.com/public',requestHeaders:{}},f.context),false);assert.equal(phase,'watch');const g=fixture();g.issue();g.revoke();assert.throws(()=>g.broker.guardIssued({...g.details,id:2,method:'GET',url:'https://example.com/public'},g.context));assert.equal(g.broker.isBlocked(2),true);});
|
||||
test('issued watcher does not block an already claimed controlled request header phase',()=>{const f=fixture();f.issue();f.broker.claim(f.details,f.body,f.context);assert.equal(f.broker.guardIssued(f.details,f.context),false);f.broker.validateHeaders(f.details,f.context);assert.equal(f.broker.finalize(1,f.context,f.details).authorized,true);});
|
||||
test('public iframe requests preserve prepared lease while iframe credential URL is denied',()=>{const f=fixture();f.issue();const child={...f.details,id:2,resourceType:'subFrame',method:'GET',url:'https://example.com/embed',frame:{},uploadData:[],requestHeaders:{Accept:'text/html'}};assert.equal(f.broker.guardIssued(child,f.context),false);child.url+='?value=Strong%21Vault123';assert.throws(()=>f.broker.guardIssued(child,f.context));assert.equal(f.broker.isBlocked(2),true);assert.equal(f.broker.claim({...f.details,id:3},f.body,f.context),null);});
|
||||
test('native reordered and interleaved two-fragment headers fail closed in all broker phases',()=>{for(const phase of ['issued','headers','redirect'])for(const reverse of [false,true]){const f=phase==='redirect'?redirectFixture():fixture();if(phase!=='redirect')f.issue();if(phase==='headers')f.broker.claim(f.details,f.body,f.context);f.details.requestHeaders={...(phase==='headers'?{'Content-Type':'application/x-www-form-urlencoded'}:{}),'X-Credential-One':reverse?'Vault123':'Strong!','sec-ch-ua-mobile':'?0','X-Credential-Two':reverse?'Strong!':'Vault123'};if(phase==='issued'){f.details.method='GET';f.details.url='https://example.com/image';assert.throws(()=>f.broker.guardIssued(f.details,f.context));}if(phase==='headers')assert.throws(()=>f.broker.validateHeaders(f.details,f.context));if(phase==='redirect')assert.throws(()=>f.broker.validateRedirect(f.details,f.context));}});
|
||||
test('bounded percent-decoded pair checking rejects encoded split credentials',()=>{const f=fixture();f.issue();f.broker.claim(f.details,f.body,f.context);f.details.requestHeaders={'Content-Type':'application/x-www-form-urlencoded','X-Second':'%56ault123','Accept':'text/html','X-First':'Strong%21'};assert.throws(()=>f.broker.validateHeaders(f.details,f.context));});
|
||||
test('pair matching work and aggregate header size overflow fail closed',()=>{for(const mode of ['work','chars']){const f=fixture();f.issue();f.broker.claim(f.details,f.body,f.context);f.details.requestHeaders={'Content-Type':'application/x-www-form-urlencoded'};if(mode==='chars')f.details.requestHeaders['X-Large']='x'.repeat(131073);else for(let i=0;i<127;i++)f.details.requestHeaders['X-'+i]='%2541'+i;assert.throws(()=>f.broker.validateHeaders(f.details,f.context));}});
|
||||
@@ -10,13 +10,14 @@ function fixture({auditFails=false}={}){
|
||||
let listener,completeScan;
|
||||
const timers=[],callbacks=[],events=[];
|
||||
const policy={expiresAt:new Date(Date.now()+60000).toISOString(),dlp:{maxBytes:65536,semantic:{enabled:false}},security:{yara:{enabled:false}}};
|
||||
const context=vm.createContext({require:specifier=>require('../apps/browser/'+specifier.slice(2)),currentTabContext:()=>true,URL,Date,ready:true,policy,localInspector:null,
|
||||
const context=vm.createContext({require:specifier=>require('../apps/browser/'+specifier.slice(2)),credentialBroker:undefined,credentialRequestCorrelations:new Map(),currentTabContext:()=>true,URL,Date,ready:true,policy,localInspector:null,
|
||||
browserSession:{webRequest:{onBeforeSendHeaders(_filter,fn){listener=fn;}}},
|
||||
developerAssetRequestAllowed:()=>false,tabs:{all:()=>[]},core:{destinationAllowed:()=>true,inspect:()=>({decision:'allow'})},
|
||||
diagnosticSpan:(_name,_attrs,fn)=>fn(),audit:async(...args)=>{events.push(args);if(auditFails)throw Error('fixture persistence failure');},
|
||||
scanEnterprise:()=>new Promise(resolve=>{completeScan=resolve;}),setTimeout:fn=>{timers.push(fn);return timers.length;},clearTimeout(){},
|
||||
});
|
||||
vm.runInContext(source.slice(source.indexOf('function agentRequestOwnerAllowed(source)'),source.indexOf('function actorDestinationAllowed')),context);
|
||||
vm.runInContext(source.slice(source.indexOf('function requestDiagnostic('),source.indexOf('function credentialContext(')),context);
|
||||
vm.runInContext(hook,context);
|
||||
listener({url:'https://example.test/path',requestHeaders:{X:'public'},webContentsId:0,resourceType:'xhr'},response=>callbacks.push(response));
|
||||
return {timers,callbacks,events,context,finish:()=>completeScan({decision:'allow',coverage:{complete:true}})};
|
||||
|
||||
@@ -2,15 +2,15 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import vm from 'node:vm';
|
||||
import {createRequire} from 'node:module';
|
||||
const require=createRequire(import.meta.url),{fillScript}=require('../apps/browser/password-fill.cjs');
|
||||
const require=createRequire(import.meta.url),{fillScript,prepareFillScript}=require('../apps/browser/password-fill.cjs');
|
||||
const credential={origin:'https://work.example',username:'employee',password:'fixture-only-generated-password'};
|
||||
function fixture({url='https://work.example/login',action='https://work.example/signin',method='post',target='',visible=true,frame=false,multiple=false,override,focused=true,visibility='visible'}={}){
|
||||
class Input{constructor(type){this.type=type;this.name=type==='password'?'password':'username';this.autocomplete='';this.disabled=false;this.readOnly=false;}set value(value){this.stored=value;}get value(){return this.stored;}closest(){return null;}getClientRects(){return visible?[{}]:[];}dispatchEvent(){}}
|
||||
const user=new Input('text'),password=new Input('password');
|
||||
const form={action,method,target,querySelectorAll:()=>[user,password]};user.form=form;password.form=form;
|
||||
const form={action,method,target,enctype:'application/x-www-form-urlencoded',querySelectorAll:()=>[user,password]};user.form=form;password.form=form;
|
||||
form.elements=override?[user,password,{getAttribute:name=>override[name]??null}]:[user,password];
|
||||
const window={isSecureContext:true};window.top=frame?{}:window;
|
||||
return {user,password,context:vm.createContext({window,location:{href:url,origin:new URL(url).origin},document:{hasFocus:()=>focused,visibilityState:visibility,querySelectorAll:()=>multiple?[password,new Input('password')]:[password]},getComputedStyle:()=>({display:'block',visibility:'visible',opacity:'1'}),HTMLInputElement:Input,Event:class{},URL})};
|
||||
return {user,password,context:vm.createContext({window,location:{href:url,origin:new URL(url).origin},document:{hasFocus:()=>focused,visibilityState:visibility,querySelectorAll:()=>multiple?[password,new Input('password')]:[password]},getComputedStyle:element=>({display:'block',visibility:'visible',opacity:element._opacity??'1'}),HTMLInputElement:Input,Event:class{},URL})};
|
||||
}
|
||||
test('explicit fill sets exact-origin visible login fields without submitting',()=>{
|
||||
const f=fixture();assert.equal(vm.runInContext(fillScript('https://work.example/login',credential),f.context).completed,true);assert.equal(f.user.stored,credential.username);assert.equal(f.password.stored,credential.password);
|
||||
@@ -28,3 +28,10 @@ test('expired policy cannot inject a password',()=>{assert.throws(()=>fillScript
|
||||
test('queued fill cannot write into a hidden or blurred target document',()=>{
|
||||
for(const options of [{focused:false},{visibility:'hidden'}]){const f=fixture(options);assert.throws(()=>vm.runInContext(fillScript('https://work.example/login',credential),f.context),/foreground/);assert.equal(f.user.stored,undefined);assert.equal(f.password.stored,undefined);}
|
||||
});
|
||||
|
||||
const approved=[{id:'company-login',actionUrl:'https://work.example/signin',usernameField:'username',passwordField:'password'}];
|
||||
test('transparent ancestors and section-prefixed new-password fields cannot receive saved login secrets',()=>{for(const mutate of [f=>f.password.parentElement={_opacity:'0'},f=>f.password.autocomplete='section-login NEW-PASSWORD',f=>f.password.closest=()=>({disabled:true})]){const f=fixture();mutate(f);assert.throws(()=>vm.runInContext(fillScript('https://work.example/login',credential),f.context));assert.equal(f.password.stored,undefined);}});
|
||||
test('controlled fill reports only an approved form ID without submitting or returning secrets',()=>{const f=fixture(),result=vm.runInContext(prepareFillScript('https://work.example/login',credential,approved),f.context);assert.deepEqual(JSON.parse(JSON.stringify(result)),{completed:true,formId:'company-login'});assert.equal(f.password.stored,credential.password);assert.equal(JSON.stringify(result).includes(credential.password),false);});
|
||||
test('controlled fill rejects changed action, encoding, duplicated fields and submit overrides before writing',()=>{
|
||||
for(const mutate of [f=>f.password.form.action='https://work.example/other',f=>f.password.form.enctype='multipart/form-data',f=>f.password.form.elements.push({name:'password'}),f=>f.password.form.elements.push({getAttribute:n=>n==='formaction'?'https://work.example/other':null}),f=>f.password.form.elements.push({getAttribute:n=>n==='formenctype'?'text/plain':null})]){const f=fixture();mutate(f);assert.throws(()=>vm.runInContext(prepareFillScript('https://work.example/login',credential,approved),f.context));assert.equal(f.password.stored,undefined);}
|
||||
});
|
||||
@@ -20,7 +20,7 @@ async function fixture(t){
|
||||
const calls=[],next=defaultPolicy();next.diagnostics=config;const keys=generateKeyPairSync('ed25519'),privateKey=keys.privateKey.export({type:'pkcs8',format:'pem'}),publicKey=keys.publicKey.export({type:'spki',format:'pem'}),deviceId='00000000-0000-4000-8000-000000000001';
|
||||
const envelope=()=>({...signPolicy(next,privateKey),bookmarks:signBookmarkSnapshot({deviceId,policyVersion:next.version,revision:0,expiresAt:next.expiresAt,items:[]},privateKey)});
|
||||
const tracked=name=>async()=>{calls.push({name,enabled:diagnostics.status().enabled});};
|
||||
const c=vm.createContext({require,process:{env:{}},core:{verifyPolicy},assertPolicyAssignment:require('../apps/browser/policy-assignment.cjs').assertPolicyAssignment,device:{id:deviceId,publicKey,minimumVersion:0},policy:next,personalBookmarks:{revision:0,items:[]},bookmarkSnapshotJSON:'',bookmarkModule:{verifyBookmarkSnapshot},saveDevice:()=>calls.push({name:'save',enabled:diagnostics.status().enabled}),diagnosticSpan:(name,attributes,fn)=>diagnostics.span(name,attributes,fn),configureDiagnostics:v=>{calls.push({name:'configure',enabled:v.enabled});diagnostics.configure(v);},reconcileAgentPolicy:tracked('agent'),enforceEvidencePolicy:()=>{},emit:()=>{},quarantine:()=>{},browserSession:{closeAllConnections:tracked('browser-close'),setProxy:tracked('browser-proxy'),clearHostResolverCache:tracked('browser-dns')},managementSession:{closeAllConnections:tracked('management-close'),setProxy:tracked('management-proxy'),clearHostResolverCache:tracked('management-dns')},app:{configureHostResolver:()=>calls.push({name:'resolver',enabled:diagnostics.status().enabled})},resolverOptions:v=>v,recentRequests:new Map(),tabs:{all:()=>[{security:{developerToolsGuard:{close:()=>{}}},view:{webContents:{closeDevTools:()=>{}}}}]},configureVisualPage:tracked('page'),audit:tracked('audit')});
|
||||
const c=vm.createContext({require,process:{env:{}},core:{verifyPolicy},assertPolicyAssignment:require('../apps/browser/policy-assignment.cjs').assertPolicyAssignment,credentialBroker:{revoke:()=>calls.push({name:'credential-revoke',enabled:diagnostics.status().enabled})},device:{id:deviceId,publicKey,minimumVersion:0},policy:next,personalBookmarks:{revision:0,items:[]},bookmarkSnapshotJSON:'',bookmarkModule:{verifyBookmarkSnapshot},saveDevice:()=>calls.push({name:'save',enabled:diagnostics.status().enabled}),diagnosticSpan:(name,attributes,fn)=>diagnostics.span(name,attributes,fn),configureDiagnostics:v=>{calls.push({name:'configure',enabled:v.enabled});diagnostics.configure(v);},reconcileAgentPolicy:tracked('agent'),enforceEvidencePolicy:()=>{},emit:()=>{},quarantine:()=>{},browserSession:{closeAllConnections:tracked('browser-close'),setProxy:tracked('browser-proxy'),clearHostResolverCache:tracked('browser-dns')},managementSession:{closeAllConnections:tracked('management-close'),setProxy:tracked('management-proxy'),clearHostResolverCache:tracked('management-dns')},app:{configureHostResolver:()=>calls.push({name:'resolver',enabled:diagnostics.status().enabled})},resolverOptions:v=>v,recentRequests:new Map(),tabs:{all:()=>[{security:{developerToolsGuard:{close:()=>{}}},view:{webContents:{closeDevTools:()=>{}}}}]},configureVisualPage:tracked('page'),audit:tracked('audit')});
|
||||
vm.runInContext(extracted,c);return {c,next,envelope,calls,diagnostics,payloads,spans:()=>payloads.flatMap(v=>v.resourceSpans||[]).flatMap(v=>v.scopeSpans||[]).flatMap(v=>v.spans||[])};
|
||||
}
|
||||
test('actual unchanged policy exports correlated verification, bookmark and agent child stages',async t=>{
|
||||
@@ -30,7 +30,7 @@ test('actual unchanged policy exports correlated verification, bookmark and agen
|
||||
});
|
||||
test('new signed policy disables diagnostics throughout network transition and honors final disable',async t=>{
|
||||
const f=await fixture(t);f.c.policy={...f.next,version:0};f.next.diagnostics={enabled:false};await f.c.applyPolicy(f.envelope());await f.diagnostics.flush();
|
||||
for(const name of ['browser-close','browser-proxy','management-close','management-proxy','browser-dns','management-dns','resolver','save','page','agent','audit'])assert.equal(f.calls.find(v=>v.name===name).enabled,false,name);
|
||||
for(const name of ['browser-close','browser-proxy','management-close','management-proxy','browser-dns','management-dns','credential-revoke','resolver','save','page','agent','audit'])assert.equal(f.calls.find(v=>v.name===name).enabled,false,name);
|
||||
assert.deepEqual(f.calls.filter(v=>v.name==='configure').map(v=>v.enabled),[false,false]);assert.equal(f.diagnostics.status().enabled,false);assert.equal(f.spans().length,0,'Retired old generation must not export');
|
||||
});
|
||||
test('enabled new policy exports only post-configuration stages after protected network transition',async t=>{
|
||||
|
||||
@@ -9,12 +9,13 @@ const hook=source.slice(source.indexOf(" browserSession.webRequest.onBeforeRequ
|
||||
function fixture(){
|
||||
let listener,completeDNS;const timers=[],events=[],callbacks=[],policy=defaultPolicy();
|
||||
const context=vm.createContext({require:specifier=>require('../apps/browser/'+specifier.slice(2)),URL,Date,ready:true,policy,
|
||||
browserSession:{webRequest:{onBeforeRequest(_filter,fn){listener=fn;}}},tabs:{all:()=>[]},currentTabContext:()=>true,
|
||||
browserSession:{webRequest:{onBeforeRequest(_filter,fn){listener=fn;}}},tabs:{all:()=>[]},credentialBroker:undefined,credentialRequestCorrelations:new Map(),currentTabContext:()=>true,
|
||||
developerAssetRequestAllowed:()=>false,diagnosticSpan:(_name,_attrs,fn)=>fn(),core:{destinationAllowed:()=>true,inspect:()=>({decision:'allow'})},
|
||||
audit:async(...args)=>events.push(args),recentRequests:new Map(),dnsGate:()=>new Promise(resolve=>completeDNS=resolve),
|
||||
setTimeout:fn=>{timers.push(fn);return timers.length;},clearTimeout(){},Buffer,
|
||||
});
|
||||
vm.runInContext(source.slice(source.indexOf('function agentRequestOwnerAllowed(source)'),source.indexOf('function actorDestinationAllowed')),context);
|
||||
vm.runInContext(source.slice(source.indexOf('function requestDiagnostic('),source.indexOf('function credentialContext(')),context);
|
||||
vm.runInContext(hook,context);listener({id:1,url:'https://example.test/',webContentsId:0,resourceType:'mainFrame',method:'GET'},r=>callbacks.push(r));
|
||||
return {timers,events,callbacks,complete:()=>completeDNS()};
|
||||
}
|
||||
|
||||
@@ -28,7 +28,10 @@ test('privileged request gate routes upload audit through request facts without
|
||||
assert.match(source,/requestFactsForTransfer\(transfer\)/);
|
||||
assert.match(source,/audit\('upload\.authorized',null,new URL\(details\.url\)\.hostname,undefined,requestFacts\)/);
|
||||
assert.match(source,/audit\('upload\.block',transfer\.ruleId,new URL\(details\.url\)\.hostname,undefined,requestFacts\)/);
|
||||
assert.match(source,/core\.inspect\(requestBytes,policy\)/);
|
||||
// Supplementary wiring check; actual receiver fixtures verify that ordinary
|
||||
// secrets and confidential residual fields still cannot leave the browser.
|
||||
assert.match(source,/scanBytes=claim\?claim\.residual:requestBytes/);
|
||||
assert.match(source,/core\.inspect\(scanBytes,policy\)/);
|
||||
assert.equal(source.includes('sha256:transfer.sha256'),false);
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ test('security defaults deny agent access and DNS/content policies enforce centr
|
||||
p.security.dns.blockedIPs=['not-an-address'];assert.equal(PolicySchema.safeParse(p).success,false);
|
||||
});
|
||||
test('password manager defaults and idle bounds reject insecure or unknown settings',()=>{
|
||||
const p=defaultPolicy();assert.deepEqual(p.passwordManager,{enabled:true,allowSaving:true,allowAutofill:true,idleLockSeconds:300});p.passwordManager.idleLockSeconds=59;assert.equal(PolicySchema.safeParse(p).success,false);p.passwordManager.idleLockSeconds=901;assert.equal(PolicySchema.safeParse(p).success,false);p.passwordManager.idleLockSeconds=60;p.passwordManager.password='secret';assert.equal(PolicySchema.safeParse(p).success,false);
|
||||
const p=defaultPolicy();assert.deepEqual(p.passwordManager,{enabled:true,allowSaving:true,allowAutofill:true,controlledSubmission:{enabled:false,forms:[]},idleLockSeconds:300});p.passwordManager.idleLockSeconds=59;assert.equal(PolicySchema.safeParse(p).success,false);p.passwordManager.idleLockSeconds=901;assert.equal(PolicySchema.safeParse(p).success,false);p.passwordManager.idleLockSeconds=60;p.passwordManager.password='secret';assert.equal(PolicySchema.safeParse(p).success,false);
|
||||
});
|
||||
test('visual references require explicit valid brands and canonical exact domain origins',()=>{
|
||||
const p=defaultPolicy();assert.equal(p.security.visualPhishing.enabled,false);p.security.visualPhishing.enabled=true;assert.equal(PolicySchema.safeParse(p).success,false);
|
||||
|
||||
@@ -17,7 +17,7 @@ test('agent vault inventory is scoped, classified and contains opaque references
|
||||
assert.deepEqual(await a.fill({credentialRef:rows[0].credentialRef}),{completed:true});assert.equal(f.scripts,1);
|
||||
await assert.rejects(a.fill({credentialRef:rows[0].credentialRef}));assert.equal(f.scripts,1);
|
||||
assert.doesNotMatch(JSON.stringify(f.events),/private|password|credentialRef/);
|
||||
assert.deepEqual(Object.keys(a).sort(),['clear','fill','list','status']);
|
||||
assert.deepEqual(Object.keys(a).sort(),['clear','fill','list','prepare','status']);
|
||||
});
|
||||
test('locked vault and failed/incomplete classifier cannot export inventory',async()=>{
|
||||
for(const mode of ['locked','block','incomplete','error','changed']){const f=fixture();if(mode==='locked')f.lock();else f.cb.inspect=async()=>{if(mode==='error')throw Error('private-password');if(mode==='changed')f.rows[0].origin='https://changed.example';return {decision:mode==='block'?'block':'allow',coverage:{complete:mode!=='incomplete'}};};await assert.rejects(f.make().list());}
|
||||
@@ -50,3 +50,8 @@ test('direct vault arguments reject secret-bearing or arbitrary extra fields',as
|
||||
test('allow verdict with unavailable inspection marker cannot export references',async()=>{
|
||||
for(const ruleId of ['uninspectable','size-limit','inspection-incomplete','unavailable','timeout']){const f=fixture();f.cb.inspect=async()=>({decision:'allow',ruleId});await assert.rejects(f.make().list());}
|
||||
});
|
||||
test('prepare submission has a separate default-off scope and regular fill never arms',async()=>{const f=fixture();let arms=0;f.cb.prepareSubmission=async()=>{arms++;return {prepared:true};};const a=f.make(),[row]=await a.list();await assert.rejects(a.prepare({credentialRef:row.credentialRef}));assert.equal(f.scripts,0);assert.deepEqual(await a.fill({credentialRef:row.credentialRef}),{completed:true});assert.equal(arms,0);});
|
||||
test('controlled prepare arms only once after native fill and returns sanitized status',async()=>{const f=fixture();f.c.policy.agents.scopes.push('vault:prepare-submit');f.c.policy.passwordManager.controlledSubmission={enabled:true,forms:[{id:'login',actionUrl:'https://work.example/session',usernameField:'user',passwordField:'pass'}]};let armed=0,credential,after=0;f.c.webContents.executeJavaScriptInIsolatedWorld=async()=>({completed:true,formId:'login'});f.cb.prepareSubmission=async value=>{armed++;credential=value.credential;assert.equal(value.formId,'login');assert.equal(value.credential.password,'private-password');return {prepared:true,password:'must-not-export',formId:'private'};};f.cb.afterFill=()=>after++;const a=f.make(),[row]=await a.list();assert.deepEqual(await a.prepare({credentialRef:row.credentialRef}),{completed:true,prepared:true});assert.equal(armed,1);assert.equal(after,1);assert.equal(credential.password,'');assert.equal(credential.username,'');await assert.rejects(a.prepare({credentialRef:row.credentialRef}));assert.equal(armed,1);assert.doesNotMatch(JSON.stringify(f.events),/private-password|credentialRef/);});
|
||||
test('revocation or failed trusted handoff stops native credential write and consumes reference',async()=>{for(const mode of ['revoked','error','aborted']){const f=fixture(),controller=new AbortController();f.cb.beforeFill=async()=>{if(mode==='revoked')f.invalidate();if(mode==='error')throw Error('private handoff error');if(mode==='aborted')controller.abort();};const a=f.make(),[row]=await a.list();await assert.rejects(a.fill({credentialRef:row.credentialRef},{signal:controller.signal}),{message:'Managed vault operation refused'});assert.equal(f.scripts,0);await assert.rejects(a.fill({credentialRef:row.credentialRef}));}});
|
||||
test('controlled policy disabled and native renderer form mismatch never arm broker',async()=>{for(const mode of ['disabled','native-fail']){const f=fixture();f.c.policy.agents.scopes.push('vault:prepare-submit');f.c.policy.passwordManager.controlledSubmission={enabled:mode!=='disabled',forms:[{id:'login',actionUrl:'https://work.example/session',usernameField:'user',passwordField:'pass'}]};let armed=0;f.cb.prepareSubmission=async()=>{armed++;return {prepared:true};};f.c.webContents.executeJavaScriptInIsolatedWorld=async()=>{throw Error('Form changed');};const a=f.make(),[row]=await a.list();await assert.rejects(a.prepare({credentialRef:row.credentialRef}));assert.equal(armed,0);}});
|
||||
test('post-arm revocation and handoff completion failure cancel only the returned private lease',async()=>{for(const mode of ['revoked','after-fill-error']){const f=fixture();f.c.policy.agents.scopes.push('vault:prepare-submit');f.c.policy.passwordManager.controlledSubmission={enabled:true,forms:[{id:'login',actionUrl:'https://work.example/session',usernameField:'user',passwordField:'pass'}]};let cancelled=0,armed=0;f.c.webContents.executeJavaScriptInIsolatedWorld=async()=>({completed:true,formId:'login'});f.cb.prepareSubmission=async()=>{armed++;if(mode==='revoked')f.invalidate();return {prepared:true,cancel:()=>cancelled++};};if(mode==='after-fill-error')f.cb.afterFill=()=>{throw Error('Private UI error');};const a=f.make(),[row]=await a.list();await assert.rejects(a.prepare({credentialRef:row.credentialRef}),{message:'Managed vault operation refused'});assert.equal(armed,1);assert.equal(cancelled,1);await assert.rejects(a.prepare({credentialRef:row.credentialRef}));assert.equal(armed,1);}});
|
||||
Reference in new issue
Block a user