Add protected interaction evidence and measured debugging phases

This commit is contained in:
Karl committed 2026-10-13 18:04:02 +02:00
1 parent ea448de2d4
commit 244f01630b
42 files changed
+640 -87

No files matched your search

+60
View File
@@ -304,3 +304,63 @@ test('history can leave a start page but reload never invokes native callback th
await assert.rejects(f.actions.back({}),/Native target/);await assert.rejects(f.actions.forward({}),/Native target/);
await assert.rejects(f.actions.reload({}),/destination blocked/);assert.equal(called,2);
});
test('completed click forensic callback excludes selectors and text, and context changes suppress it',async()=>{
const events=[];const f=fixture({onInteraction:async value=>events.push(value)});
f.context.webContents.executeJavaScript=async()=>({completed:true,target:{tag:'button',role:'button',x:12,y:24,text:'SECRET',selector:'#private'}});
assert.deepEqual(await f.actions.click({selector:'#public'}),{completed:true});
assert.deepEqual(events,[{action:'click',sessionId:f.id,pageUrl:'https://work.example/',outcome:'completed',sensitiveField:false,target:{tag:'button',role:'button',x:12,y:24}}]);
f.context.webContents.executeJavaScript=async()=>{f.invalidate();return{completed:true,target:{tag:'button'}};};
await assert.rejects(f.actions.click({selector:'#public'}));assert.equal(events.length,1);
});
test('completed click callback failure is surfaced without repeating the committed click',async()=>{
let calls=0;const f=fixture({onInteraction:async()=>{throw Error('Evidence unavailable');}});
f.context.webContents.executeJavaScript=async()=>{calls++;return{completed:true};};
await assert.rejects(f.actions.click({selector:'#public'}),/Evidence unavailable/);assert.equal(calls,1);
});
test('session inventory classifies full titles and redacts corporate, semantic and credential content',async()=>{
for(const kind of ['corporate','semantic','credential','boundary']){
let calls=0,seen;const title=kind==='credential'?'password=SyntheticCredential':kind==='boundary'?'x'.repeat(150)+' password=SyntheticCredential':'Synthetic confidential title';
const f=fixture({getSessions:()=>[{id:'inventory',title,foreground:false}],inspect:async text=>{calls++;seen=text;return {decision:'block',ruleId:kind==='semantic'?'semantic-confidential':'corporate'};}});
f.context.policy.agents.allowSensitiveRead=true;
assert.deepEqual(await f.actions.sessions(),[{id:'inventory',title:'Protected page',foreground:false}]);
assert.equal(calls,['credential','boundary'].includes(kind)?0:1);if(calls)assert.equal(seen,title);
}
const title='x'.repeat(170),f=fixture({getSessions:()=>[{id:'inventory',title,foreground:true}],inspect:async text=>{assert.equal(text.length,170);return {decision:'allow'};}});
f.setUrl('about:blank');assert.equal((await f.actions.sessions())[0].title.length,160);
});
test('session inventory refuses unavailable inspection, changed policy, native context and inventory',async()=>{
for(const kind of ['missing','outage','incomplete','throw','abort','version','policy','token','title']){
const controller=new AbortController();let title='Safe title',f;
f=fixture({getSessions:()=>[{id:'inventory',title,foreground:true}],inspect:async()=>{
if(kind==='missing')return undefined;if(kind==='outage')return {decision:'block',ruleId:'inspector-unavailable'};
if(kind==='incomplete')return {decision:'block',ruleId:'corporate',coverage:{complete:false}};if(kind==='throw')throw Error('Scanner failed');
if(kind==='abort')controller.abort();if(kind==='version')f.setVersion(2);if(kind==='policy')f.context.policy={...f.context.policy};if(kind==='token')f.invalidate();if(kind==='title')title='Changed title';
return {decision:'allow'};
}});
await assert.rejects(f.actions.sessions({}, {signal:controller.signal}),/unavailable|changed|Scanner failed/);
}
});
test('session titles use real core decoding and classify corporate suffix beyond wire truncation',async()=>{
for(const title of ['sk-SyntheticFixture123456789012345','x'.repeat(160)+' confidential']){
const f=fixture({getSessions:()=>[{id:'inventory',title,foreground:true}],inspect:async text=>inspectPolicy(Buffer.from(text),{dlp:{maxBytes:10000,rules:[{id:'corporate',kind:'literal',value:'confidential'}]}})});
assert.equal((await f.actions.sessions())[0].title,'Protected page');
}
const title='sk%2DSyntheticFixture123456789012345',f=fixture({getSessions:()=>[{id:'inventory',title,foreground:true}],inspect:async text=>inspectPolicy(Buffer.from(text),{dlp:{maxBytes:10000,rules:[]}})});
assert.equal((await f.actions.sessions())[0].title,'Protected page');
});
test('inventory uses one bounded aggregate scan and masks all titles when any classified title blocks',async()=>{
for(const decision of ['allow','block']){
let calls=0;const rows=[{id:'public',title:'Public page',foreground:true},{id:'corporate',title:'Registered corporate page',foreground:false},{id:'credential',title:'password=SyntheticCredential',foreground:false}];
const f=fixture({getSessions:()=>rows,inspect:async text=>{calls++;assert.equal(text,'Public page\nRegistered corporate page');return {decision,ruleId:decision==='block'?'corporate':null};}});
const result=await f.actions.sessions();assert.equal(calls,1);assert.deepEqual(result.map(row=>row.id),rows.map(row=>row.id));
assert.deepEqual(result.map(row=>row.title),decision==='allow'?['Public page','Registered corporate page','Protected page']:['Protected page','Protected page','Protected page']);
}
for(const rows of [Array.from({length:1001},()=>({id:'inventory',title:'Safe'})),[{id:'inventory',title:'x'.repeat(201)}]]){
let calls=0;const f=fixture({getSessions:()=>rows,inspect:async()=>{calls++;return {decision:'allow'};}});await assert.rejects(f.actions.sessions(),/Invalid managed/);assert.equal(calls,0);
}
});