Implement managed browser security, individual agents and observability

This commit is contained in:
Karl committed 2026-10-13 16:58:15 +02:00
1 parent ff482ebe23
commit ea448de2d4
172 files changed
+5404 -347

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import vm from 'node:vm';
import {DiagnosticsSchema} from '../packages/core/diagnostics.mjs';
const html=readFileSync(new URL('../apps/server/public/index.html',import.meta.url),'utf8');
test('every centrally offered agent scope has an accessible structured control',()=>{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 scopes)assert.ok(html.includes(`id="agent-scope-${scope}"`),scope);for(const scope of ['inspect','scroll','library:organize-plan','library:organize-apply'])assert.ok(scopes.includes(scope));});
test('diagnostic admin roundtrip retains bounded export settings and denies auditors',()=>{const fields=new Map(),get=id=>{if(!fields.has(id))fields.set(id,{value:'',checked:false,disabled:false,addEventListener(){}});return fields.get(id);};let p={diagnostics:DiagnosticsSchema.parse({enabled:true,mode:'debug',endpoint:'https://collector.example',maxQueueSize:123,flushIntervalMs:333,maxRecordAgeMs:4567,exportTimeoutMs:999})};const context=vm.createContext({$:get,fill(){},draft:()=>structuredClone(p),applyOperatorPermissions(){},independentApprovalRequired:true,policy:null,currentPrincipal:null,updateDraftStatus(){}});vm.runInContext(readFileSync(new URL('../apps/server/public/diagnostics-admin.js',import.meta.url),'utf8'),context);context.fill(p);assert.deepEqual(DiagnosticsSchema.parse(context.draft().diagnostics),p.diagnostics);context.applyOperatorPermissions({role:'auditor',permissions:['policy.read']});assert.equal(get('diagnostics-enabled').disabled,true);context.applyOperatorPermissions({role:'operator',permissions:['policy.prepare']});assert.equal(get('diagnostics-enabled').disabled,false);get('diagnostics-enabled').checked=false;assert.equal(DiagnosticsSchema.parse(context.draft().diagnostics).enabled,false);});
test('SIEM control is administrator-only and writes the loaded revision',async()=>{const fields=new Map(),get=id=>{if(!fields.has(id))fields.set(id,{value:'',checked:false,disabled:false,hidden:false,textContent:'',addEventListener(type,fn){this.listener=fn;}});return fields.get(id);};const calls=[],context=vm.createContext({$:get,applyOperatorPermissions(){},load:async()=>{},task:fn=>fn,currentPrincipal:null,api:async(path,body,method)=>{calls.push({path,body,method});return {enabled:body?.enabled??true,revision:8,format:'ecs',pending:2,failures:0,configured:true};}});vm.runInContext(readFileSync(new URL('../apps/server/public/siem-admin.js',import.meta.url),'utf8'),context);context.currentPrincipal={role:'auditor',permissions:['policy.read']};context.applyOperatorPermissions(context.currentPrincipal);assert.equal(get('siem-controls').hidden,true);assert.equal(calls.length,0);await assert.rejects(get('siem-save').listener(),/Administrator/);context.currentPrincipal={role:'admin',permissions:['policy.read']};context.fillSIEMExport({enabled:true,revision:7,format:'cef'});context.applyOperatorPermissions(context.currentPrincipal);get('siem-enabled').checked=false;await get('siem-save').listener();assert.equal(calls[0].path,'/api/admin/siem');assert.equal(calls[0].method,'PUT');assert.equal(calls[0].body.enabled,false);assert.equal(calls[0].body.expectedRevision,7);});
import {FileTransfersSchema} from '../packages/core/file-transfer.mjs';
test('download scan settings remain explicit and cannot alter upload policy',()=>{const fields=new Map(),get=id=>{if(!fields.has(id))fields.set(id,{checked:false,disabled:false});return fields.get(id);};const policy={fileTransfers:FileTransfersSchema.parse({upload:{blockUnknown:true},download:{}}),security:{yara:{enabled:true,rules:[],scanOfficeMacros:true}}};const context=vm.createContext({$:get,fill(){},draft:()=>structuredClone(policy),applyOperatorPermissions(){},independentApprovalRequired:true,policy:null,currentPrincipal:null});vm.runInContext(readFileSync(new URL('../apps/server/public/download-scan-admin.js',import.meta.url),'utf8'),context);context.fill(policy);assert.equal(get('download-document-dlp').checked,true);get('download-document-dlp').checked=false;get('yara-office-macros').checked=false;const updated=context.draft();assert.equal(FileTransfersSchema.parse(updated.fileTransfers).download.requireDocumentDlp,false);assert.deepEqual(updated.fileTransfers.upload,policy.fileTransfers.upload);assert.equal(updated.security.yara.scanOfficeMacros,false);context.applyOperatorPermissions({role:'auditor',permissions:['policy.read']});assert.equal(get('yara-office-macros').disabled,true);});
@@ -0,0 +1,24 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import vm from 'node:vm';
import {readFileSync} from 'node:fs';
// Execute the shipped history review code against a minimal DOM, not a duplicate implementation.
const source=readFileSync(new URL('../apps/server/public/admin.js',import.meta.url),'utf8');
const history=source.slice(source.indexOf('let historyNextBefore='),source.indexOf("$('history-load').onclick="));
function fixture(comparison,permissions=['policy.write']){
const fields=new Map(),element=()=>({children:[],hidden:false,checked:false,disabled:false,textContent:'',append(...items){this.children.push(...items);},replaceChildren(...items){this.children=items;},scrollIntoView(){}}),get=id=>{if(!fields.has(id))fields.set(id,element());return fields.get(id);};
const context=vm.createContext({$:get,document:{createElement:element},publishedSnapshot:{version:5},operatorPermissions:new Set(permissions),independentApprovalRequired:false,api:async path=>path.startsWith('/api/admin/policy/diff')?comparison:{currentVersion:5},cell(){},task:fn=>fn});
vm.runInContext(history,context);
return {context,get,review:()=>context.reviewHistory(3)};
}
const empty=()=>({changes:[],truncated:false,groupDiff:{changes:[],truncated:false},profileDiff:{changes:[],truncated:false}});
test('profile-only restore displays assignments, grants and categories even when baseline/groups unchanged',async()=>{
const comparison=empty();comparison.profileDiff.changes=[{path:'/profiles/0/subjects',kind:'changed',before:['verified-admin'],after:['verified-user']},{path:'/profiles/0/deviceIds',kind:'changed',before:['one'],after:['two']},{path:'/profiles/0/features/devTools',kind:'changed',before:'allow',after:'deny'},{path:'/categories/0/hosts',kind:'changed',before:['*.company.example'],after:['wiki.company.example']}];
const f=fixture(comparison);await f.review();const rows=f.get('history-diff-list').children;
assert.equal(rows.length,4);assert.match(rows[0].children[0].textContent,/^\/userProfiles\/profiles\/0\/subjects/);assert.match(rows[0].children[1].textContent,/Selected version:.*verified-admin/);assert.match(rows[2].children[1].textContent,/Selected version: "allow"/);assert.match(f.get('history-review-summary').textContent,/0 baseline paths, 0 group paths and 4 user-profile/);assert.match(f.get('history-review-summary').textContent,/developer-tool grants/);assert.equal(f.get('history-review').hidden,false);assert.equal(f.get('history-approve').disabled,false);assert.equal(f.get('history-restore').disabled,true);
});
for(const part of ['baseline','group','profile'])test(`truncated ${part} diff never arms restoration`,async()=>{
const comparison=empty();if(part==='baseline')comparison.truncated=true;else comparison[`${part}Diff`].truncated=true;const f=fixture(comparison);await assert.rejects(f.review(),/supported review size/);assert.equal(f.get('history-review').hidden,true);assert.equal(f.get('history-restore').disabled,true);assert.equal(vm.runInContext('historyReview',f.context),null);
});
test('missing user-profile diff fails closed rather than treating it as unchanged',async()=>{const comparison=empty();delete comparison.profileDiff;const f=fixture(comparison);await assert.rejects(f.review(),/Complete baseline, group and user-profile/);assert.equal(f.get('history-review').hidden,true);assert.equal(vm.runInContext('historyReview',f.context),null);});
test('profile values are rendered as literal text and read-only identities cannot approve',async()=>{const comparison=empty();comparison.profileDiff.changes=[{path:'/profiles/0/name',kind:'changed',before:'<img src=x onerror=alert(1)>',after:'Current'}];const f=fixture(comparison,['policy.read']);await f.review();assert.match(f.get('history-diff-list').children[0].children[1].textContent,/<img src=x onerror=alert\(1\)>/);assert.equal(f.get('history-approve').disabled,true);assert.equal(f.get('history-restore').disabled,true);});
+132
View File
@@ -172,3 +172,135 @@ test('whole-application policy capabilities are opt-in and unknown scopes are re
assert.deepEqual(AgentsSchema.parse({enabled:true,scopes}).scopes,scopes);
assert.throws(()=>AgentsSchema.parse({enabled:true,scopes:['policy:override']}));
});
test('structured agent inspection classifies field values and rejects policy races',async()=>{
const f=fixture();f.context.policy.agents.scopes.push('inspect','scroll');
f.context.webContents.executeJavaScript=async()=>({controls:[{selector:'#search',role:'input',label:'Search',value:'Public query',text:''}]});
assert.equal((await f.actions.inspect({})).controls[0].value,'Public query');
f.context.webContents.executeJavaScript=async()=>({controls:[{value:'password=hunter22'}]});
await assert.rejects(f.actions.inspect({}),/Sensitive/);
f.context.webContents.executeJavaScript=async()=>{f.setVersion(2);return{controls:[]};};
await assert.rejects(f.actions.inspect({}),/changed/);
});
test('agent scroll is bounded and restricted to an interactive foreground session',async()=>{
const f=fixture();f.context.policy.agents.scopes.push('scroll');
assert.equal((await f.actions.scroll({x:0,y:200})).completed,true);
await assert.rejects(f.actions.scroll({x:0,y:-1}),/Invalid scroll/);
});
test('structured DOM inspection excludes credentials and hidden inputs',async()=>{
const f=fixture();f.context.policy.agents.scopes.push('inspect');
f.context.webContents.executeJavaScript=async code=>{
const element=(id,type,value,hidden=false)=>({id,type,value,tagName:'INPUT',parentElement:null,innerText:'',getAttribute:a=>a==='id'?id:null,closest:()=>null,getClientRects:()=>hidden?[]:[{}]});
const elements=[element('search','text','Public value'),element('password','password','PRIVATE_PASSWORD_SENTINEL'),element('token','text','PRIVATE_TOKEN_SENTINEL'),element('hidden','text','HIDDEN_SENTINEL',true)];
return vm.runInNewContext(code,{location:{href:'https://work.example/'},document:{body:{},querySelectorAll:s=>s.startsWith('#')?elements.filter(e=>'#'+e.id===s):elements},getComputedStyle:()=>({display:'block',visibility:'visible',opacity:'1'})});
};
const result=await f.actions.inspect({});assert.equal(result.controls.length,1);assert.equal(result.controls[0].selector,'#search');assert.equal(result.controls[0].value,'Public value');
});
function formFixture(node) {
const f=fixture(); f.context.policy.agents.scopes.push('select','check');
let clicks=0;
class Select {} Object.defineProperty(Select.prototype,'value',{set(value){this.value=value;}});
for(const option of node.options||[]) {option.getAttribute??=()=>null;option.parentElement??=node;}
node.getAttribute ??= ()=>null; node.closest ??= ()=>null; node.getClientRects ??= ()=>[{}]; node.dispatchEvent ??= ()=>{};
node.click ??= ()=>{clicks++;node.checked=!node.checked;};
f.context.webContents.executeJavaScript=async code=>vm.runInNewContext(code,{location:{href:'https://work.example/'},document:{body:{},querySelector:()=>node,querySelectorAll:()=>[node]},getComputedStyle:e=>e.style??{display:'block',visibility:'visible',opacity:'1'},HTMLSelectElement:Select,Event:class {constructor(type){this.type=type;}}});
return {...f,clicks:()=>clicks};
}
test('fixed select chooses a unique enabled existing option and checks hostile handlers',async()=>{
const node={tagName:'SELECT',value:'a',options:[{value:'a'},{value:'b'}]};
const f=formFixture(node);assert.equal((await f.actions.select({selector:'#choice',value:'b'})).completed,true);assert.equal(node.value,'b');
node.options.push({value:'b',getAttribute:()=>null,parentElement:node});await assert.rejects(f.actions.select({selector:'#choice',value:'b'}),/ambiguous/);node.options.pop();
node.options[1].disabled=true;await assert.rejects(f.actions.select({selector:'#choice',value:'b'}),/unavailable/);node.options[1].disabled=false;
node.options[1].parentElement={disabled:true};await assert.rejects(f.actions.select({selector:'#choice',value:'b'}),/unavailable/);node.options[1].parentElement=node;
node.options[1].getAttribute=a=>a==='data-sensitive'?'':null;await assert.rejects(f.actions.select({selector:'#choice',value:'b'}),/unavailable/);node.options[1].getAttribute=()=>null;
node.options[1].parentElement={hidden:true,getAttribute:()=>null,parentElement:node};await assert.rejects(f.actions.select({selector:'#choice',value:'b'}),/unavailable/);node.options[1].parentElement=node;
await assert.rejects(f.actions.select({selector:'#choice',value:'missing'}),/unavailable/);
node.dispatchEvent=()=>{node.value='a';};await assert.rejects(f.actions.select({selector:'#choice',value:'b'}),/changed/);
});
test('checkbox check is idempotent, rejects radio and verifies hostile toggles',async()=>{
const node={tagName:'INPUT',type:'checkbox',checked:false};const f=formFixture(node);
await f.actions.check({selector:'#choice',checked:true});assert.equal(f.clicks(),1);
await f.actions.check({selector:'#choice',checked:true});assert.equal(f.clicks(),1);
await f.actions.check({selector:'#choice',checked:false});assert.equal(f.clicks(),2);
node.type='radio';await assert.rejects(f.actions.check({selector:'#choice',checked:true}),/cannot be checked/);node.type='checkbox';
node.click=()=>{};await assert.rejects(f.actions.check({selector:'#choice',checked:true}),/changed/);
await assert.rejects(f.actions.check({selector:'#choice',checked:'true'}),/Invalid checked/);
});
test('select and check reject hidden, credential and disabled-ancestor targets',async()=>{
for(const mode of ['select','check'])for(const kind of ['hidden','password','disabled','ancestor','sensitive-empty','sensitive-body']) {
const node={tagName:mode==='select'?'SELECT':'INPUT',type:mode==='check'?'checkbox':'select-one',checked:false,options:[{value:'b'}]};const f=formFixture(node);
if(kind==='sensitive-empty')node.getAttribute=a=>a==='data-sensitive'?'':null;
if(kind==='sensitive-body')node.parentElement={parentElement:null,getAttribute:a=>a==='data-sensitive'?'':null};
if(kind==='hidden')node.getClientRects=()=>[];
if(kind==='password')node.getAttribute=a=>a==='name'?'password':null;
if(kind==='disabled')node.closest=()=>({});
if(kind==='ancestor')node.parentElement={parentElement:null,getAttribute:()=>null,style:{display:'none',visibility:'visible',opacity:'1'}};
await assert.rejects(f.actions[mode]({selector:'#choice',value:'b',checked:true}),/unavailable|sensitive/);
}
});
test('select and check retain native generation and scope checks around execution',async()=>{
for(const mode of ['select','check']) {
const f=fixture();f.context.policy.agents.scopes.push(mode);f.context.webContents.executeJavaScript=async()=>{f.invalidate();return {completed:true};};
await assert.rejects(f.actions[mode]({selector:'#choice',value:'b',checked:true}),/changed/);
const g=fixture();await assert.rejects(g.actions[mode]({selector:'#choice',value:'b',checked:true}),/scope/);
}
});
test('structured inspection returns bounded safe dropdown choices and checkbox state',async()=>{
const f=fixture();f.context.policy.agents.scopes.push('inspect');
const base={parentElement:null,getAttribute:()=>null,closest:()=>null,getClientRects:()=>[{}]};
const select={...base,id:'choice',tagName:'SELECT',value:'one',options:[]};
select.options=[{...base,parentElement:select,value:'one',label:'One'},{...base,parentElement:select,value:'locked',label:'Locked',disabled:true},{...base,parentElement:select,value:'secret',label:'SECRET_SENTINEL',getAttribute:a=>a==='data-sensitive'?'':null}];
const checkbox={...base,id:'accept',tagName:'INPUT',type:'checkbox',checked:true,value:'on'};const elements=[select,checkbox];
f.context.webContents.executeJavaScript=async code=>vm.runInNewContext(code,{location:{href:'https://work.example/'},document:{body:{},querySelectorAll:s=>s.startsWith('#')?elements.filter(e=>'#'+e.id===s):elements},getComputedStyle:()=>({display:'block',visibility:'visible',opacity:'1'})});
const result=await f.actions.inspect({});assert.equal(result.controls[0].options.length,2);assert.equal(result.controls[0].options[1].disabled,true);assert.equal(result.controls[1].checked,true);assert.equal(JSON.stringify(result).includes('SECRET_SENTINEL'),false);
});
test('native history refreshes intentional navigation while binding the original tab',async()=>{
for(const action of ['back','forward','reload']) {
let f,calls=0;
f=fixture({history:async(mode,initial,{signal})=>{
calls++;assert.equal(mode,action);assert.equal(initial.sessionId,f.id);assert.equal(signal,undefined);
f.setUrl('https://work.example/history');
return {completed:true,context:{...f.context,sessionId:f.id,foreground:true,policyVersion:1,token:{generation:1,version:1,url:'https://work.example/history'}}};
}});
f.context.policy.agents.scopes.push(action);
assert.deepEqual(await f.actions[action]({}),{completed:true});assert.equal(calls,1);
}
});
test('history actions require explicit scope, native callback and interactive foreground',async()=>{
for(const action of ['back','forward','reload']) {
const f=fixture();await assert.rejects(f.actions[action]({}),/scope/);
f.context.policy.agents.scopes.push(action);await assert.rejects(f.actions[action]({}),/unavailable/);
let called=false;
const callbacks={getContext:()=>({...f.context,sessionId:f.id,foreground:false,token:{}}),getSessions:()=>[],contextCurrent:()=>true,navigate:async()=>{},history:async()=>{called=true;},ai:async()=>{},inspect:async()=>({decision:'allow'}),audit:async()=>{}};
await assert.rejects(createAgentAdapters(callbacks)[action]({}),/unavailable/);assert.equal(called,false);
callbacks.getContext=()=>({...f.context,sessionId:f.id,foreground:true,interactive:false,token:{}});
await assert.rejects(createAgentAdapters(callbacks)[action]({}),/unavailable/);assert.equal(called,false);
}
});
test('history suppresses cancellation, policy races, switched tabs and blocked final destinations',async()=>{
for(const kind of ['abort','policy','identity','session','token','blocked','host','scheme','incomplete','scope']) {
let f;const controller=new AbortController();
f=fixture({history:async()=>{
f.setUrl(kind==='scheme'?'file:///private':'https://work.example/history');
if(kind==='abort')controller.abort();if(kind==='policy')f.setVersion(2);
if(kind==='blocked')f.context.destinationAllowed=()=>false;
if(kind==='host')f.context.policy.agents.allowedHosts=['other.example'];
if(kind==='scope')f.context.policy.agents.scopes=[];
return {completed:kind!=='incomplete',context:{...f.context,sessionId:kind==='session'?'other':f.id,foreground:true,webContents:kind==='identity'?{}:f.context.webContents,policyVersion:kind==='policy'?2:1,token:{generation:kind==='token'?0:1,version:1,url:'https://work.example/history'}}};
}});
f.context.policy.agents.scopes.push('back');
await assert.rejects(f.actions.back({}, {signal:controller.signal}),/changed|unavailable|blocked|scope/);
}
});
test('history can leave a start page but reload never invokes native callback there',async()=>{
let called=0;const f=fixture({history:async()=>{called++;throw Error('Native target unavailable');}});
f.context.policy.agents.scopes.push('back','forward','reload');f.setUrl('about:blank');
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);
});
+63
View File
@@ -0,0 +1,63 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {randomUUID} from 'node:crypto';
import audit from '../packages/core/agent-audit.cjs';
import {toECS,toCEF} from '../packages/core/siem.mjs';
test('agent audit binds every fixed action to its scope and rejects secret-bearing extras',()=>{
for(const [action,scope] of Object.entries(audit.AGENT_SCOPES)){
const outcome=action==='agent-request'?'denied':'completed';
assert.deepEqual(audit.agentAuditFacts({action,outcome}),{action,scope,outcome});
assert.throws(()=>audit.AgentAuditSchema.parse({action,scope:'wrong',outcome}));
}
for(const extra of ['args','prompt','url','principalId','token'])assert.throws(()=>audit.agentAuditFacts({action:'read',outcome:'completed',[extra]:'synthetic-secret'}));
assert.throws(()=>audit.agentAuditFacts({action:'agent-request',outcome:'authorized'}));
assert.throws(()=>audit.agentAuditFacts({action:'read',outcome:'completed',sessionId:'secret-path'}));
});
test('agent lifecycle exporters preserve trace and chain fields and exclude invalid metadata',()=>{
const sessionId=randomUUID(),metadata={agent:audit.agentAuditFacts({action:'type',outcome:'completed',sessionId}),ruleId:'type:completed',policyVersion:2,traceId:'a'.repeat(32),spanId:'b'.repeat(16)};
const event={id:7,time:new Date().toISOString(),kind:'agent.action',device_id:'device',metadata,audit:{hash:'hash',sequence:7}};
const ecs=toECS(event),cef=toCEF(event);
assert.deepEqual(ecs.safetynet.agent,{action:'type',scope:'browser:interact',outcome:'completed',session_id:sessionId});
for(const value of ['cs1Label=RuleId','cs2Label=AgentAction','cs3Label=AgentScope','cs4Label=TraceId','cs5Label=SpanId','cs6Label=AgentOutcome','flexString1Label=AuditRecordHash',`requestContext=${sessionId}`])assert.ok(cef.includes(value),value);
event.kind='download.released';assert.equal(toECS(event).safetynet.agent,undefined);assert.equal(toCEF(event).includes('AgentAction'),false);
event.kind='agent.denied';assert.equal(toECS(event).safetynet.agent,undefined);
metadata.agent={...metadata.agent,outcome:'denied',prompt:'synthetic-secret'};assert.equal(toECS(event).safetynet.agent,undefined);assert.equal(toCEF(event).includes('synthetic-secret'),false);
delete metadata.agent.prompt;assert.equal(toECS(event).safetynet.agent.outcome,'denied');
metadata.agent.outcome='failed';assert.equal(toECS(event).event.outcome,'failure');assert.deepEqual(toECS(event).event.type,['error']);
metadata.request={bytes:123,detectedType:'text'};assert.ok(toCEF(event).includes('cs6=failed'));assert.equal(toCEF(event).includes('DetectedRequestType'),false);
});
test('central agent facts ingestion validates kind pairing and rejects unstructured content',async t=>{
const {mkdtempSync,rmSync}=await import('node:fs'),{tmpdir}=await import('node:os'),{join,resolve}=await import('node:path');
const {openStore,digest}=await import('../apps/server/store.mjs'),{createApp}=await import('../apps/server/app.mjs');
const prefix=join(tmpdir(),'safetynet-agent-audit-'),dir=mkdtempSync(prefix),store=openStore(dir),credential='synthetic-device-agent-audit-token';
store.db.prepare('INSERT INTO devices(id,name,token_hash) VALUES(?,?,?)').run('device','Synthetic',digest(credential));
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();assert.ok(resolve(dir).startsWith(resolve(prefix)));rmSync(dir,{recursive:true,force:true});});
const send=body=>fetch(`http://127.0.0.1:${server.address().port}/api/device/events`,{method:'POST',headers:{authorization:`Bearer ${credential}`,'content-type':'application/json'},body:JSON.stringify({eventId:randomUUID(),policyVersion:1,...body})});
const agent=audit.agentAuditFacts({action:'read',outcome:'authorized',sessionId:randomUUID()});
assert.equal((await send({kind:'agent.action',agent})).status,200);
const attributed={...agent,principalId:'agent-'+randomUUID(),credentialGeneration:1};
assert.equal((await send({kind:'agent.action',agent:attributed})).status,200);
for(const partial of [{...agent,principalId:attributed.principalId},{...agent,credentialGeneration:1}])assert.equal((await send({kind:'agent.action',agent:partial})).status,400);
for(const body of [{kind:'agent.denied',agent},{kind:'browser.started',agent},{kind:'agent.action',agent:{...agent,scope:'browser:interact'}},{kind:'agent.action',agent:{...agent,prompt:'synthetic-secret'}}])assert.equal((await send(body)).status,400);
assert.equal((await send({kind:'agent.denied',agent:{...agent,outcome:'denied'}})).status,200);
assert.equal((await send({kind:'agent.action',ruleId:'read:completed'})).status,200); // Older enrolled client compatibility.
const rows=store.db.prepare('SELECT * FROM events').all();assert.equal(rows.length,4);assert.equal(rows.some(row=>row.metadata.includes('synthetic-secret')),false);assert.equal(store.auditIntegrity.verify().valid,true);
});
test('agent principal attribution is bounded, paired and never invented for legacy events',()=>{
const base={action:'read',outcome:'completed'},principalId='agent:'+randomUUID(),credentialGeneration=2;
const parsed=audit.agentAuditFacts({...base,principalId,credentialGeneration});
assert.equal(parsed.principalId,principalId);assert.equal(parsed.credentialGeneration,2);
for(const extra of [{principalId},{credentialGeneration},{principalId:'a'.repeat(81),credentialGeneration},{principalId:'bad principal',credentialGeneration},{principalId:'https://secret',credentialGeneration},{principalId,credentialGeneration:0},{principalId,credentialGeneration:1.5},{principalId,credentialGeneration:Number.MAX_SAFE_INTEGER+1},{principalId:null,credentialGeneration}])assert.throws(()=>audit.agentAuditFacts({...base,...extra}));
assert.equal(audit.agentAuditFacts(base).principalId,undefined);
const event={id:9,time:new Date().toISOString(),kind:'agent.action',metadata:{actor:'human-actor',agent:parsed,ruleId:'read:completed',traceId:'a'.repeat(32),spanId:'b'.repeat(16)},audit:{hash:'hash',sequence:9}};
const ecs=toECS(event),cef=toCEF(event);
assert.equal(ecs.safetynet.agent.principal_id,principalId);assert.equal(ecs.safetynet.agent.credential_generation,2);assert.equal(ecs.user.id,'human-actor');
for(const field of [`suid=${principalId}`,'flexNumber1Label=AgentCredentialGeneration','flexNumber1=2','suser=human-actor','cs1Label=RuleId','cs2Label=AgentAction','cs3Label=AgentScope','cs4Label=TraceId','cs5Label=SpanId','cs6Label=AgentOutcome','flexString1Label=AuditRecordHash'])assert.ok(cef.includes(field),field);
event.metadata.agent=audit.agentAuditFacts(base);assert.equal(toECS(event).safetynet.agent.principal_id,undefined);assert.equal(toCEF(event).includes('suid='),false);assert.equal(toCEF(event).includes('AgentCredentialGeneration'),false);
event.metadata.agent={...parsed,credentialGeneration:undefined};assert.equal(toECS(event).safetynet.agent,undefined);assert.equal(toCEF(event).includes('suid='),false);
});
+11
View File
@@ -0,0 +1,11 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
const require=createRequire(import.meta.url),{startAgentControl}=require('../apps/browser/agent-control.cjs'),{requestAgent}=require('../apps/browser/agent-transport-client.cjs');
test('bookmark and management failures produce static diagnostic reasons and retain fail-closed replies',async t=>{
const reports=[];let failure;
const api=await startAgentControl({authorize:async()=>({allowed:true,role:'agent',scopes:['bookmarks:mutate']}),actions:{'bookmarks:mutate':async()=>{throw Error(failure);}},audit:async()=>{},onError:async report=>reports.push(report),maxRequestsPerMinute:60});t.after(()=>api.close());
const reasons=['Management update in progress. Try again.','Managed policy changed during bookmark update','Bookmark update awaits management synchronization','Bookmark update requires a fresh signed policy','Management request refused','Managed application changed','Invalid bookmark details'];
for(const reason of reasons){failure=reason;const response=await requestAgent(api,{action:'bookmarks:mutate',args:{action:'add',expectedRevision:0,policyVersion:1,bookmark:{id:'fixture',title:'Fixture',url:'https://example.com/'}}});assert.equal(response.status,500);const body=await response.json();assert.equal(body.error,'Agent action failed');assert.equal('result' in body,false);assert.equal(reports.at(-1).reason,reason);assert.equal(reports.at(-1).action,'bookmarks:mutate');}
failure='Confidential unexpected error https://internal.example/?password=secret';const response=await requestAgent(api,{action:'bookmarks:mutate',args:{action:'delete',id:'fixture',expectedRevision:0,policyVersion:1}});assert.equal(response.status,500);assert.equal(reports.at(-1).reason,'unclassified-action-failure');assert.equal(JSON.stringify(reports).includes('password=secret'),false);
});
+60
View File
@@ -0,0 +1,60 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import {generateKeyPairSync} from 'node:crypto';
import channel from '../packages/core/policy-channel.cjs';
import control from '../apps/browser/agent-control.cjs';
import transport from '../apps/browser/agent-transport-client.cjs';
const input={action:'bookmarks:mutate',args:{action:'delete',id:'fixture',expectedRevision:7,policyVersion:9}};
const busy=()=>Object.assign(Error('Management update in progress. Try again.'),{status:409,code:'browser-management-busy'});
async function fixture(t,fn,authorize=async({scope})=>({allowed:true,role:'agent',scopes:[scope]})){
let attempts=0,mutations=0,challenges=0;const seen=[];
const api=await control.startAgentControl({authorize,audit:async()=>{},traceOperation:async(name,run)=>{if(name==='agent.challenge')challenges++;return run();},actions:{'bookmarks:mutate':async args=>{attempts++;seen.push(args);await fn(attempts);mutations++;return {completed:true};}}});
t.after(()=>api.close());return {api,stats:()=>({attempts,mutations,challenges,seen})};
}
test('authenticated busy retries use fresh challenges and preserve CAS inputs, mutation executes once',async t=>{
const x=await fixture(t,n=>{if(n<=2)throw busy();});const response=await transport.requestAgent(x.api,input);assert.equal(response.status,200);
const s=x.stats();assert.equal(s.attempts,3);assert.equal(s.mutations,1);assert.equal(s.challenges,3);for(const args of s.seen)assert.deepEqual(args,input.args);
});
test('busy retries stop after three retries',async t=>{
const x=await fixture(t,()=>{throw busy();});const response=await transport.requestAgent(x.api,input);assert.equal(response.status,409);assert.equal((await response.json()).code,'browser-management-busy');
assert.equal(x.stats().attempts,4);assert.equal(x.stats().challenges,4);assert.equal(x.stats().mutations,0);
});
test('CAS conflicts and denial never retry',async t=>{
const x=await fixture(t,()=>{throw Object.assign(Error('Bookmarks changed. Refresh before editing.'),{status:409});});assert.equal((await transport.requestAgent(x.api,input)).status,409);assert.equal(x.stats().attempts,1);
const denied=await fixture(t,()=>{},async()=>({allowed:false}));assert.equal((await transport.requestAgent(denied.api,input)).status,403);assert.equal(denied.stats().challenges,1);assert.equal(denied.stats().attempts,0);
});
test('unauthenticated plaintext busy code cannot trigger retry',async t=>{
let calls=0;const server=http.createServer((req,res)=>{calls++;res.writeHead(409,{'content-type':'application/json'});res.end(JSON.stringify({error:'Management update in progress. Try again.',code:'browser-management-busy'}));});await new Promise(r=>server.listen(0,'127.0.0.1',r));t.after(()=>new Promise(r=>server.close(r)));
const response=await transport.requestAgent({address:`http://127.0.0.1:${server.address().port}`,token:'synthetic',protocol:'safetynet-policy-mlkem1024-mldsa87-v1'},input);assert.equal(response.status,409);assert.equal(calls,1);assert.equal((await response.json()).code,undefined);
});
test('busy-looking message or code alone does not authorize retry',async t=>{
for(const error of [Object.assign(Error('Management update in progress. Try again.'),{status:409}),Object.assign(Error('Unrelated conflict'),{status:409,code:'browser-management-busy'})]){
const x=await fixture(t,()=>{throw error;});const response=await transport.requestAgent(x.api,input);assert.equal(response.status,409);assert.equal((await response.json()).code,undefined);assert.equal(x.stats().attempts,1);
}
});
async function rewriteOuterStatus(api,status,run){
const original=globalThis.fetch;
globalThis.fetch=async(url,options)=>{const response=await original(url,options);if(String(url)===api.address+'/v1/actions')return new Response(await response.text(),{status,headers:{'content-type':'application/json'}});return response;};
try{return await run();}finally{globalThis.fetch=original;}
}
test('authenticated busy 409 still retries when outer HTTP status is changed to 200',async t=>{
const x=await fixture(t,n=>{if(n<=2)throw busy();});
const response=await rewriteOuterStatus(x.api,200,()=>transport.requestAgent(x.api,input));assert.equal(response.status,200);assert.equal(x.stats().attempts,3);assert.equal(x.stats().mutations,1);
});
test('authenticated success 200 never retries or fails when outer HTTP status is changed to 409',async t=>{
const x=await fixture(t,()=>{});const response=await rewriteOuterStatus(x.api,409,()=>transport.requestAgent(x.api,input));assert.equal(response.ok,true);assert.equal(response.status,200);assert.equal(x.stats().attempts,1);assert.equal(x.stats().mutations,1);
});
test('missing or invalid authenticated status refuses the encrypted response',async t=>{
const ed=generateKeyPairSync('ed25519'),pq=generateKeyPairSync('ml-dsa-87');let item,status;
const server=http.createServer(async(req,res)=>{
const chunks=[];for await(const chunk of req)chunks.push(chunk);res.writeHead(200,{'content-type':'application/json'});
if(req.url==='/v1/challenge'){item=channel.challenge('local-agent',ed.privateKey,pq.privateKey);res.end(JSON.stringify(item.bundle));return;}
const wire=JSON.parse(Buffer.concat(chunks)),context=channel.receive(wire.handshake,item.bundle,item.privateKey);
try{res.end(JSON.stringify(channel.response({kind:'agent-response',...(status===undefined?{}:{status}),result:{completed:true}},item.bundle,context)));}finally{context.responseKey.fill(0);}
});await new Promise(r=>server.listen(0,'127.0.0.1',r));t.after(()=>new Promise(r=>server.close(r)));
const credential={address:`http://127.0.0.1:${server.address().port}`,token:'synthetic',protocol:channel.VERSION,edPublic:ed.publicKey.export({type:'spki',format:'pem'}),pqPublic:pq.publicKey.export({type:'spki',format:'pem'})};
for(const invalid of [undefined,199,600,409.5,'409']){status=invalid;await assert.rejects(transport.requestAgent(credential,input),/Protected agent response refused/);}
});
+87 -2
View File
@@ -1,14 +1,87 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import http from 'node:http';
const {requestAgent,authorization}=createRequire(import.meta.url)('../apps/browser/agent-transport-client.cjs');
const { startAgentControl } = createRequire(import.meta.url)('../apps/browser/agent-control.cjs');
const channel=createRequire(import.meta.url)('../packages/core/policy-channel.cjs');
async function fixture(t, overrides = {}) {
const calls = [], events = [];
const api = await startAgentControl({ authorize: async ({scope}) => ({allowed:true, role:'agent',scopes:[scope]}), actions: {sessions:async()=>[{id:'work',title:'Work',cookies:'secret'}], navigate:async args=>{calls.push(args);return{};}, read:async()=>({text:'sanitized',password:'secret'}),type:async()=>({}),click:async()=>({}),ai:async()=>({text:'answer'})},audit:async event=>events.push(event),...overrides });
t.after(()=>api.close());
const request = (action,args={},headers={})=>fetch(`${api.address}/v1/actions`,{method:'POST',headers:{'Content-Type':'application/json',Authorization:`Bearer ${api.token}`,...headers},body:JSON.stringify({action,args})});
const request = (action,args={},headers={})=>requestAgent(api,{action,args},{headers});
return {api, request,calls,events};
}
test('postquantum channel refuses plaintext, tampering and replays before adapter execution',async t=>{
const {api,calls}=await fixture(t);
const headers=(route,body='{}')=>({'Content-Type':'application/json',Authorization:authorization(api.token,route,body)});
const send=body=>fetch(`${api.address}/v1/actions`,{method:'POST',headers:headers('/v1/actions',JSON.stringify(body)),body:JSON.stringify(body)});
assert.equal((await send({action:'navigate',args:{url:'https://example.com'}})).status,400);
const bundle=await(await fetch(`${api.address}/v1/challenge`,{method:'POST',headers:headers('/v1/challenge'),body:'{}'})).json();
channel.verifyChallenge(bundle,'local-agent',api.edPublic,api.pqPublic);
const context=channel.prepare(bundle);
try{
const payload=channel.response({kind:'agent-request',action:'navigate',args:{url:'https://example.com'}},bundle,context);
const envelope={handshake:context.request,payload};
const wire=JSON.stringify(envelope);
assert.equal(wire.includes('https://example.com'),false);assert.equal(wire.includes('navigate'),false);
const result=await send(envelope);assert.equal(result.status,200);
const response=await result.json();assert.ok(response.encrypted);
assert.deepEqual(channel.finish(response,bundle,context),{kind:'agent-response',status:200,result:{completed:true}});
assert.equal((await send(envelope)).status,400);assert.equal(calls.length,1);
const bundle2=await(await fetch(`${api.address}/v1/challenge`,{method:'POST',headers:headers('/v1/challenge'),body:'{}'})).json();
const ctx2=channel.prepare(bundle2);
try{const changed=channel.response({kind:'agent-request',action:'navigate',args:{url:'https://example.com'}},bundle2,ctx2);changed.encrypted.tag='AAAAAAAAAAAAAAAAAAAAAA';assert.equal((await send({handshake:ctx2.request,payload:changed})).status,400);assert.equal(calls.length,1);}finally{ctx2.responseKey.fill(0);}
}finally{context.responseKey.fill(0);}
});
test('postquantum server identity must match protected pairing pins',async t=>{
const {api}=await fixture(t);
await assert.rejects(requestAgent({...api,pqPublic:api.edPublic},{action:'sessions'}));
});
test('proof of pairing possession is route-bound, non-secret and one-use',async t=>{
const {api}=await fixture(t);const proof=authorization(api.token,'/v1/challenge');
assert.equal(proof.includes(api.token),false);
const send=route=>fetch(`${api.address}${route}`,{method:'POST',headers:{'Content-Type':'application/json',Authorization:proof},body:'{}'});
assert.equal((await send('/v1/actions')).status,401);
assert.equal((await send('/v1/challenge')).status,200);
assert.equal((await send('/v1/challenge')).status,401);
const bound=authorization(api.token,'/v1/challenge','{}');
assert.equal((await fetch(`${api.address}/v1/challenge`,{method:'POST',headers:{'Content-Type':'application/json',Authorization:bound},body:'{"modified":true}'})).status,401);
});
test('bounded transport supports maximum multibyte action text',async t=>{
let text;const {request}=await fixture(t,{actions:{type:async args=>{text=args.text;return{};}}});
const value='界'.repeat(16384);assert.equal((await request('type',{selector:'#message',text:value})).status,200);assert.equal(text,value);
});
test('encrypted response bounds support maximum multibyte sanitized page text',async t=>{
const text='界'.repeat(65536);const {request}=await fixture(t,{actions:{read:async()=>({text})}});
const response=await request('read');assert.equal(response.status,200);assert.equal((await response.json()).result.text,text);
});
test('revocation during completion audit never releases successful content',async t=>{
let capability;const {api,request}=await fixture(t,{audit:async event=>{if(event.outcome==='completed')capability.revoke();}});capability=api;
const response=await request('read');assert.equal(response.status,403);assert.equal(Object.hasOwn(await response.json(),'result'),false);
});
test('future-dated possession proofs remain replay-protected for their entire acceptance window',async t=>{
const {api}=await fixture(t);const original=Date.now,now=original();let proof;
try{Date.now=()=>now+20000;proof=authorization(api.token,'/v1/challenge');Date.now=()=>now;
const send=()=>fetch(`${api.address}/v1/challenge`,{method:'POST',headers:{'Content-Type':'application/json',Authorization:proof},body:'{}'});
assert.equal((await send()).status,200);Date.now=()=>now+35000;assert.equal((await send()).status,401);
}finally{Date.now=original;}
});
test('client rejects plaintext success and tampered encrypted responses',async t=>{
const {api}=await fixture(t);let mode='plaintext';
const proxy=http.createServer(async(req,res)=>{
const chunks=[];for await(const chunk of req)chunks.push(chunk);
const upstream=await fetch(`${api.address}${req.url}`,{method:'POST',headers:{'Content-Type':'application/json',Authorization:authorization(api.token,req.url,Buffer.concat(chunks))},body:Buffer.concat(chunks)});
const value=await upstream.json();
if(req.url==='/v1/actions'){if(mode==='plaintext'){res.end(JSON.stringify({result:[{id:'forged'}]}));return;}value.encrypted.tag='AAAAAAAAAAAAAAAAAAAAAA';}
res.end(JSON.stringify(value));
});
await new Promise(resolve=>proxy.listen(0,'127.0.0.1',resolve));
t.after(()=>new Promise(resolve=>{proxy.closeAllConnections();proxy.close(resolve);}));
const credential={...api,address:`http://127.0.0.1:${proxy.address().port}`};
await assert.rejects(requestAgent(credential,{action:'sessions'}),/Protected agent response required/);
mode='tamper';await assert.rejects(requestAgent(credential,{action:'sessions'}));
});
test('loopback capability authenticates and strips private adapter fields',async t=>{
const {request,calls,events}=await fixture(t);
assert.equal((await request('sessions',{}, {Authorization:'Bearer wrong'})).status,401);
@@ -18,6 +91,15 @@ test('loopback capability authenticates and strips private adapter fields',async
assert.equal(calls.length,1); assert.ok(events.some(e=>e.outcome==='authorized'));
});
test('library organization actions are typed and never project credentials or executable instructions',async t=>{
const {request}=await fixture(t,{actions:{'library:organize-plan':async()=>({proposalId:'proposal-1',plan:{bookmarks:[{id:'b',folder:'Work',url:'https://secret.test'}],tabs:[{id:'t',group:'Research',script:'evil'}]},binding:{credential:'private'},expiresAt:{token:'private'}}),'library:organize-apply':async()=>({completed:true,token:'private'})}});
assert.equal((await request('library:organize-plan',{prompt:'execute JS'})).status,400);
const result=(await(await request('library:organize-plan')).json()).result;
assert.deepEqual(result,{proposalId:'proposal-1',plan:{bookmarks:[{id:'b',folder:'Work'}],tabs:[{id:'t',group:'Research'}]},expiresAt:null});
assert.equal((await request('library:organize-apply',{proposalId:'proposal-1',confirm:'yes'})).status,400);
assert.deepEqual((await(await request('library:organize-apply',{proposalId:'proposal-1',confirm:true})).json()).result,{completed:true});
});
test('omitted session remains omitted and native inventory exposes only approved foreground metadata',async t=>{
let captured;
const {request}=await fixture(t,{actions:{sessions:async()=>[{id:'71911cdc-a390-4e73-a604-d98d05fd52a5',title:'Work',foreground:true,cookies:'secret'}],read:async args=>{captured=args;return{text:'safe'};}}});
@@ -76,7 +158,7 @@ test('revocation immediately disables capability and policy denial changes live'
});
test('timeouts abort adapter and rate limit prevents repeated actions',async t=>{
let signal;
const {request}=await fixture(t,{timeoutMs:20,maxRequestsPerMinute:1,actions:{read:async(_,context)=>{signal=context.signal;await new Promise(resolve=>setTimeout(resolve,100));return{text:'late'};}}});
const {request}=await fixture(t,{timeoutMs:20,maxRequestsPerMinute:2,actions:{read:async(_,context)=>{signal=context.signal;await new Promise(resolve=>setTimeout(resolve,100));return{text:'late'};}}});
assert.equal((await request('read',{sessionId:'work'})).status,504);assert.equal(signal.aborted,true);
assert.equal((await request('read',{sessionId:'work'})).status,429);
});
@@ -94,3 +176,6 @@ test('trusted diagnostics report known static reasons and suppress arbitrary sen
assert.equal(diagnostics[1].reason,'unclassified-action-failure');
assert.equal(JSON.stringify(diagnostics).includes('do-not-log-this'),false);
});
test('fixed form actions enforce typed scopes and never accept caller code',async t=>{const calls=[];const {request}=await fixture(t,{actions:{select:async args=>{calls.push(args);return {completed:true};},check:async args=>{calls.push(args);return {completed:true};}}});assert.equal((await request('select',{sessionId:'work',selector:'#language',value:''})).status,200);assert.equal((await request('check',{sessionId:'work',selector:'#remember',checked:false})).status,200);for(const checked of ['true',1,null])assert.equal((await request('check',{selector:'#remember',checked})).status,400);assert.equal((await request('select',{selector:'#language',value:'one',script:'alert(1)'})).status,400);assert.equal(calls.length,2);});
test('inspection projects only bounded option and checkbox metadata',async t=>{const {request}=await fixture(t,{actions:{inspect:async()=>({text:'',controls:[{selector:'#language',role:'select',options:[{value:'en',label:'English',disabled:false,secret:'must-not-pass'}]},{selector:'#remember',role:'input',checked:true,cookies:'must-not-pass'}]})}});const r=await request('inspect');assert.equal(r.status,200);const data=await r.json();assert.equal(data.result.controls[0].options[0].value,'en');assert.equal(data.result.controls[1].checked,true);assert.equal(JSON.stringify(data).includes('must-not-pass'),false);});
+9
View File
@@ -0,0 +1,9 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {AgentsSchema,AgentGrantSchema} from '../packages/core/security.mjs';
import {defaultPolicy,PolicySchema} from '../packages/core/policy.mjs';
import {applyPolicyGroups} from '../packages/core/policy-groups.mjs';
import {applyUserProfiles} from '../packages/core/user-profiles.mjs';
const grant=()=>({id:'research-agent',name:'Research agent',enabled:true,expiresAt:'2099-12-31T23:59:59Z',scopes:['sessions','read'],allowedHosts:['*.company.example'],generation:1});
test('individual agent grants are explicit bounded unique and secret free',()=>{assert.equal(AgentsSchema.parse({}).identityMode,'legacy');assert.deepEqual(AgentsSchema.parse({}).grants,[]);const a=AgentsSchema.parse({identityMode:'individual',grants:[grant()]});assert.equal(a.identityMode,'individual');assert.deepEqual(a.grants[0],grant());for(const change of [{id:'bad id'},{generation:0},{generation:1.5},{expiresAt:'invalid'},{scopes:['not-a-scope']},{scopes:['read','read']},{privateKey:'secret'},{token:'secret'},{name:''}])assert.equal(AgentGrantSchema.safeParse({...grant(),...change}).success,false);assert.equal(AgentsSchema.safeParse({grants:[grant(),grant()]}).success,false);assert.equal(AgentsSchema.safeParse({grants:Array.from({length:33},(_,i)=>({...grant(),id:`a${i}`}))}).success,false);assert.deepEqual(AgentGrantSchema.parse({...grant(),scopes:[]}).scopes,[]);});
test('signed policy schema preserves individual grants through restrictive group and profile assignment',()=>{const p=defaultPolicy();p.agents=AgentsSchema.parse({enabled:true,identityMode:'individual',grants:[grant()]});const parsed=PolicySchema.parse(p);assert.deepEqual(parsed.agents.grants,[grant()]);const grouped=applyPolicyGroups(parsed,[{id:'locked',name:'Locked',memberDeviceIds:['one'],restrictions:{disableAgents:true},rollout:{stage:'all',deviceIds:[]}}],'one');assert.equal(grouped.agents.enabled,false);assert.equal(grouped.agents.identityMode,'individual');assert.deepEqual(grouped.agents.grants,[grant()]);const profiled=applyUserProfiles(parsed,{profiles:[{id:'restricted',name:'Restricted',deviceIds:['one'],features:{agents:'deny'}}]}, {id:'one'});assert.equal(profiled.agents.enabled,false);assert.deepEqual(profiled.agents.grants,[grant()]);assert.equal(p.agents.enabled,true);});
+38
View File
@@ -0,0 +1,38 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {mkdtempSync,rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {createRequire} from 'node:module';
import {openStore,digest} from '../apps/server/store.mjs';
import {createApp} from '../apps/server/app.mjs';
import {verifyPolicy} from '../packages/core/policy.mjs';
const {reconcileGenerationFloor}=createRequire(import.meta.url)('../apps/browser/agent-grants.cjs');
async function fixture(t){
const dir=mkdtempSync(join(tmpdir(),'safetynet-agent-publication-')),store=openStore(dir),origin='http://127.0.0.1:4310';
const server=createApp(store,{publicUrl:origin,secure:false,requireIndependentPolicyApproval:true}).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));
t.after(async()=>{await new Promise(r=>server.close(r));store.db.close();rmSync(dir,{recursive:true,force:true});});
async function api(path,method='GET',body,subject='author',role='admin'){
const credential=`synthetic-agent-publication-${subject}-12345678901234567890`;
store.db.prepare('INSERT OR REPLACE INTO sessions VALUES(?,?,?)').run(digest(credential),Date.now()+60000,JSON.stringify({sub:subject,role}));
const response=await fetch(`http://127.0.0.1:${server.address().port}${path}`,{method,headers:{cookie:`sn_session=${credential}`,origin,'content-type':'application/json'},body:body===undefined?undefined:JSON.stringify(body)});
return {status:response.status,body:await response.json()};
}
const approve=(review,subject='reviewer',role='admin')=>api(`/api/admin/policy-reviews/${review.reviewId}/approve`,'POST',{payloadHash:review.payloadHash,expectedVersion:review.expectedVersion,approve:true},subject,role);
return {store,api,approve,signed:()=>verifyPolicy(store.envelope(),store.publicKey())};
}
const grant=generation=>({id:'research-agent',name:'Research Agent',enabled:true,expiresAt:'2099-12-31T23:59:59Z',scopes:['sessions','read'],allowedHosts:['*.company.example'],generation});
test('individual grants require independent review and survive signed history restoration without credential resurrection',async t=>{
const f=await fixture(t),candidate=f.store.get('policy');candidate.version++;candidate.agents={...candidate.agents,enabled:true,identityMode:'individual',scopes:['sessions','read'],allowedHosts:['*.company.example'],grants:[grant(1)]};
const queued=await f.api('/api/admin/policy','PUT',candidate);assert.equal(queued.status,202);assert.equal(f.signed().agents.enabled,false);
const detail=await f.api(`/api/admin/policy-reviews/${queued.body.reviewId}`);assert.equal(detail.status,200);assert.ok(detail.body.diff.changes.some(c=>c.path==='/agents/grants'&&c.after[0].id==='research-agent'));
assert.equal((await f.approve(queued.body,'author')).status,403);assert.equal((await f.approve(queued.body,'auditor','auditor')).status,403);assert.equal((await f.approve(queued.body)).status,200);assert.deepEqual(f.signed().agents.grants,[grant(1)]);
const floor1=reconcileGenerationFloor(f.signed().agents.grants).floor;
const rotated=f.store.get('policy');rotated.version++;rotated.agents.grants[0].generation=2;rotated.agents.grants[0].scopes=['sessions'];const rotation=await f.api('/api/admin/policy','PUT',rotated);assert.equal(rotation.status,202);assert.equal((await f.approve(rotation.body)).status,200);
const floor2=reconcileGenerationFloor(f.signed().agents.grants,floor1).floor;assert.equal(floor2['research-agent'],2);
const restore=await f.api('/api/admin/policy/restore','POST',{sourceVersion:2,expectedVersion:3,approve:true});assert.equal(restore.status,202);assert.equal(f.signed().version,3);
const restoreReview=await f.api(`/api/admin/policy-reviews/${restore.body.reviewId}`);assert.ok(restoreReview.body.diff.changes.some(c=>c.path==='/agents/grants'&&c.after[0].generation===1));assert.equal((await f.approve(restore.body,'author')).status,403);assert.equal((await f.approve(restore.body)).status,200);
const restored=f.signed();assert.equal(restored.version,4);assert.deepEqual(restored.agents.grants,[grant(1)]);const effective=reconcileGenerationFloor(restored.agents.grants,floor2);assert.equal(effective.grants[0].enabled,false);assert.equal(effective.floor['research-agent'],2);
const history=await f.api('/api/admin/policy/history/2');assert.deepEqual(history.body.policy.agents.grants,[grant(1)]);
assert.equal((await f.api('/api/admin/policy','PUT',{...restored,version:5},'auditor','auditor')).status,403);assert.equal((await f.api('/api/admin/policy/restore','POST',{sourceVersion:2,expectedVersion:4,approve:true},'auditor','auditor')).status,403);assert.equal(f.signed().version,4);
});
+47
View File
@@ -0,0 +1,47 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import grants from '../apps/browser/agent-grants.cjs';
const now=Date.now(),expiry=new Date(now+60000).toISOString();
const policy=()=>({expiresAt:expiry,agents:{enabled:true,identityMode:'individual',scopes:['read','navigate'],allowedHosts:['corp.example'],grants:[{id:'worker.a',name:'Reader',enabled:true,expiresAt:expiry,scopes:['read'],allowedHosts:['corp.example'],generation:2}]}});
const authorize=changes=>grants.authorizeGrant({policy:policy(),ready:true,principalId:'worker.a',generation:2,action:'read',url:'https://corp.example/',hostMatches:(a,b)=>a===b,now,...changes});
test('active grant discovery excludes expired and disabled grants and exposes no mutable policy reference',()=>{
const p=policy();p.agents.grants.push({...p.agents.grants[0],id:'disabled',enabled:false},{...p.agents.grants[0],id:'expired',expiresAt:new Date(now-1).toISOString()});
const active=grants.activeGrants(p,now);assert.deepEqual(active.map(g=>g.id),['worker.a']);active[0].scopes.push('navigate');assert.deepEqual(p.agents.grants[0].scopes,['read']);
assert.deepEqual(grants.activeGrants(p,now+60001),[]);
});
test('individual authorization intersects global and principal rights and destinations',()=>{
assert.equal(authorize().allowed,true);
for(const changes of [{principalId:'worker.b'},{generation:1},{action:'navigate'},{ready:false},{url:'https://evil.example/'},{url:'file:///secret'},{now:now+60001}])assert.equal(authorize(changes).allowed,false);
const p=policy();p.agents.scopes=[];assert.equal(authorize({policy:p}).allowed,false);
p.agents.scopes=['read'];p.agents.grants[0].scopes=[];assert.equal(authorize({policy:p}).allowed,false);
p.agents.grants[0].scopes=['read'];p.agents.grants[0].allowedHosts=['other.example'];assert.equal(authorize({policy:p}).allowed,false);
});
test('legacy mode never accepts individual credentials and individual mode has no shared bypass',()=>{
assert.equal(authorize({principalId:undefined,generation:undefined}).allowed,false);
const p=policy();p.agents.identityMode='legacy';assert.equal(authorize({policy:p}).allowed,false);
assert.equal(authorize({policy:p,principalId:undefined,generation:undefined}).allowed,true);
delete p.agents.identityMode;assert.equal(authorize({policy:p,principalId:undefined,generation:undefined}).allowed,false);
});
test('generation floor survives removal and restart and disables restored older credentials',()=>{
const first=grants.reconcileGenerationFloor(policy().agents.grants,{});
const removed=grants.reconcileGenerationFloor([],JSON.parse(JSON.stringify(first.floor)));
const restored=grants.reconcileGenerationFloor([{...policy().agents.grants[0],generation:1}],removed.floor);
assert.equal(restored.grants[0].enabled,false);assert.equal(restored.floor['worker.a'],2);
assert.equal(grants.reconcileGenerationFloor([{...policy().agents.grants[0],generation:3}],restored.floor).grants[0].enabled,true);
});
test('protected credential publication is atomic, bounded to hashed names and stale-safe',async()=>{
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'agent-grants-'));
try{
const filePath=grants.descriptorPath(dir,'worker.a');assert.match(path.basename(filePath),/^agent-[a-f0-9]{64}\.json$/);
await grants.persistConnection({filePath,connection:{token:'synthetic-secret'},protect:async()=> 'opaque-dpapi',isCurrent:()=>true});
const prior=fs.readFileSync(filePath,'utf8');assert.equal(prior.includes('synthetic-secret'),false);
let current=true;
await assert.rejects(grants.persistConnection({filePath,connection:{token:'other'},protect:async()=>{current=false;return 'new';},isCurrent:()=>current}),/context changed/);
assert.equal(fs.readFileSync(filePath,'utf8'),prior);assert.equal(fs.readdirSync(dir).length,1);
const linked=path.join(dir,'linked.json');fs.linkSync(filePath,linked);
await assert.rejects(grants.persistConnection({filePath,connection:{},protect:async()=>'',isCurrent:()=>true}),/Unsafe credential/);
}finally{fs.rmSync(dir,{recursive:true,force:true});}
});
+23
View File
@@ -0,0 +1,23 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
const require=createRequire(import.meta.url),{startAgentControl}=require('../apps/browser/agent-control.cjs'),{requestAgent,authorization}=require('../apps/browser/agent-transport-client.cjs'),channel=require('../packages/core/policy-channel.cjs');
const grant=(id,more={})=>({id,generation:1,enabled:true,expiresAt:new Date(Date.now()+60000).toISOString(),scopes:['sessions:list'],...more});
async function fixture(t,extra={}){const grants=[grant('agent.one'),grant('agent-two')],events=[];const api=await startAgentControl({principalMode:'individual',principals:grants,authorize:async({scope,principalId,generation})=>({allowed:!!principalId&&generation===1,role:'agent',scopes:[scope]}),actions:{sessions:async()=>[{id:'work'}]},audit:async e=>events.push(e),...extra});t.after(()=>api.close());return {api,grants,events,one:api.issuePrincipal(...[grants[0].id,1,grants[0].expiresAt]),two:api.issuePrincipal(...[grants[1].id,1,grants[1].expiresAt])};}
const send=(c,route,body='{}')=>fetch(`${c.address}${route}`,{method:'POST',headers:{'Content-Type':'application/json',Authorization:authorization(c.token,route,body,c)},body});
test('individual principals have separate capabilities, authenticated identity and no shared master',async t=>{let persisted=false;const f=await fixture(t,{persistCredential:()=>{persisted=true;}});assert.equal(f.api.token,undefined);assert.equal(persisted,false);assert.notEqual(f.one.token,f.two.token);for(const c of [f.one,f.two]){const response=await requestAgent(c,{action:'sessions'});assert.equal(response.ok,true);}assert.ok(f.events.some(e=>e.principalId==='agent.one'&&e.generation===1&&e.outcome==='completed'));assert.ok(f.events.some(e=>e.principalId==='agent-two'&&e.outcome==='completed'));const r=await fetch(`${f.one.address}/v1/challenge`,{method:'POST',headers:{'Content-Type':'application/json',Authorization:authorization(f.one.token,'/v1/challenge')},body:'{}'});assert.equal(r.status,401);});
test('another principal cannot consume a signed challenge and client checks principal binding',async t=>{const f=await fixture(t);const bundle=await(await send(f.one,'/v1/challenge')).json();channel.verifyChallenge(bundle,f.one.principalBinding,f.one.edPublic,f.one.pqPublic);assert.throws(()=>channel.verifyChallenge(bundle,f.two.principalBinding,f.two.edPublic,f.two.pqPublic));const ctx=channel.prepare(bundle);try{const body=JSON.stringify({handshake:ctx.request,payload:channel.response({kind:'agent-request',action:'sessions',args:{}},bundle,ctx)});assert.equal((await send(f.two,'/v1/actions',body)).status,400);const r=await send(f.one,'/v1/actions',body);assert.equal(r.status,200);assert.equal(channel.finish(await r.json(),bundle,ctx).status,200);}finally{ctx.responseKey.fill(0);}});
test('individual revocation leaves other principal usable and old generation cannot return',async t=>{const f=await fixture(t);f.api.reconcilePrincipals([{...f.grants[0],enabled:false},f.grants[1]]);assert.equal((await send(f.one,'/v1/challenge')).status,401);assert.equal((await requestAgent(f.two,{action:'sessions'})).ok,true);const rotated={...f.grants[0],generation:2};f.api.reconcilePrincipals([rotated,f.grants[1]]);const newer=f.api.issuePrincipal(rotated.id,2,rotated.expiresAt);assert.notEqual(newer.token,f.one.token);assert.equal((await send(f.one,'/v1/challenge')).status,401);assert.equal((await send(newer,'/v1/challenge')).status,200);});
test('rights changes abort only owned operation and retain same-generation capability',async t=>{let started,release;const began=new Promise(r=>started=r),wait=new Promise(r=>release=r);const f=await fixture(t,{actions:{sessions:async(_args,{signal})=>{started();await wait;assert.equal(signal.aborted,true);return[{id:'work'}];}}});const pending=requestAgent(f.one,{action:'sessions'});await began;f.api.reconcilePrincipals([{...f.grants[0],scopes:[]},f.grants[1]]);release();assert.equal((await pending).status,403);assert.equal(f.api.issuePrincipal(f.one.principalId,1,f.grants[0].expiresAt).token,f.one.token);assert.equal((await send(f.two,'/v1/challenge')).status,200);});
test('principal expiry aborts in-flight result and removes owned capability',async t=>{let started,release;const began=new Promise(r=>started=r),wait=new Promise(r=>release=r);const f=await fixture(t,{actions:{sessions:async()=>{started();await wait;return[{id:'work'}];}}});const short={...f.grants[0],expiresAt:new Date(Date.now()+250).toISOString()};f.api.reconcilePrincipals([short,f.grants[1]]);const credential=f.api.issuePrincipal(short.id,1,short.expiresAt);const pending=requestAgent(credential,{action:'sessions'});await began;await new Promise(r=>setTimeout(r,350));release();assert.equal((await pending).status,403);assert.equal((await send(credential,'/v1/challenge')).status,401);});
import {AsyncLocalStorage} from 'node:async_hooks';
test('each protected RPC has one parent trace and timed named child stages without content',async t=>{const storage=new AsyncLocalStorage(),spans=[];let seq=0;const traceOperation=async(name,fn)=>{const parent=storage.getStore(),row={id:++seq,name,parent:parent?.id,start:performance.now()};spans.push(row);return storage.run(row,async()=>{try{return await fn();}finally{row.duration=performance.now()-row.start;}});};const f=await fixture(t,{traceOperation});assert.equal((await requestAgent(f.one,{action:'sessions'})).ok,true);const parents=spans.filter(s=>s.name==='agent.request');assert.equal(parents.length,2);assert.ok(parents.every(s=>s.parent===undefined&&s.duration>=0));const actionParent=parents.find(p=>spans.some(s=>s.parent===p.id&&s.name==='agent.dispatch'));for(const name of ['agent.decrypt','agent.authorize','agent.audit','agent.dispatch','agent.project','agent.encrypt'])assert.ok(spans.some(s=>s.parent===actionParent.id&&s.name===name&&s.duration>=0));assert.equal(JSON.stringify(spans).includes(f.one.token),false);});
test('individual release boundary reauthorization prevents a post-dispatch denial from releasing content',async t=>{let allowed=true;const f=await fixture(t,{authorize:async({scope})=>({allowed,role:'agent',scopes:[scope]}),actions:{sessions:async()=>{allowed=false;return[{id:'private-result'}];}}});const response=await requestAgent(f.one,{action:'sessions'});assert.equal(response.status,403);assert.equal(JSON.stringify(await response.json()).includes('private-result'),false);});
test('generation rollback stays revoked even when old grant is reenabled',async t=>{const f=await fixture(t);f.api.reconcilePrincipals([{...f.grants[0],generation:2},f.grants[1]]);f.api.reconcilePrincipals(f.grants);assert.equal((await send(f.one,'/v1/challenge')).status,401);assert.throws(()=>f.api.issuePrincipal(f.one.principalId,1,f.grants[0].expiresAt));assert.equal((await requestAgent(f.two,{action:'sessions'})).ok,true);});
test('forged individual authentication never attributes an unverified actor',async t=>{const f=await fixture(t);const forged={...f.one,token:f.two.token};const r=await send(forged,'/v1/challenge');assert.equal(r.status,401);assert.equal(f.events.length,1);assert.equal(f.events[0].principalId,undefined);assert.equal(f.events[0].generation,undefined);assert.equal(f.events[0].outcome,'denied');});
test('dispatch receives only the authenticated principal and generation',async t=>{let received;const f=await fixture(t,{actions:{sessions:async(_args,context)=>{received=context;return[{id:'work'}];}}});assert.equal((await requestAgent(f.two,{action:'sessions'})).ok,true);assert.equal(received.principalId,f.two.principalId);assert.equal(received.generation,f.two.generation);assert.ok(received.signal instanceof AbortSignal);});
test('revocation during asynchronous encryption never releases computed content',async t=>{let api;const f=await fixture(t,{traceOperation:async(name,fn)=>{const result=await fn();if(name==='agent.encrypt')api.reconcilePrincipals([]);return result;}});api=f.api;const response=await requestAgent(f.one,{action:'sessions'});assert.equal(response.status,403);assert.equal(JSON.stringify(await response.json()).includes('work'),false);});
test('readiness denial during encryption is rechecked before encrypted result release',async t=>{let allowed=true;const f=await fixture(t,{authorize:async({scope})=>({allowed,role:'agent',scopes:[scope]}),traceOperation:async(name,fn)=>{const result=await fn();if(name==='agent.encrypt')allowed=false;return result;}});const response=await requestAgent(f.one,{action:'sessions'});assert.equal(response.status,403);assert.equal(JSON.stringify(await response.json()).includes('work'),false);});
+20
View File
@@ -0,0 +1,20 @@
import {test} from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
const source=fs.readFileSync(new URL('../apps/browser/renderer.js',import.meta.url),'utf8');
function fixture(state){
const elements=new Map(),calls=[];let tick;
class Element{constructor(tag){this.tag=tag;this.children=[];this.open=false;this.listeners={};this.textContent='';}set id(value){this._id=value;elements.set(value,this);}get id(){return this._id;}setAttribute(){}append(...nodes){this.children.push(...nodes);}replaceChildren(){this.children=[];}addEventListener(name,fn){this.listeners[name]=fn;}showModal(){this.open=true;}close(){this.open=false;this.listeners.close?.();}}
const agents=new Element('button');agents.id='agents';
const context={lastState:state,Date,JSON,Number,Array,Error,setInterval:fn=>{tick=fn;},document:{createElement:tag=>new Element(tag),body:new Element('body')},$:id=>elements.get(id),requireSection:()=>{if(state.blocked)throw Error('managed');},call:async(method,id)=>{calls.push({method,id});return {address:'http://127.0.0.1:4567',credentialPath:'protected.json'};},updateChrome:async()=>{},task:fn=>fn,button:(label,fn)=>{const e=new Element('button');e.textContent=label;e.onclick=fn;return e;}};
vm.createContext(context);vm.runInContext(source.slice(source.indexOf('// Pairing selects'),source.indexOf("$('ai-form').onsubmit")),context);
return {context,elements,calls,tick:()=>tick()};
}
const grant={id:'reviewer',name:'Security reviewer',generation:2,expiresAt:new Date(Date.now()+60000).toISOString()};
test('individual pairing requires selecting a published identity and closes on revocation',async()=>{
const f=fixture({ready:true,agentsEnabled:true,agentIdentityMode:'individual',agentGrants:[grant]});await f.context.openAgentPairing();assert.equal(f.calls.length,0);const list=f.elements.get('agent-grant-list');assert.equal(list.children.length,1);await list.children[0].onclick();assert.equal(f.calls[0].id,'reviewer');assert.match(f.elements.get('agent-pairing-result').textContent,/Security reviewer/);f.context.lastState.agentGrants=[];f.tick();assert.equal(f.elements.get('agent-pairing').open,false);assert.equal(f.elements.get('agent-pairing-result').textContent,'');
});
test('empty individual mode cannot issue legacy credential and managed section rejects pairing',async()=>{
const state={ready:true,agentsEnabled:true,agentIdentityMode:'individual',agentGrants:[]},f=fixture(state);await f.context.openAgentPairing();assert.match(f.elements.get('agent-grant-list').children[0].textContent,/No active agent/);assert.equal(f.calls.length,0);state.blocked=true;await assert.rejects(f.context.openAgentPairing(),/managed/);
});
+29
View File
@@ -0,0 +1,29 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {mkdtempSync,rmSync,readFileSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {openStore} from '../apps/server/store.mjs';
import {createApp} from '../apps/server/app.mjs';
import {startSIEM} from '../apps/server/siem.mjs';
import {startTelemetry} from '../apps/server/telemetry.mjs';
import {adminRoutePermission} from '../apps/server/admin-access.mjs';
test('audit API verifies real transactional events and exposes bounded metadata-only proof',{timeout:20000},async t=>{
const directory=mkdtempSync(join(tmpdir(),'safetynet-audit-api-')),store=openStore(directory),credential='test-only-audit-administrator-123456789';
const sdk=startTelemetry();const server=createApp(store,{publicUrl:'http://127.0.0.1',adminToken:credential,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));await sdk?.shutdown();store.db.close();assert.ok(directory.startsWith(join(tmpdir(),'safetynet-audit-api-')));rmSync(directory,{recursive:true,force:true});});
const url=`http://127.0.0.1:${server.address().port}`,headers={authorization:`Bearer ${credential}`};
store.event(null,'dlp.block',{ruleId:'test',content:'SYNTHETIC_CONTENT_NOT_IN_PROOF'});
assert.equal((await fetch(url+'/api/admin/audit/integrity')).status,401);
assert.equal(adminRoutePermission('GET','/api/admin/audit/integrity'),'audit.read');
assert.equal(adminRoutePermission('GET','/api/admin/audit/entries'),'audit.read');
let response=await fetch(url+'/api/admin/audit/integrity',{headers});assert.equal(response.status,200);let result=await response.json();assert.equal(result.valid,true);assert.equal(result.authenticated,false);
response=await fetch(url+'/api/admin/audit/entries?limit=1',{headers});assert.equal(response.status,200);assert.equal((await response.text()).includes('SYNTHETIC_CONTENT_NOT_IN_PROOF'),false);
assert.equal((await fetch(url+'/api/admin/audit/entries?limit=1001',{headers})).status,400);
const exporter=startSIEM(store,directory,{});try{await exporter.flush();const event=JSON.parse(readFileSync(join(directory,'security.ndjson'),'utf8').trim());assert.equal(event.safetynet.audit.hash,store.auditIntegrity.linkage(1).hash);assert.equal(JSON.stringify(event).includes('SYNTHETIC_CONTENT_NOT_IN_PROOF'),false);}finally{exporter.stop();}
store.db.prepare("UPDATE events SET kind='changed' WHERE id=1").run();response=await fetch(url+'/api/admin/audit/integrity',{headers});result=await response.json();assert.equal(result.valid,false);assert.ok(result.errors.includes('event-digest'));
});
test('failed audit append rolls back event and Sigma alert atomically',t=>{
const directory=mkdtempSync(join(tmpdir(),'safetynet-audit-atomic-')),s=openStore(directory);t.after(()=>{s.db.close();assert.ok(directory.startsWith(join(tmpdir(),'safetynet-audit-atomic-')));rmSync(directory,{recursive:true,force:true});});
s.db.exec("CREATE TRIGGER reject_audit BEFORE INSERT ON audit_integrity BEGIN SELECT RAISE(ABORT,'audit fixture'); END");assert.throws(()=>s.event(null,'dlp.block',{}),/audit fixture/);assert.equal(s.db.prepare('SELECT count(*) n FROM events').get().n,0);
});
+10
View File
@@ -0,0 +1,10 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
const {createAuditIntegrityController}=createRequire(import.meta.url)('../apps/server/public/audit-integrity-admin.js');
function fixture(){let principal={sub:'one',role:'analyst',permissions:['audit.read']};const calls=[],renders=[];const view=createAuditIntegrityController({principal:()=>principal,request:path=>new Promise((resolve,reject)=>calls.push({path,resolve,reject})),render:(...args)=>renders.push(args),clear:()=>renders.push(['clear'])});return {view,calls,renders,change:p=>{principal=p;view.permissions();}};}
const result={valid:true,authenticated:false,externallyAnchored:false,head:{sequence:1,hash:'a'.repeat(64)}};
test('verification requires an explicit action and preserves unsigned/unanchored distinctions',async()=>{const f=fixture();f.view.permissions();assert.equal(f.calls.length,0);const pending=f.view.verify();assert.equal(f.calls[0].path,'/api/admin/audit/integrity');f.calls[0].resolve(result);await pending;assert.deepEqual(f.renders.at(-1),['result',result]);});
for(const change of ['identity','permission','reset'])test(`late integrity results are suppressed after ${change}`,async()=>{const f=fixture(),pending=f.view.verify();if(change==='reset')f.view.reset();else f.change({sub:change==='identity'?'two':'one',role:'analyst',permissions:change==='permission'?[]:['audit.read']});f.calls[0].resolve(result);await pending;assert.deepEqual(f.renders.at(-1),['clear']);});
test('backend error details and malformed integrity claims are suppressed',async()=>{const f=fixture();let pending=f.view.verify();f.calls[0].reject(Error('secret'));await pending;assert.deepEqual(f.renders.at(-1),['error']);pending=f.view.verify();f.calls[1].resolve({valid:true});await pending;assert.deepEqual(f.renders.at(-1),['error']);});
test('unprivileged identity cannot trigger verification',async()=>{const f=fixture();f.change(null);await f.view.verify();assert.equal(f.calls.length,0);});
+16
View File
@@ -0,0 +1,16 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {DatabaseSync} from 'node:sqlite';
import {initializeAuditIntegrity} from '../apps/server/audit-integrity.mjs';
function setup(legacy=false){const db=new DatabaseSync(':memory:');db.exec('CREATE TABLE events(id INTEGER PRIMARY KEY,time TEXT,device_id TEXT,kind TEXT,metadata TEXT)');if(legacy)db.prepare('INSERT INTO events VALUES(?,?,?,?,?)').run(1,'2026-01-01',null,'old','{}');const audit=initializeAuditIntegrity(db);function insert(id){db.prepare('INSERT INTO events VALUES(?,?,?,?,?)').run(id,'2026-01-01','device','test','{"bytes":4}');audit.append(db.prepare('SELECT * FROM events WHERE id=?').get(id));}return {db,audit,insert};}
test('audit detects event mutations and returns reconstructable proof',()=>{const {db,audit,insert}=setup();insert(1);assert.equal(audit.verify().valid,true);assert.equal(audit.verify().authenticated,false);assert.equal(audit.entries({}).entries[0].type,'event');db.exec("UPDATE events SET metadata='{}' WHERE id=1");assert.ok(audit.verify().errors.includes('event-digest'));db.prepare('INSERT INTO events VALUES(?,?,?,?,?)').run(2,'now',null,'audit.retention-applied','{}');audit.append(db.prepare('SELECT * FROM events WHERE id=2').get());assert.throws(()=>audit.authorizePrune([1],2),/changed/);db.close();});
test('unauthorized deletion and anchored tail truncation are detected',()=>{const {db,audit,insert}=setup();insert(1);insert(2);const expectedHead=audit.head();db.exec('DELETE FROM events WHERE id=2');assert.ok(audit.verify().errors.includes('missing-event'));db.exec('DELETE FROM audit_integrity WHERE seq=2');assert.ok(audit.verify({expectedHead}).errors.includes('anchor-mismatch'));db.close();});
test('authorized retention keeps digest verification without retaining plaintext',()=>{const {db,audit,insert}=setup();insert(1);db.prepare('INSERT INTO events VALUES(?,?,?,?,?)').run(2,'now',null,'audit.retention-applied','{}');audit.append(db.prepare('SELECT * FROM events WHERE id=2').get());audit.authorizePrune([1],2);db.exec('DELETE FROM events WHERE id=1');assert.equal(audit.verify().valid,true);assert.equal(audit.entries({}).entries[2].body.includes('bytes'),false);db.close();});
test('migration explicitly marks legacy and restart does not backfill orphan events',()=>{const {db,audit}=setup(true);assert.equal(audit.verify().legacyEvents,1);db.prepare('INSERT INTO events VALUES(?,?,?,?,?)').run(2,'now',null,'orphan','{}');assert.throws(()=>initializeAuditIntegrity(db),/verification failed/);assert.ok(audit.verify().errors.includes('uncovered-events'));assert.equal(audit.head().sequence,1);db.close();});
test('chain-body mutation, sequence gaps and malformed retention are detected',()=>{const {db,audit,insert}=setup();insert(1);db.exec("UPDATE audit_integrity SET body='{}' WHERE seq=1");assert.equal(audit.verify().valid,false);assert.throws(()=>audit.entries({limit:1001}));db.close();});
import {generateKeyPairSync} from 'node:crypto';
import {mkdtempSync,writeFileSync,rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {execFileSync} from 'node:child_process';
test('ML-DSA-87 signatures authenticate entries and require protected signer on restart',()=>{const dir=mkdtempSync(join(tmpdir(),'safetynet-audit-test-')),file=join(dir,'key.pem'),keys=generateKeyPairSync('ml-dsa-87');let db;try{writeFileSync(file,keys.privateKey.export({type:'pkcs8',format:'pem'}),{mode:0o600});if(process.platform==='win32'){execFileSync(join(process.env.SystemRoot,'System32/WindowsPowerShell/v1.0/powershell.exe'),['-NoProfile','-NonInteractive','-Command',"$ErrorActionPreference='Stop';$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User;$acl=New-Object System.Security.AccessControl.FileSecurity;$acl.SetOwner($sid);$acl.SetAccessRuleProtection($true,$false);$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($sid,'FullControl','Allow')));Set-Acl -LiteralPath $env:TEST_AUDIT_KEY -AclObject $acl"],{windowsHide:true,env:{SystemRoot:process.env.SystemRoot,TEST_AUDIT_KEY:file},stdio:'pipe'});}db=new DatabaseSync(':memory:');db.exec('CREATE TABLE events(id INTEGER PRIMARY KEY,time TEXT,device_id TEXT,kind TEXT,metadata TEXT)');const audit=initializeAuditIntegrity(db,{signingKeyFile:file});db.prepare('INSERT INTO events VALUES(?,?,?,?,?)').run(1,'now',null,'test','{}');audit.append(db.prepare('SELECT * FROM events').get());const trustedPublicKey=keys.publicKey.export({type:'spki',format:'pem'}).toString(),expectedHead=audit.head();assert.equal(audit.verify({trustedPublicKey,expectedHead}).externallyAnchored,true);assert.equal(audit.verify().authenticated,true);assert.throws(()=>initializeAuditIntegrity(db),/unavailable/);assert.equal(initializeAuditIntegrity(db,{signingKeyFile:file}).verify().valid,true);db.exec("UPDATE audit_integrity SET signature='AAAA' WHERE type='event'");assert.ok(audit.verify().errors.includes('signature'));const body=JSON.parse(db.prepare("SELECT body FROM audit_integrity WHERE type='event'").get().body);body.legacy=true;db.prepare("UPDATE audit_integrity SET body=?,signature=NULL,public_key=NULL WHERE type='event'").run(JSON.stringify(body));assert.ok(audit.verify().errors.includes('legacy-after-seal'));db.exec('DELETE FROM audit_integrity');assert.ok(audit.verify().errors.includes('missing-baseline-seal'));db.exec('DELETE FROM events');assert.throws(()=>initializeAuditIntegrity(db,{signingKeyFile:file}),/verification failed/);assert.equal(db.prepare('SELECT count(*) AS n FROM audit_integrity').get().n,0);}finally{db?.close();rmSync(dir,{recursive:true,force:true});}});
+10 -10
View File
@@ -10,30 +10,30 @@ const old='2020-01-01T00:00:00.000Z';
test('retention preserves pending export, incident evidence and replay deduplication',t=>{
const s=fixture(t);s.db.exec('CREATE TABLE IF NOT EXISTS incident_cases(id TEXT PRIMARY KEY,event_id INTEGER REFERENCES events(id))');
for(let i=0;i<4;i++)s.event('device','dlp.block',{ruleId:'rule'});
s.db.prepare('UPDATE events SET time=?').run(old);
s.db.prepare('INSERT INTO incident_cases(id,event_id) VALUES(?,?)').run('case',2);
s.db.prepare('INSERT INTO device_events VALUES(?,?)').run('device','replay-id');
const p=pruneAuditEvents(s,{days:30,exportedThrough:3});assert.equal(p.eligible,2);assert.equal(s.db.prepare('SELECT count(*) n FROM events').get().n,4);
const r=pruneAuditEvents(s,{days:30,exportedThrough:3,dryRun:false});assert.equal(r.eligible,2);
const p=pruneAuditEvents(s,{now:Date.now()+60*86400000,days:30,exportedThrough:3});assert.equal(p.eligible,2);assert.equal(s.db.prepare('SELECT count(*) n FROM events').get().n,4);
const r=pruneAuditEvents(s,{now:Date.now()+60*86400000,days:30,exportedThrough:3,dryRun:false});assert.equal(r.eligible,2);
assert.deepEqual(s.db.prepare('SELECT id FROM events ORDER BY id').all().map(x=>x.id),[2,4,5]);
assert.equal(s.db.prepare('SELECT count(*) n FROM device_events').get().n,1);
assert.equal(s.db.prepare('SELECT kind FROM events WHERE id=5').get().kind,'audit.retention-applied');
});
test('batch and SIEM boundaries never orphan a Sigma correlation',t=>{
const s=fixture(t),p=s.get('policy');p.security.sigma.rules=[{id:'r',title:'Rule',level:'high',detection:{selection:{kind:'dlp.block'},condition:'selection'}}];s.put('policy',p);s.event('d','dlp.block',{});s.db.prepare('UPDATE events SET time=?').run(old);
assert.equal(pruneAuditEvents(s,{days:30,limit:1,exportedThrough:2,dryRun:false}).eligible,0);
assert.equal(pruneAuditEvents(s,{days:30,exportedThrough:1,dryRun:false}).eligible,0);
assert.equal(pruneAuditEvents(s,{days:30,exportedThrough:2,dryRun:false}).eligible,2);
const s=fixture(t),p=s.get('policy');p.security.sigma.rules=[{id:'r',title:'Rule',level:'high',detection:{selection:{kind:'dlp.block'},condition:'selection'}}];s.put('policy',p);s.event('d','dlp.block',{});
assert.equal(pruneAuditEvents(s,{now:Date.now()+60*86400000,days:30,limit:1,exportedThrough:2,dryRun:false}).eligible,0);
assert.equal(pruneAuditEvents(s,{now:Date.now()+60*86400000,days:30,exportedThrough:1,dryRun:false}).eligible,0);
assert.equal(pruneAuditEvents(s,{now:Date.now()+60*86400000,days:30,exportedThrough:2,dryRun:false}).eligible,2);
assert.equal(s.db.prepare('SELECT count(*) n FROM alerts').get().n,0);
});
test('missing cursor fails closed and retention requires explicit deployment opt-in',t=>{
const s=fixture(t);s.event(null,'policy.published',{});s.db.prepare('UPDATE events SET time=?').run(old);
const s=fixture(t);s.event(null,'policy.published',{});
assert.throws(()=>pruneAuditEvents(s,{days:30}),/cursor/);
const off=startAuditRetention(s,{status:()=>({cursor:1})});off.run();assert.equal(off.status().enabled,false);assert.equal(s.db.prepare('SELECT count(*) n FROM events').get().n,1);off.stop();
});
test('failed deletion rolls back the whole retention operation',t=>{
const s=fixture(t);s.event(null,'policy.published',{});s.db.prepare('UPDATE events SET time=?').run(old);
const s=fixture(t);s.event(null,'policy.published',{});
s.db.exec("CREATE TRIGGER retention_reject BEFORE INSERT ON events WHEN NEW.kind='audit.retention-applied' BEGIN SELECT RAISE(ABORT,'fixture'); END");
assert.throws(()=>pruneAuditEvents(s,{days:30,exportedThrough:1,dryRun:false}),/fixture/);
assert.throws(()=>pruneAuditEvents(s,{now:Date.now()+60*86400000,days:30,exportedThrough:1,dryRun:false}),/fixture/);
assert.equal(s.db.prepare('SELECT count(*) n FROM events').get().n,1);
});
+10
View File
@@ -0,0 +1,10 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import vm from 'node:vm';
import {readFileSync} from 'node:fs';
function fixture(){const fields=new Map(),get=id=>{if(!fields.has(id))fields.set(id,{value:'',checked:false,disabled:false,hidden:false,files:[],addEventListener(type,fn){this[type]=fn;},removeAttribute(key){delete this[key];}});return fields.get(id);};const policy={brand:{name:'Example',color:'#123456',logoDataUrl:'data:image/png;base64,synthetic'},startPage:{title:'Team',subtitle:'Hello',newsEnabled:true,feedUrl:'https://news.example/feed'}};const context=vm.createContext({$:get,policy,fill(){},draft:()=>({brand:{name:'Edited',color:'#abcdef'},browser:{}}),applyOperatorPermissions(){},independentApprovalRequired:true,currentPrincipal:null,task:fn=>fn,updateDraftStatus(){},Uint8Array});vm.runInContext(readFileSync(new URL('../apps/server/public/branding-admin.js',import.meta.url),'utf8'),context);return {context,get};}
test('company draft retains logo during name edits and removes it explicitly',()=>{const {context,get}=fixture();assert.equal(context.draft().brand.logoDataUrl,'data:image/png;base64,synthetic');assert.equal(context.draft().brand.name,'Edited');assert.equal(context.draft().startPage.feedUrl,'https://news.example/feed');get('brand-logo-remove').click();assert.equal(context.draft().brand.logoDataUrl,undefined);assert.equal(get('brand-logo-preview').hidden,true);get('startpage-news-enabled').checked=false;get('startpage-feed-url').value='';assert.equal(context.draft().startPage.newsEnabled,false);assert.equal(context.draft().startPage.feedUrl,undefined);});
test('auditors cannot edit company policy; independent operators can prepare drafts',()=>{const {context,get}=fixture();context.applyOperatorPermissions({role:'auditor',permissions:['policy.read']});assert.equal(get('brand-logo-file').disabled,true);assert.equal(get('startpage-feed-url').disabled,true);context.applyOperatorPermissions({role:'operator',permissions:['policy.prepare']});assert.equal(get('brand-logo-file').disabled,false);});
test('logo decode rejects unsupported, oversized and forged PNG inputs before rasterization',async()=>{const {context}=fixture();await assert.rejects(context.encodeCompanyLogo({type:'image/svg+xml',size:10}),/PNG/);await assert.rejects(context.encodeCompanyLogo({type:'image/png',size:131073}),/128 KiB/);await assert.rejects(context.encodeCompanyLogo({type:'image/png',size:8,arrayBuffer:async()=>new Uint8Array(8).buffer}),/not a PNG/);});
import {StartPageSchema} from '../packages/core/branding.mjs';
test('admin start-page draft uses the signed policy root rather than unsupported browser settings',()=>{const {context}=fixture(),p=context.draft();assert.equal(p.browser.startPage,undefined);assert.equal(StartPageSchema.parse(p.startPage).feedUrl,'https://news.example/feed');});
+4
View File
@@ -0,0 +1,4 @@
import test from 'node:test';import assert from 'node:assert/strict';import {BrandSchema,StartPageSchema} from '../packages/core/branding.mjs';import {defaultPolicy,verifyPolicy} from '../packages/core/policy.mjs';import {createApp} from '../apps/server/app.mjs';import {openStore,digest} from '../apps/server/store.mjs';import {mkdtempSync,rmSync} from 'node:fs';import {join,resolve,sep} from 'node:path';import {tmpdir} from 'node:os';
const logo='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aL1sAAAAASUVORK5CYII=';
test('company PNG and feed policy reject active content and preserve conservative defaults',()=>{assert.equal(BrandSchema.parse({name:'Example',color:'#137c66',logoDataUrl:logo}).logoDataUrl,logo);for(const v of ['https://bad/logo.svg','data:image/svg+xml,<svg/>','data:image/png;base64,AAAA'])assert.throws(()=>BrandSchema.parse({name:'Example',color:'#137c66',logoDataUrl:v}));assert.equal(StartPageSchema.parse({}).newsEnabled,false);assert.throws(()=>StartPageSchema.parse({newsEnabled:true}));assert.throws(()=>StartPageSchema.parse({newsEnabled:true,feedUrl:'http://company/feed'}));assert.equal(StartPageSchema.parse({newsEnabled:true,feedUrl:'https://company.example/feed?List=42'}).newsEnabled,true);});
test('branding rolls out in signed device policy and news stays device authenticated',async t=>{const dir=mkdtempSync(join(tmpdir(),'safetynet-branding-')),store=openStore(dir),adminToken='synthetic-admin-credential-at-least-thirty-two',deviceToken='synthetic-device-credential-at-least-thirty-two',config={publicUrl:'http://127.0.0.1',secure:false,adminToken};store.db.prepare('INSERT INTO devices(id,name,platform,user,token_hash,last_seen) VALUES(?,?,?,?,?,?)').run('3b19cb2b-852f-4df7-bf88-bb16d164d130','Fixture','win32','User',digest(deviceToken),'now');const server=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));const base=`http://127.0.0.1:${server.address().port}`;t.after(async()=>{server.closeAllConnections();await new Promise(r=>server.close(r));store.db.close();if(!resolve(dir).startsWith(resolve(tmpdir())+sep))throw Error('Unsafe cleanup');rmSync(dir,{recursive:true,force:true});});const p=defaultPolicy();p.version=2;p.brand={name:'Example Company',color:'#224466',logoDataUrl:logo};p.startPage={title:'Company portal',subtitle:'Welcome to your company',newsEnabled:false};const publication=await fetch(base+'/api/admin/policy',{method:'PUT',headers:{authorization:`Bearer ${adminToken}`,'content-type':'application/json'},body:JSON.stringify(p)});assert.equal(publication.status,200);const signed=store.envelope('3b19cb2b-852f-4df7-bf88-bb16d164d130');const delivered=verifyPolicy(signed,store.publicKey());assert.equal(delivered.brand.logoDataUrl,logo);assert.equal(delivered.startPage.title,'Company portal');assert.equal((await fetch(base+'/api/device/start-page/news')).status,401);const news=await fetch(base+'/api/device/start-page/news',{headers:{authorization:`Bearer ${deviceToken}`}});assert.equal(news.status,200);assert.deepEqual(await news.json(),{policyVersion:2,news:[]});});
+22
View File
@@ -0,0 +1,22 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import vm from 'node:vm';
import {readFileSync} from 'node:fs';
import {createRequire} from 'node:module';
const require=createRequire(import.meta.url),source=readFileSync(new URL('../apps/browser/main.cjs',import.meta.url),'utf8');
const shipped=source.slice(source.indexOf('function auditAgent(event)'),source.indexOf('function applyPolicy(envelope)'));
function fixture(){const calls=[],context=vm.createContext({require:()=>require('../packages/core/agent-audit.cjs'),audit:(...args)=>calls.push(args)});vm.runInContext(shipped,context);return {run:context.auditAgent,calls};}
test('native agent audit preserves typed scope/session but never status, arguments or credentials',()=>{
const f=fixture(),sessionId='00000000-0000-4000-8000-000000000001';
f.run({action:'read',scope:'browser:read',outcome:'completed',sessionId,status:200,args:{password:'PRIVATE_ARGUMENT'},token:'PRIVATE_CAPABILITY'});
const [kind,rule,,,,,agent]=f.calls[0];assert.equal(kind,'agent.action');assert.equal(rule,'read:completed');assert.equal(agent.scope,'browser:read');assert.equal(agent.sessionId,sessionId);
assert.equal(JSON.stringify(f.calls).includes('PRIVATE_'),false);assert.deepEqual(Object.keys(agent).sort(),['action','outcome','scope','sessionId']);
});
test('failed and adapter-denied actions retain truthful outcomes and malformed sessions cannot suppress audit',()=>{
const f=fixture();f.run({action:'type',outcome:'failed',status:500,sessionId:'00000000-0000-f000-0000-000000000001'});
assert.equal(f.calls[0][0],'agent.denied');assert.equal(f.calls[0][6].outcome,'failed');assert.equal(f.calls[0][6].sessionId,undefined);
f.run({action:'agent.read',outcome:'blocked'});assert.equal(f.calls[1][6].action,'read');assert.equal(f.calls[1][6].outcome,'denied');
f.run({action:'agent-request',outcome:'denied',status:401});assert.equal(f.calls[2][6].scope,'agent:request');
});
test('browser retains authenticated individual actor without credential secrets',()=>{const f=fixture();f.run({action:'read',outcome:'completed',principalId:'security-agent',generation:3,token:'PRIVATE_TOKEN'});const agent=f.calls[0][6];assert.equal(agent.principalId,'security-agent');assert.equal(agent.credentialGeneration,3);assert.equal(JSON.stringify(f.calls).includes('PRIVATE_TOKEN'),false);});
+25
View File
@@ -0,0 +1,25 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import vm from 'node:vm';
import {readFileSync} from 'node:fs';
import {createRequire} from 'node:module';
const require=createRequire(import.meta.url),source=readFileSync(new URL('../apps/browser/main.cjs',import.meta.url),'utf8');
const shipped=source.slice(source.indexOf('async function reconcileAgentPolicy()'),source.indexOf('async function pairIndividualAgent'));
function fixture(){const events=[],grants=[{id:'reader',name:'Reader',generation:2,enabled:true,expiresAt:new Date(Date.now()+60000).toISOString(),scopes:['read'],allowedHosts:[]}];const service={revoke:()=>events.push('revoke'),close:async()=>events.push('close'),reconcilePrincipals:g=>events.push(g)};const context=vm.createContext({agentGrants:require('../apps/browser/agent-grants.cjs'),policy:{agents:{identityMode:'individual',enabled:true,grants},browser:{settings:{hiddenSections:[],lockedSections:[]}}},device:{},saveDevice:()=>events.push('save'),agentService:service,agentServiceMode:'individual',ready:false,fs:{unlinkSync:()=>{}},agentFile:()=>'/synthetic',featureAllowed:()=>false,managedSettings:()=>({})});vm.runInContext(shipped,context);return {context,events,service,run:context.reconcileAgentPolicy};}
test('accepted policy reconciles individual grants while temporary readiness pause preserves peer service',async()=>{const f=fixture();await f.run();assert.equal(f.context.agentService,f.service);assert.equal(f.events[0],'save');assert.equal(f.events[1][0].generation,2);assert.equal(f.events.includes('revoke'),false);f.context.policy.agents.grants[0].generation=1;await f.run();assert.equal(f.events.at(-1)[0].enabled,false);assert.equal(f.context.device.agentGenerationFloor.reader,2);});
test('mode changes and centrally hidden agents revoke old broker before reissue',async()=>{for(const mode of ['legacy','hidden']){const f=fixture();if(mode==='legacy')f.context.policy.agents.identityMode='legacy';else f.context.policy.browser.settings.hiddenSections=['agents'];await f.run();assert.ok(f.events.includes('revoke'));assert.ok(f.events.includes('close'));assert.equal(f.context.agentService,null);}});
test('stored library proposal cannot be applied by an actor excluded from its destinations',async()=>{
const helper=source.slice(source.indexOf('function assertAgentLibraryDestinations('),source.indexOf('function getLibraryOrganizer('));
const callback=source.split(/\r?\n/).find(line=>line.includes(' assertCurrent:(snapshot,')).trim().slice('assertCurrent:'.length,-1);
let actor={principalId:'planner'},commits=0;
const policy={version:1},identity={},snapshot={policy,identity,epoch:'stable',bookmarkRevision:0,bookmarks:[{id:'b',url:'https://excluded.example/private'}],tabs:[{id:'t',url:'https://excluded.example/work'}]};
const context=vm.createContext({agentActorStorage:{getStore:()=>actor},actorDestinationAllowed:url=>actor.principalId==='planner'||new URL(url).hostname==='allowed.example',requireLibraryAI:()=>{},policy,device:identity,nativeLibraryEpoch:()=> 'stable',personalBookmarks:{revision:0}});
vm.runInContext(helper+'\n globalThis.checkSnapshot='+callback+';',context);
const manager=require('../apps/browser/library-organization.cjs').createLibraryOrganization({capture:async()=>snapshot,assertCurrent:context.checkSnapshot,inspect:async()=>{},propose:async()=>({proposalId:'shared-proposal',plan:{bookmarks:[{id:'b',folder:'Work'}],tabs:[{id:'t',group:'Work'}]}}),commit:async()=>{commits++;return {version:1};},applyTabs:()=>{},audit:async()=>{}});
await manager.plan();actor={principalId:'restricted-applier'};
await assert.rejects(manager.apply({proposalId:'shared-proposal',confirm:true}),/Agent destination denied/);
assert.equal(commits,0);
});
test('individual network gates refuse ownerless traffic even after tab authority is cleared',()=>{const shippedOwner=source.slice(source.indexOf('function agentRequestOwnerAllowed(source)'),source.indexOf('function actorDestinationAllowed'));const c=vm.createContext({policy:{agents:{identityMode:'individual'}}});vm.runInContext(shippedOwner,c);assert.equal(c.agentRequestOwnerAllowed(null),false);assert.equal(c.agentRequestOwnerAllowed({guarded:false}),false);assert.equal(c.agentRequestOwnerAllowed({guarded:true,security:{agentAuthority:null}}),true);c.policy.agents.identityMode='legacy';assert.equal(c.agentRequestOwnerAllowed(null),true);assert.ok(source.includes('||!agentRequestOwnerAllowed(source)||'));});
+10
View File
@@ -0,0 +1,10 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import vm from 'node:vm';
import {DomainCategorySchema} from '../packages/core/user-profiles.mjs';
const source=readFileSync(new URL('../apps/server/public/profile-admin.js',import.meta.url),'utf8');
function fixture(){const fields=new Map();const get=id=>{if(!fields.has(id))fields.set(id,{value:'',checked:false,disabled:false,dataset:{},querySelectorAll:()=>[],addEventListener(){},replaceChildren(){}});return fields.get(id);};const context=vm.createContext({$:get,task:fn=>fn,currentPrincipal:null,structuredClone,document:{},independentApprovalRequired:false});vm.runInContext(source,context);return {context,get};}
test('category validation matches backend host, identifier, duplicate and bounds constraints',()=>{const {context}=fixture();const valid=[{id:'social',name:'Social',hosts:['*.social.example','intranet']}];assert.equal(vm.runInContext(`validateCategoryDraft(${JSON.stringify(valid)}).length`,context),1);DomainCategorySchema.parse(valid[0]);for(const change of [{id:'bad id'},{name:''},{hosts:['https://host.example']},{hosts:['host.example:443']},{hosts:['x','x']},{hosts:['a'.repeat(254)]}]){const category={...valid[0],...change};assert.equal(DomainCategorySchema.safeParse(category).success,false);assert.throws(()=>vm.runInContext(`validateCategoryDraft(${JSON.stringify([category])})`,context));}assert.throws(()=>vm.runInContext(`validateCategoryDraft(${JSON.stringify([valid[0],valid[0]])})`,context));});
test('dirty state retains original CAS base and referenced category removal remains locked',()=>{const {context,get}=fixture();vm.runInContext("categoryDraftVersion=12;profileState={version:13,profiles:[{name:'Employees',blockedCategoryIds:['social']}],categories:[]};categoryChanged()",context);assert.equal(vm.runInContext('categoryDraftVersion',context),12);assert.equal(vm.runInContext('categoryDraftDirty',context),true);assert.equal(vm.runInContext("categoryReferences('social').length",context),1);assert.match(get('profile-category-status').textContent,/v12/);assert.match(source,/expectedVersion:categoryDraftVersion/);assert.match(source,/if\(!categoryDraftDirty\)/);});
test('structured accessible controls replace mandatory JSON and retain explicit publication',()=>{const html=readFileSync(new URL('../apps/server/public/index.html',import.meta.url),'utf8');assert.ok(html.includes('id="profile-category-editor"'));assert.ok(html.includes('id="profile-category-status" role="status"'));assert.ok(!html.includes('id="profile-category-json"'));assert.match(source,/input.readOnly=profileState.categories.some/);assert.match(source,/button.dataset.categoryReferenced==='true'/);});
+36
View File
@@ -0,0 +1,36 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createCompanyNewsReader,parseCompanyNews,publicNewsAddress} from '../apps/server/company-news.mjs';
const policy={version:1,startPage:{newsEnabled:true,feedUrl:'https://news.example/feed'}};
const rss=Buffer.from('<rss><channel><item><title><![CDATA[<b>Company news</b> & update]]></title><link>https://intranet.example/article</link><pubDate>2026-10-13</pubDate></item><item><title>Bad URL</title><link>javascript:alert(1)</link></item></channel></rss>');
test('RSS and Atom produce bounded plain text and safe links',()=>{
assert.deepEqual(parseCompanyNews(rss),[{title:'Company news & update',url:'https://intranet.example/article',publishedAt:'2026-10-13T00:00:00.000Z'}]);
assert.equal(parseCompanyNews(Buffer.from('<feed><entry><title>Announcement</title><link href="https://company.example/story" rel="alternate"/></entry></feed>'))[0].title,'Announcement');
assert.throws(()=>parseCompanyNews(Buffer.from('<!DOCTYPE rss [<!ENTITY x SYSTEM "file:///secret">]><rss/>')));
assert.throws(()=>parseCompanyNews(Buffer.from('<rss><item></rss>')));
assert.throws(()=>parseCompanyNews(Buffer.alloc(524289)));
});
test('news disabled does not resolve or request anything',async()=>{
const read=createCompanyNewsReader({lookup:()=>{throw Error('Must not resolve');}});
assert.deepEqual(await read({version:1,startPage:{newsEnabled:false}}),[]);
});
test('public DNS is pinned and feed is cached without article requests',async()=>{
let calls=0;const read=createCompanyNewsReader({lookup:async()=>[{address:'8.8.8.8',family:4}],request:async(url,options)=>{calls++;assert.equal(options.address,'8.8.8.8');assert.equal(url.href,policy.startPage.feedUrl);return rss;}});
assert.equal((await read(policy)).length,1);await read(policy);assert.equal(calls,1);
});
test('SSRF rejects private, loopback, mapped IPv6, mixed DNS and numeric URLs',async()=>{
for(const address of ['10.0.0.8','127.0.0.1','::1','::ffff:127.0.0.1','169.254.169.254']){
assert.equal(publicNewsAddress(address),false);
const read=createCompanyNewsReader({lookup:async()=>[{address,family:address.includes(':')?6:4}],request:()=>{throw Error('Must not fetch');}});await assert.rejects(read(policy));
}
const mixed=createCompanyNewsReader({lookup:async()=>[{address:'8.8.8.8',family:4},{address:'10.0.0.8',family:4}]});await assert.rejects(mixed(policy));
const read=createCompanyNewsReader();for(const feedUrl of ['http://news.example/feed','https://user:[email protected]/feed','https://127.0.0.1/feed'])await assert.rejects(read({...policy,startPage:{...policy.startPage,feedUrl}}));
});
test('exact corporate allowlist permits private DNS but never loopback',async()=>{
const read=createCompanyNewsReader({allowedHosts:'news.example',lookup:async()=>[{address:'10.0.0.8',family:4}],request:async()=>rss});assert.equal((await read(policy)).length,1);
const loop=createCompanyNewsReader({allowedHosts:'news.example',lookup:async()=>[{address:'127.0.0.1',family:4}]});await assert.rejects(loop(policy));
const suffix=createCompanyNewsReader({allowedHosts:'example',lookup:async()=>[{address:'10.0.0.8',family:4}]});await assert.rejects(suffix(policy));
});
test('failed feeds are not cached',async()=>{
let calls=0;const read=createCompanyNewsReader({lookup:async()=>[{address:'8.8.8.8',family:4}],request:async()=>{calls++;throw Error('Offline');}});await assert.rejects(read(policy));await assert.rejects(read(policy));assert.equal(calls,2);
});
+5
View File
@@ -0,0 +1,5 @@
import test from 'node:test';import assert from 'node:assert/strict';import {normalizeCollectorBatch} from '../scripts/debug-collector-batch.mjs';
const span={traceId:'1234567890abcdef1234567890abcdef',spanId:'1234567890abcdef',name:'synthetic',startTimeUnixNano:'1000000000000000000',endTimeUnixNano:'1000000000001000000'};
const batch=value=>({resourceSpans:[{scopeSpans:[{spans:[value]}]}]});
test('collector diagnostics normalize valid timestamps, correlation and empty batches before mutation',()=>{assert.equal(normalizeCollectorBatch('traces',batch(span)).spans[0].durationMs,1);assert.deepEqual(normalizeCollectorBatch('traces',{}).spans,[]);assert.equal(normalizeCollectorBatch('logs',{resourceLogs:[]}).logs,0);assert.equal(normalizeCollectorBatch('metrics',{}).metrics,1);});
test('collector rejects unsafe durations, non-array hierarchy and invalid diagnostic identities',()=>{for(const value of [{...span,endTimeUnixNano:'100000000000000000'}, {...span,startTimeUnixNano:'0',endTimeUnixNano:String(BigInt(Number.MAX_SAFE_INTEGER)+1n)}, {...span,spanId:'0000000000000000'}, {...span,parentSpanId:123}, {...span,name:null}])assert.throws(()=>normalizeCollectorBatch('traces',batch(value)));for(const payload of [{resourceSpans:{}},{resourceSpans:[{scopeSpans:{}}]},{resourceSpans:[{scopeSpans:[{spans:{}}]}]}])assert.throws(()=>normalizeCollectorBatch('traces',payload));assert.throws(()=>normalizeCollectorBatch('metrics',{resourceMetrics:[{scopeMetrics:[{metrics:{}}]}]}));});
+19
View File
@@ -0,0 +1,19 @@
import test from 'node:test';import assert from 'node:assert/strict';import {mkdtempSync,rmSync,readdirSync,readFileSync} from 'node:fs';import {tmpdir} from 'node:os';import {join,resolve} from 'node:path';import {spawn} from 'node:child_process';import {createServer} from 'node:net';
test('actual Windows protected collector rotates encrypted exports and rejects oversized storage without acknowledgement',{skip:process.platform!=='win32'},async t=>{
const root=mkdtempSync(join(tmpdir(),'safetynet-collector-http-'));const probe=createServer();await new Promise(r=>probe.listen(0,'127.0.0.1',r));const port=probe.address().port;await new Promise(r=>probe.close(r));
const child=spawn(process.execPath,[resolve('scripts/debug-collector.mjs')],{windowsHide:true,stdio:'pipe',env:{...process.env,DEBUG_COLLECTOR_DIRECTORY:root,DEBUG_COLLECTOR_PORT:String(port),DEBUG_COLLECTOR_SEGMENT_BYTES:'1024',DEBUG_COLLECTOR_RETIRED_SEGMENTS:'1'}});
let output='';child.stdout.on('data',b=>output+=b);child.stderr.on('data',b=>output+=b);t.after(async()=>{if(child.exitCode===null){child.kill();await new Promise(r=>child.once('exit',r));}assert.ok(root.startsWith(join(tmpdir(),'safetynet-collector-http-')));rmSync(root,{recursive:true,force:true});});
const base=`http://127.0.0.1:${port}`;let health;
for(let i=0;i<80;i++){if(child.exitCode!==null)throw Error('Collector test process failed: '+output);try{health=await (await fetch(base+'/health')).json();break;}catch{await new Promise(r=>setTimeout(r,100));}}
assert.ok(health);const marker='SYNTHETIC_COLLECTOR_ROTATION';const post=body=>fetch(base+'/v1/logs',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)});
for(let i=0;i<12;i++)assert.equal((await post({resourceLogs:[{scopeLogs:[{logRecords:[{body:{stringValue:marker}}]}]}]})).status,200);
health=await (await fetch(base+'/api/summary')).json();assert.equal(health.requests,12);assert.equal(health.counts.logs,12);assert.ok(health.rotations>1);assert.ok(health.droppedSegments>0);assert.equal(health.retiredSegments,1);assert.ok(health.activeBytes+health.retiredBytes<=2048);
for(const name of readdirSync(root).filter(n=>n.endsWith('.ndjson'))){const content=readFileSync(join(root,name),'utf8');assert.equal(content.includes(marker),false);for(const line of content.trim().split('\n')){const envelope=JSON.parse(line);assert.equal(envelope.v,1);assert.ok(envelope.nonce&&envelope.tag&&envelope.ciphertext);}}
assert.equal((await post({large:'x'.repeat(2048)})).status,507);health=await (await fetch(base+'/health')).json();assert.equal(health.requests,12);assert.equal(health.rejectedWrites,1);
const before=structuredClone(health);const tracePost=body=>fetch(base+'/v1/traces',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)});
const span={traceId:'1234567890abcdef1234567890abcdef',spanId:'1234567890abcdef',name:'synthetic.stage',startTimeUnixNano:'10',endTimeUnixNano:'11'};
for(const bad of [{...span,endTimeUnixNano:'9'},{...span,startTimeUnixNano:'18446744073709551616'},{...span,traceId:123},{...span,endTimeUnixNano:'oops'},{...span,attributes:{}}])assert.equal((await tracePost({resourceSpans:[{scopeSpans:[{spans:[bad]}]}]})).status,400);
assert.equal((await tracePost({resourceSpans:{}})).status,400);assert.equal((await post({resourceLogs:[{scopeLogs:{}}]})).status,400);
health=await (await fetch(base+'/health')).json();for(const field of ['requests','storedBytes','activeBytes','legacyBytes','retiredBytes','rotations','droppedSegments','droppedBytes','rejectedWrites','retainedSpans'])assert.equal(health[field],before[field]);assert.deepEqual(health.counts,before.counts);
assert.equal((await tracePost({resourceSpans:[]})).status,200);assert.equal((await tracePost({resourceSpans:[{scopeSpans:[{spans:[span]}]}]})).status,200);const trace=await (await fetch(base+'/api/trace/'+span.traceId)).json();assert.equal(trace.spans[0].durationMs,0.000001);
});
+10
View File
@@ -0,0 +1,10 @@
import test from 'node:test';import assert from 'node:assert/strict';
import {mkdtempSync,writeFileSync,readFileSync,readdirSync,rmSync,mkdirSync,linkSync} from 'node:fs';import {tmpdir} from 'node:os';import {join,resolve} from 'node:path';
import {collectorStorage} from '../scripts/debug-collector-storage.mjs';
function fixture(t){const root=mkdtempSync(join(tmpdir(),'safetynet-collector-rotation-'));t.after(()=>{assert.ok(resolve(root).startsWith(resolve(tmpdir())+'\\safetynet-collector-rotation-')||resolve(root).startsWith(resolve(tmpdir())+'/safetynet-collector-rotation-'));rmSync(root,{recursive:true,force:true});});return root;}
test('collector exact budget rotates before append and pins original encrypted archive',t=>{const root=fixture(t),old='encrypted-old-record\n';writeFileSync(join(root,'signals.encrypted.ndjson'),old);const store=collectorStorage(root,{segmentBytes:1024,retiredSegments:2});store.append('x'.repeat(1024));assert.equal(readFileSync(join(root,'signals.legacy.encrypted.ndjson'),'utf8'),old);assert.equal(store.summary().storedBytes,1024);assert.equal(store.summary().rotations,1);});
test('rolling developer segments are bounded across restart while legacy remains pinned',t=>{const root=fixture(t);let store=collectorStorage(root,{segmentBytes:1024,retiredSegments:2});for(let i=0;i<8;i++)store.append(String(i).repeat(1024));let summary=store.summary();assert.equal(summary.retiredSegments,2);assert.equal(summary.droppedSegments,4);assert.equal(summary.activeBytes+summary.retiredBytes,3072);assert.equal(readFileSync(join(root,'signals.legacy.encrypted.ndjson'),'utf8'),'0'.repeat(1024));store=collectorStorage(root,{segmentBytes:1024,retiredSegments:2});store.append('z');assert.equal(store.summary().retiredSegments,2);assert.equal(readFileSync(join(root,'signals.legacy.encrypted.ndjson'),'utf8'),'0'.repeat(1024));});
test('oversized batch is rejected without writing or retiring archive',t=>{const root=fixture(t),store=collectorStorage(root,{segmentBytes:1024});assert.throws(()=>store.append('x'.repeat(1025)));assert.equal(store.summary().rejectedWrites,1);assert.equal(store.summary().rotations,0);assert.equal(readdirSync(root).length,0);});
test('unexpected entry and directory substitution fail closed before deletion or append',t=>{const root=fixture(t),store=collectorStorage(root,{segmentBytes:1024});store.append('x'.repeat(1024));writeFileSync(join(root,'unrelated.txt'),'preserve');assert.throws(()=>store.append('z'));assert.equal(readFileSync(join(root,'unrelated.txt'),'utf8'),'preserve');assert.equal(readFileSync(join(root,'signals.encrypted.ndjson'),'utf8').length,1024);});
test('retention refuses unexpected directory masquerading as generated segment',t=>{const root=fixture(t);mkdirSync(join(root,'signals.rolling.1234567890123.aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa.encrypted.ndjson'));assert.throws(()=>collectorStorage(root,{segmentBytes:1024}));});
test('hardlinked encrypted target is refused without altering its other name',t=>{const root=fixture(t),outside=fixture(t),path=join(outside,'preserved');writeFileSync(path,'external-original');linkSync(path,join(root,'signals.encrypted.ndjson'));assert.throws(()=>collectorStorage(root,{segmentBytes:1024}));assert.equal(readFileSync(path,'utf8'),'external-original');});
+24
View File
@@ -15,4 +15,28 @@ test('browser diagnostics exports real OTLP parent/child traces and correlated s
test('disable and reconfiguration discard queued generation before export',{timeout:10000},async t=>{const {requests,endpoint}=await collector(t),diagnostics=createDiagnostics();t.after(()=>diagnostics.shutdown());diagnostics.configure(config(endpoint));await diagnostics.span('browser.navigation',{operation:'navigate'},async()=>{});diagnostics.event('browser.security',{decision:'block'});diagnostics.configure({enabled:false});await diagnostics.flush();await delay(100);assert.equal(requests.length,0);let calls=0;await diagnostics.span('browser.navigation',{},async()=>{calls++;});assert.equal(calls,1);assert.equal(diagnostics.status().enabled,false);diagnostics.configure(config(endpoint),{policyVersion:9});await diagnostics.span('browser.navigation',{operation:'navigate'},async()=>{});await diagnostics.flush();assert.equal(spans(requests).length,1);assert.equal(spans(requests)[0].attributes.find(a=>a.key==='safetynet.policy.version').value.intValue,9);diagnostics.configure({...config(endpoint),endpoint:'http://10.0.0.1:4318'});assert.equal(diagnostics.status().enabled,false);});
test('diagnostics bounded queues expire old records and never repeat failed security operations',{timeout:10000},async t=>{const {requests,endpoint}=await collector(t),diagnostics=createDiagnostics();t.after(()=>diagnostics.shutdown());diagnostics.configure({...config(endpoint),maxQueueSize:16,maxRecordAgeMs:1000});await diagnostics.span('browser.request',{requestType:'xhr'},async()=>{});await delay(1100);await diagnostics.flush();assert.equal(requests.length,0,'Expired diagnostic record exported');diagnostics.configure({...config(endpoint),maxQueueSize:16});for(let i=0;i<50;i++)diagnostics.event('browser.request',{requestType:'xhr'});await diagnostics.flush();assert.ok(logs(requests).length<=50);for(const request of requests)if(request.path.endsWith('/logs'))assert.ok(logs([request]).length<=16,'Export batch exceeded bounded queue');let calls=0;diagnostics.configure({...config(endpoint),endpoint:'http://127.0.0.1:1',exportTimeoutMs:250});assert.equal(await diagnostics.span('browser.request',{},async()=>{calls++;return 'security decision';}),'security decision');await diagnostics.flush();assert.equal(calls,1);assert.ok(diagnostics.status().failedExports>=1);});
test('diagnostic attributes are typed and never invoke hostile getters',()=>{const input={operation:'read',sessionId:'not-a-uuid',status:'ok',secret:'never',bytes:32};Object.defineProperty(input,'prompt',{enumerable:true,get(){throw Error('getter called');}});assert.deepEqual(attributes(input),{'safetynet.operation':'read','safetynet.status':'ok','safetynet.bytes':32});});
test('fixed diagnostic reasons export in real OTLP fault spans and correlated logs while injected reasons are discarded',{timeout:15000},async t=>{
const {requests,endpoint}=await collector(t),diagnostics=createDiagnostics();t.after(()=>diagnostics.shutdown());diagnostics.configure(config(endpoint));
const reasons=['request-source-context-changed','header-source-context-changed','headers-inspection-timeout','request-inspection-timeout','upload-lease-lost','upload-lease-release-failed','invalid-source-owner','policy-changed'];
const secret='PASSWORD_REASON_SECRET_MUST_NEVER_EXPORT';
for(const reason of reasons)await diagnostics.span('browser.upload.fault',{reason,status:'error'},async()=>diagnostics.event('browser.security',{reason,decision:'block'}));
await diagnostics.span('browser.upload.fault',{reason:secret},async()=>diagnostics.event('browser.security',{reason:secret}));
await diagnostics.flush();const traceSpans=spans(requests),records=logs(requests);
assert.equal(traceSpans.length,reasons.length+1);
for(const reason of reasons){
const span=traceSpans.find(s=>s.attributes.some(a=>a.key==='safetynet.reason'&&a.value.stringValue===reason));assert.ok(span,reason);assert.equal(span.name,'browser.upload.fault');
assert.ok(records.some(log=>log.traceId===span.traceId&&log.spanId===span.spanId&&log.attributes.some(a=>a.key==='safetynet.reason'&&a.value.stringValue===reason)),reason);
}
assert.equal(traceSpans.filter(s=>s.attributes.some(a=>a.key==='safetynet.reason')).length,reasons.length);
assert.equal(JSON.stringify(requests).includes(secret),false);
for(const reason of [null,42,{},'upload-lease-lost '+secret,'__proto__'])assert.deepEqual(attributes({reason}),{});
const hostile={};Object.defineProperty(hostile,'reason',{enumerable:true,get(){throw Error('getter must not run');}});assert.deepEqual(attributes(hostile),{});
});
test('OTLP redirects do not forward diagnostics and zero sampling suppresses records',{timeout:10000},async t=>{const {requests,endpoint}=await collector(t);const redirect=createServer((_req,res)=>{res.statusCode=307;res.setHeader('location',endpoint+'/v1/traces');res.end('redirect');}).listen(0,'127.0.0.1');await new Promise(r=>redirect.once('listening',r));t.after(()=>new Promise(r=>{redirect.closeAllConnections();redirect.close(r);}));const diagnostics=createDiagnostics();t.after(()=>diagnostics.shutdown());diagnostics.configure(config(`http://127.0.0.1:${redirect.address().port}`));await diagnostics.span('browser.navigation',{},async()=>{});await diagnostics.flush();assert.equal(requests.length,0,'Collector redirect received diagnostic payload');assert.ok(diagnostics.status().failedExports>=1);diagnostics.configure({...config(endpoint),sampleRate:0,debugSampleRate:0});await diagnostics.span('browser.ipc',{operation:'state'},async()=>{assert.deepEqual(diagnostics.correlation(),{});diagnostics.event('browser.security',{decision:'block'});});await diagnostics.flush();assert.equal(requests.length,0);});
test('inspector timing batches export exact nested spans only inside live debug operations',{timeout:15000},async t=>{const {requests,endpoint}=await collector(t),diagnostics=createDiagnostics();t.after(()=>diagnostics.shutdown());diagnostics.configure(config(endpoint));const base=Date.now()-100,batch={baseUnixMs:base,secret:'STAGE_SECRET',records:[{id:1,parentId:null,stage:'inspect',startOffsetMs:0,durationMs:80,status:'ok',secret:'STAGE_SECRET'},{id:2,parentId:1,stage:'parse',startOffsetMs:10,durationMs:20,status:'ok'},{id:3,parentId:1,stage:'semantic',startOffsetMs:35,durationMs:40,status:'error'}]};diagnostics.importStages(batch);await diagnostics.span('browser.inspect.local',{},async()=>{diagnostics.importStages(batch);});await diagnostics.flush();const output=spans(requests),outer=output.find(s=>s.name==='browser.inspect.local'),inspect=output.find(s=>s.name==='inspector.inspect'),parse=output.find(s=>s.name==='inspector.parse'),semantic=output.find(s=>s.name==='inspector.semantic');assert.equal(output.length,4);assert.equal(inspect.parentSpanId,outer.spanId);assert.equal(parse.parentSpanId,inspect.spanId);assert.equal(semantic.traceId,outer.traceId);assert.equal(semantic.status.code,2);assert.equal(BigInt(parse.startTimeUnixNano),BigInt(base+10)*1000000n);assert.equal(BigInt(parse.endTimeUnixNano)-BigInt(parse.startTimeUnixNano),20000000n);assert.equal(JSON.stringify(requests).includes('STAGE_SECRET'),false);
diagnostics.configure({...config(endpoint),mode:'operational'});await diagnostics.span('browser.inspect.local',{},async()=>diagnostics.importStages(batch));await diagnostics.flush();assert.equal(spans(requests).filter(s=>s.name.startsWith('inspector.')).length,3);
});
test('inspector timing validation rejects entire malformed batches and hostile accessors',{timeout:15000},async t=>{const {requests,endpoint}=await collector(t),diagnostics=createDiagnostics();t.after(()=>diagnostics.shutdown());diagnostics.configure(config(endpoint));const record={id:1,parentId:null,stage:'parse',startOffsetMs:0,durationMs:5,status:'ok'},good=()=>({baseUnixMs:Date.now()-20,records:[record]});const bad=[{...good(),baseUnixMs:Date.now()-61000},{...good(),baseUnixMs:Date.now()+3000},{...good(),records:[record,record]},{...good(),records:[{...record,parentId:2}]},{...good(),records:[{...record,stage:'PASSWORD_SECRET'}]},{...good(),records:[{...record,durationMs:NaN}]},{...good(),records:[{...record,startOffsetMs:29999,durationMs:2}]},{...good(),records:Array(65).fill(record)}];const hostile={};Object.defineProperty(hostile,'baseUnixMs',{get(){throw Error('getter must not run');}});bad.push(hostile);await diagnostics.span('browser.inspect.local',{},async()=>{for(const batch of bad)assert.doesNotThrow(()=>diagnostics.importStages(batch));});await diagnostics.flush();assert.equal(spans(requests).length,1);diagnostics.configure({...config(endpoint),debugSampleRate:0});await diagnostics.span('browser.inspect.local',{},async()=>diagnostics.importStages(good()));await diagnostics.flush();assert.equal(spans(requests).length,1);
});
+14
View File
@@ -36,3 +36,17 @@ test('magic-byte executable and mismatch blocks precede scanner, dialog and OS w
test('no-scanner downloads require explicit central approval and carry actual safe forensic facts',async()=>{
const f=setup();f.context.policy.security.yara.enabled=false;assert.equal((await f.gate(f.input)).reason,'malware-scanner-required');f.context.policy.fileTransfers={download:{allowWithoutMalwareScanner:true}};const v=await f.gate(f.input);assert.equal(v.released,true);assert.equal(v.facts.detectedType,'text');assert.equal(v.facts.size,15);
});
test('opt-in terminal evidence carries inspected metadata and sanitized source URL',async()=>{
const events=[];const f=setup({audit:async(...args)=>events.push(args)});f.context.policy.evidence={enabled:true,captureDownloads:true};
assert.equal((await f.gate({...f.input,url:'https://user:[email protected]/file?token=secret#private'})).released,true);
assert.equal(events.find(e=>e[0]==='download.scan')[4],undefined);const e=events.find(e=>e[0]==='download.released')[4];
assert.equal(e.sourceUrl,'https://example.com/file');assert.equal(e.bytes,15);assert.equal(e.sha256,createHash('sha256').update('benign download').digest('hex'));assert.equal(e.decision,'allow');assert.equal('content' in e,false);
});
test('disabled, cancelled or stale downloads cannot enqueue protected evidence',async()=>{
for(const settings of [{enabled:false,captureDownloads:true},{enabled:true,captureDownloads:false},undefined]){const events=[];const f=setup({audit:async(...args)=>events.push(args)});f.context.policy.evidence=settings;await f.gate(f.input);assert.ok(events.every(e=>!e[4]));}
const events=[];const f=setup({choosePath:async()=>null,audit:async(...args)=>events.push(args)});f.context.policy.evidence={enabled:true,captureDownloads:true};assert.equal((await f.gate(f.input)).reason,'user-cancelled');assert.ok(events.every(e=>!e[4]));
let current=true;const stale=[];const g=setup({isCurrent:()=>current,scan:async()=>{current=false;return {decision:'block',ruleId:'malware'};},audit:async(...args)=>stale.push(args)});g.context.policy.evidence={enabled:true,captureDownloads:true};await g.gate(g.input);assert.ok(stale.every(e=>!e[4]));
});
test('malware rejection captures complete blocked facts without filesystem release',async()=>{
const events=[];const f=setup({scan:async()=>({decision:'block',ruleId:'malware'}),audit:async(...args)=>events.push(args)});f.context.policy.evidence={enabled:true,captureDownloads:true};assert.equal((await f.gate(f.input)).released,false);assert.equal(events[0][4].decision,'block');assert.equal(events[0][4].ruleId,'malware');assert.equal(f.calls.includes('write'),false);
});
+9
View File
@@ -38,3 +38,12 @@ test('explicit no antivirus mode still requires complete DLP and never substitut
assert.equal((await inspection.scanDownloadEnterprise(bytes,p,{inspect:async()=>({decision:'allow',coverage:{complete:true}})})).decision,'allow');
const mandatory={...p,security:{yara:{enabled:true}}};assert.equal((await inspection.scanDownloadEnterprise(bytes,mandatory,{inspect:async()=>({decision:'allow',coverage:{complete:true}})})).ruleId,'yara-unavailable');
});
test('incoming document DLP can be explicitly disabled only while malware scanning remains mandatory',async()=>{
const p={...policy,fileTransfers:{download:{requireDocumentDlp:false}}};
assert.equal((await inspection.scanDownloadEnterprise(bytes,p,null,async()=>allow())).decision,'allow');
assert.equal((await inspection.scanDownloadEnterprise(bytes,p,null,async()=>({decision:'block',ruleId:'icap-malicious-macro'}))).ruleId,'icap-malicious-macro');
const disabled={...p,enterprise:{icap:{enabled:false}}};assert.equal((await inspection.scanDownloadEnterprise(bytes,disabled,null)).ruleId,'malware-scanner-required');
disabled.fileTransfers.download.allowWithoutMalwareScanner=true;assert.equal((await inspection.scanDownloadEnterprise(bytes,disabled,null)).ruleId,'download-dlp-unavailable');
const both={...p,security:{yara:{enabled:true}}};assert.equal((await inspection.scanDownloadEnterprise(bytes,both,null,async()=>allow())).ruleId,'yara-unavailable');
});
+7
View File
@@ -0,0 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
const {buildDownloadEvidence,validateDownloadEvidence}=createRequire(import.meta.url)('../apps/browser/evidence-capture.cjs');
const policy={evidence:{enabled:true,captureDownloads:true}},input={url:'https://example.com/file?secret=omit#omit',name:'../report.pdf',mime:'Application/PDF; charset=utf8',facts:{size:4,sha256:'a'.repeat(64)},decision:'allow',ruleId:'scanner-allow'};
test('metadata builder allowlists and canonicalizes fields',()=>{const p=buildDownloadEvidence({...input,contents:'never stored'},policy);assert.deepEqual(p,{sourceUrl:'https://example.com/file',name:'report.pdf',sha256:'a'.repeat(64),bytes:4,mime:'application/pdf',decision:'allow',ruleId:'scanner-allow'});assert.equal(validateDownloadEvidence({...p,password:'secret'}),false);});
test('metadata rejects incomplete facts and recognizable credential paths or names',()=>{for(const extra of [{facts:null},{facts:{size:-1,sha256:'a'.repeat(64)}},{url:'file:///secret'},{url:'https://example.com/password%3Dsecret123'},{name:'api_key=secret123.txt'},{url:'https://example.com/%ZZ'}])assert.equal(buildDownloadEvidence({...input,...extra},policy),null);});
+47
View File
@@ -0,0 +1,47 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {generateKeyPairSync} from 'node:crypto';
import channel from '../packages/core/policy-channel.cjs';
import browser from '../apps/browser/management-channel.cjs';
const ed=generateKeyPairSync('ed25519'),pq=generateKeyPairSync('ml-dsa-87');
const publicKey=ed.publicKey.export({type:'spki',format:'pem'}),signatureKey=pq.publicKey.export({type:'spki',format:'pem'});
const payload={eventId:'4d113278-cabd-4e6d-b4be-022220f7a505',type:'download',payload:{name:'confidential-synthetic-only.xlsx'}};
function fixture({legacy=false,result={status:201,body:{id:'synthetic-ack'}},mutateResponse,mutateChallenge}={}){
const device={server:'https://synthetic.invalid',id:'device-a',token:'synthetic-token',publicKey,...(!legacy?{channelSignatureKey:signatureKey}:{}),privateEnrollmentFlag:true};
const calls=[];let item,saved=0;
const request=async(server,route,body,token)=>{
assert.equal(server,device.server);assert.equal(token,device.token);calls.push({route,body});
if(route==='/api/device/channel/identity')return channel.identity(ed.privateKey,pq.privateKey,signatureKey);
if(route==='/api/device/channel/evidence/challenge'){item=channel.evidenceChannel.challenge(device.id,ed.privateKey,pq.privateKey);return mutateChallenge?mutateChallenge(item.bundle):item.bundle;}
assert.equal(route,'/api/device/channel/evidence');assert.equal(JSON.stringify(body).includes(payload.payload.name),false);
const context=channel.evidenceChannel.receive(body,item.bundle,item.privateKey);
try{assert.deepEqual(context.payload,payload);const response=channel.evidenceChannel.response(result,item.bundle,context);return mutateResponse?mutateResponse(response):response;}finally{context.responseKey.fill(0);}
};
return {device,calls,request,save:actual=>{assert.equal(actual,device);assert.equal(actual.privateEnrollmentFlag,true);saved++;},saved:()=>saved};
}
test('browser evidence uses real PQ encryption and authenticated acknowledgement; legacy pin migration preserves enrollment state',async()=>{
const f=fixture({legacy:true});assert.deepEqual(await browser.protectedEvidenceRequest({...f,payload}),{id:'synthetic-ack'});assert.equal(f.saved(),1);assert.equal(f.device.channelSignatureKey,signatureKey);
assert.deepEqual(f.calls.map(c=>c.route),['/api/device/channel/identity','/api/device/channel/evidence/challenge','/api/device/channel/evidence']);
await browser.protectedEvidenceRequest({...f,payload});assert.equal(f.saved(),1);
});
test('authenticated evidence rejection preserves status without disclosing server error content',async()=>{
const f=fixture({result:{status:403,body:{error:'synthetic confidential server detail'}}});
await assert.rejects(browser.protectedEvidenceRequest({...f,payload}),error=>error.status===403&&error.message==='Protected evidence submission refused');
});
test('tampered evidence response and unauthenticated status never become a trusted acknowledgement',async()=>{
for(const mutateResponse of [r=>({...r,status:403}),r=>({...r,encrypted:{...r.encrypted,tag:Buffer.alloc(16).toString('base64url')}})]){
const f=fixture({mutateResponse});await assert.rejects(browser.protectedEvidenceRequest({...f,payload}),error=>error.status===undefined);
}
for(const result of [{status:201,body:[]},{status:200,body:null},{status:201,body:{},extra:true},{status:199,body:{}}])await assert.rejects(browser.protectedEvidenceRequest({...fixture({result}),payload}));
});
test('wrong pin and cross-purpose challenge are refused before evidence transmission',async()=>{
const wrong=fixture();wrong.device.channelSignatureKey=generateKeyPairSync('ml-dsa-87').publicKey.export({type:'spki',format:'pem'});
await assert.rejects(browser.protectedEvidenceRequest({...wrong,payload}));assert.equal(wrong.calls.length,1);
const cross=fixture({mutateChallenge:()=>channel.challenge('device-a',ed.privateKey,pq.privateKey).bundle});
await assert.rejects(browser.protectedEvidenceRequest({...cross,payload}));assert.equal(cross.calls.length,1);
});
test('transport failure has no plaintext evidence fallback',async()=>{
const f=fixture(),request=async(...args)=>{if(args[1]==='/api/device/channel/evidence')throw Error('Synthetic offline transport');return f.request(...args);};
await assert.rejects(browser.protectedEvidenceRequest({...f,request,payload}));assert.equal(f.calls.some(c=>c.route==='/api/device/evidence'),false);
});
+31
View File
@@ -0,0 +1,31 @@
import test from 'node:test';import assert from 'node:assert/strict';import {mkdtempSync,writeFileSync,rmSync} from 'node:fs';import {tmpdir} from 'node:os';import {join} from 'node:path';import {execFileSync} from 'node:child_process';import {randomBytes,randomUUID} from 'node:crypto';
import channel from '../packages/core/policy-channel.cjs';import browser from '../apps/browser/management-channel.cjs';import {openStore,digest} from '../apps/server/store.mjs';import {createApp} from '../apps/server/app.mjs';
async function fixture(t){
const dir=mkdtempSync(join(tmpdir(),'safetynet-pq-evidence-')),keyFile=join(dir,'evidence.key');writeFileSync(keyFile,randomBytes(32),{mode:0o600});if(process.platform==='win32')execFileSync(join(process.env.SystemRoot,'System32/WindowsPowerShell/v1.0/powershell.exe'),['-NoProfile','-NonInteractive','-Command',"$ErrorActionPreference='Stop';$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User;$acl=New-Object System.Security.AccessControl.FileSecurity;$acl.SetOwner($sid);$acl.SetAccessRuleProtection($true,$false);$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($sid,'FullControl','Allow')));Set-Acl -LiteralPath $env:TEST_PQ_EVIDENCE_KEY -AclObject $acl"],{windowsHide:true,env:{SystemRoot:process.env.SystemRoot,TEST_PQ_EVIDENCE_KEY:keyFile},stdio:'pipe'});
const store=openStore(dir),p=store.get('policy');p.evidence={enabled:true,captureDownloads:true};store.put('policy',p);const token='synthetic-pq-evidence-device-token-123456789',otherToken='synthetic-pq-evidence-other-token-123456789',id='device-a';store.db.prepare('INSERT INTO devices(id,token_hash,revoked) VALUES(?,?,0)').run(id,digest(token));store.db.prepare('INSERT INTO devices(id,token_hash,revoked) VALUES(?,?,0)').run('device-b',digest(otherToken));
const config={publicUrl:'http://127.0.0.1',secure:false,evidenceKeyFile:keyFile,requirePQManagement:true},server=createApp(store,config).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();assert.ok(dir.startsWith(join(tmpdir(),'safetynet-pq-evidence-')));rmSync(dir,{recursive:true,force:true});});
const base=`http://127.0.0.1:${server.address().port}`,wire=[];const api=async(route,body,credential=token)=>{wire.push({route,body});const r=await fetch(base+route,{method:body?'POST':'GET',headers:{authorization:`Bearer ${credential}`,...(body?{'content-type':'application/json'}:{})},body:body?JSON.stringify(body):undefined});return {status:r.status,body:await r.json()};};
const eventId=randomUUID();store.event(id,'clipboard.copy',{eventId,policyVersion:1});const payload={eventId,type:'interaction',payload:{pageUrl:'https://intranet.example/file',action:'copy',content:'CONFIDENTIAL_SYNTHETIC_PQ_EVIDENCE',sensitiveField:false}};
const device={id,token,server:base,publicKey:store.publicKey(),channelSignatureKey:store.get('channelSigningKeys').publicKey};const make=async()=>{const challenge=await api('/api/device/channel/evidence/challenge',{});assert.equal(challenge.status,200);channel.evidenceChannel.verifyChallenge(challenge.body,id,device.publicKey,device.channelSignatureKey);return {bundle:challenge.body,context:channel.evidenceChannel.prepare(challenge.body,payload)};};return {store,api,wire,payload,device,make};
}
test('real central PQ evidence storage rejects plaintext and supports authenticated idempotent retry',async t=>{
const f=await fixture(t);assert.equal((await f.api('/api/device/evidence',f.payload)).status,403);
const request=async(_base,route,body,credential)=>{const response=await f.api(route,body,credential);if(response.status>=400)throw Object.assign(Error('Transport refused'),{status:response.status});return response.body;};
const created=await browser.protectedEvidenceRequest({device:f.device,request,save:()=>{},payload:f.payload});assert.match(created.id,/^[0-9a-f-]{36}$/);const again=await browser.protectedEvidenceRequest({device:f.device,request,save:()=>{},payload:f.payload});assert.equal(again.id,created.id);assert.equal(again.replayed,true);
assert.equal(f.store.db.prepare('SELECT count(*) n FROM protected_evidence').get().n,1);assert.equal(f.store.db.prepare("SELECT count(*) n FROM events WHERE kind='evidence.recorded'").get().n,1);
assert.equal(JSON.stringify(f.wire.filter(r=>r.route.includes('/channel/'))).includes(f.payload.payload.content),false);assert.equal(f.store.db.prepare('SELECT metadata FROM events').all().some(r=>r.metadata.includes(f.payload.payload.content)),false);
const row=f.store.db.prepare('SELECT ciphertext FROM protected_evidence').get();assert.equal(Buffer.from(row.ciphertext).includes(Buffer.from(f.payload.payload.content)),false);
await assert.rejects(browser.protectedEvidenceRequest({device:f.device,request,save:()=>{},payload:{...f.payload,payload:{...f.payload.payload,content:'Different synthetic payload'}}}),e=>e.status===409);
});
test('challenge consumption covers successful replay, invalid ciphertext and cross-device substitution',async t=>{
const f=await fixture(t);let a=await f.make();assert.equal((await f.api('/api/device/channel/evidence',a.context.request,'synthetic-pq-evidence-other-token-123456789')).status,403);
let response=await f.api('/api/device/channel/evidence',a.context.request);assert.equal(response.status,200);assert.equal(channel.evidenceChannel.finish(response.body,a.bundle,a.context).status,201);assert.equal((await f.api('/api/device/channel/evidence',a.context.request)).status,403);a.context.responseKey.fill(0);
a=await f.make();const altered=structuredClone(a.context.request);altered.encrypted.tag=Buffer.alloc(16).toString('base64url');assert.equal((await f.api('/api/device/channel/evidence',altered)).status,403);assert.equal((await f.api('/api/device/channel/evidence',a.context.request)).status,403);a.context.responseKey.fill(0);
});
test('central disable after issuance returns encrypted denial and revoked credentials cannot upload',async t=>{
const f=await fixture(t),a=await f.make(),p=f.store.get('policy');p.evidence.enabled=false;f.store.put('policy',p);const response=await f.api('/api/device/channel/evidence',a.context.request);assert.equal(response.status,200);assert.equal(channel.evidenceChannel.finish(response.body,a.bundle,a.context).status,403);assert.equal(f.store.db.prepare('SELECT count(*) n FROM protected_evidence').get().n,0);assert.equal((await f.api('/api/device/channel/evidence/challenge',{})).status,403);a.context.responseKey.fill(0);
p.evidence.enabled=true;f.store.put('policy',p);const b=await f.make();f.store.db.prepare('UPDATE devices SET revoked=1 WHERE id=?').run(f.device.id);assert.equal((await f.api('/api/device/channel/evidence',b.context.request)).status,401);b.context.responseKey.fill(0);
});
test('expired evidence challenge never decrypts or stores payload and remains one-use',async t=>{
const now=Date.now();t.mock.timers.enable({apis:['Date'],now});const f=await fixture(t),a=await f.make();t.mock.timers.setTime(now+60001);assert.equal((await f.api('/api/device/channel/evidence',a.context.request)).status,403);assert.equal((await f.api('/api/device/channel/evidence',a.context.request)).status,403);assert.equal(f.store.db.prepare('SELECT count(*) n FROM protected_evidence').get().n,0);a.context.responseKey.fill(0);
});
+47
View File
@@ -0,0 +1,47 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {generateKeyPairSync,randomBytes} from 'node:crypto';
import policy from '../packages/core/policy-channel.cjs';
const channel=policy.evidenceChannel;
const ed=generateKeyPairSync('ed25519'),pq=generateKeyPairSync('ml-dsa-87');
const edPublic=ed.publicKey.export({type:'spki',format:'pem'}),pqPublic=pq.publicKey.export({type:'spki',format:'pem'});
const make=()=>channel.challenge('synthetic-device',ed.privateKey,pq.privateKey);
function exchange(){const challenge=make(),client=channel.prepare(challenge.bundle,{content:'synthetic confidential evidence',sourceEventId:'fixture-event'}),server=channel.receive(client.request,challenge.bundle,challenge.privateKey);return {...challenge,client,server};}
test('real evidence KEM encrypts payload and authenticates signed identity and response',()=>{
const identity=channel.identity(ed.privateKey,pq.privateKey,pqPublic);assert.equal(channel.verifyIdentity(identity,edPublic,pqPublic),pqPublic);
const x=exchange();channel.verifyChallenge(x.bundle,'synthetic-device',edPublic,pqPublic);
assert.equal(JSON.stringify(x.client.request).includes('synthetic confidential'),false);assert.equal(x.server.payload.content,'synthetic confidential evidence');
const response=channel.response({accepted:true},x.bundle,x.server);assert.deepEqual(channel.finish(response,x.bundle,x.client),{accepted:true});
assert.throws(()=>channel.finish({...response,encrypted:x.client.request.encrypted},x.bundle,x.client));
x.client.responseKey.fill(0);x.server.responseKey.fill(0);
});
test('policy and evidence protocols cannot substitute identity, challenge or ciphertext',()=>{
const x=exchange(),other=policy.challenge('synthetic-device',ed.privateKey,pq.privateKey);
assert.throws(()=>policy.verifyIdentity(channel.identity(ed.privateKey,pq.privateKey,pqPublic),edPublic,pqPublic));
assert.throws(()=>policy.verifyChallenge(x.bundle,'synthetic-device',edPublic,pqPublic));
assert.throws(()=>channel.verifyChallenge(other.bundle,'synthetic-device',edPublic,pqPublic));
assert.throws(()=>channel.prepare(other.bundle,{content:'fixture'}));
assert.throws(()=>policy.receive(x.client.request,x.bundle,x.privateKey));
const p=policy.prepare(other.bundle);assert.throws(()=>channel.receive(p.request,other.bundle,other.privateKey));
p.responseKey.fill(0);x.client.responseKey.fill(0);x.server.responseKey.fill(0);
});
test('evidence refuses tampering, altered device, challenge, response key and request identity',()=>{
const x=exchange(),other=make();
assert.throws(()=>channel.verifyChallenge({...x.bundle,deviceId:'other'},'other',edPublic,pqPublic));
assert.throws(()=>channel.receive({...x.client.request,deviceId:'other'},x.bundle,x.privateKey));
assert.throws(()=>channel.receive({...x.client.request,id:other.bundle.id},x.bundle,x.privateKey));
const altered=structuredClone(x.client.request);altered.encrypted.tag=randomBytes(16).toString('base64url');assert.throws(()=>channel.receive(altered,x.bundle,x.privateKey));
const response=channel.response({accepted:true},x.bundle,x.server);
assert.throws(()=>channel.finish(response,x.bundle,{...x.client,responseKey:randomBytes(32)}));
assert.throws(()=>channel.finish(response,x.bundle,{...x.client,requestId:'other'}));
assert.throws(()=>channel.finish({...response,id:other.bundle.id},x.bundle,x.client));
x.client.responseKey.fill(0);x.server.responseKey.fill(0);
});
test('evidence payload size and strict envelope are bounded; replay consumption belongs to server',()=>{
const x=make();assert.throws(()=>channel.prepare(x.bundle,{content:'x'.repeat(65536)}));assert.throws(()=>channel.prepare(x.bundle,[]));
const client=channel.prepare(x.bundle,{content:'x'.repeat(65500)}),first=channel.receive(client.request,x.bundle,x.privateKey);
assert.equal(first.payload.content.length,65500);
assert.throws(()=>channel.receive({...client.request,extra:true},x.bundle,x.privateKey));
assert.throws(()=>channel.receive(client.request,{...x.bundle,expiresAt:Date.now()-1},x.privateKey));
client.responseKey.fill(0);first.responseKey.fill(0);
});
+36
View File
@@ -0,0 +1,36 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
const {deliverAuditRecord,stripPendingEvidence,collectionAllowed,validatedEvidence}=createRequire(import.meta.url)('../apps/browser/evidence-delivery.cjs');
function fixture(){
const payload={sourceUrl:'https://example.com/report',name:'report.pdf',sha256:'a'.repeat(64),bytes:12,mime:'application/pdf',decision:'allow',ruleId:'scanner-allow'};
const record={eventId:'b7a17c1d-680e-4c90-9a3c-3d18f50a4e3f',kind:'download.released',file:{sha256:payload.sha256,bytes:payload.bytes},protectedEvidence:{deviceId:'device-a',payload}};
const context={ready:true,deviceId:'device-a',policy:{expiresAt:new Date(Date.now()+60000).toISOString(),evidence:{enabled:true,captureDownloads:true}}};
return {payload,record,context};
}
test('event-first retries preserve original UUID and atomic tuple without leaking evidence into events',async()=>{
const f=fixture(),calls=[],snapshot=structuredClone(f.record);let failure=true;
const send=async(route,body)=>{calls.push({route,body});if(route.endsWith('/evidence')){if(failure){const error=Error('temporarily unavailable');error.status=503;throw error;}return {id:'00000000-0000-4000-8000-000000000001',sourceEventId:7};}return {};};
await assert.rejects(deliverAuditRecord({record:f.record,send,getContext:()=>f.context}),/temporarily unavailable/);assert.deepEqual(f.record,snapshot);
failure=false;assert.equal((await deliverAuditRecord({record:f.record,send,getContext:()=>f.context})).evidenceDelivered,true);
assert.deepEqual(calls.map(c=>c.route),['/api/device/events','/api/device/evidence','/api/device/events','/api/device/evidence']);
for(const call of calls){assert.equal(call.body.eventId,f.record.eventId);if(call.route.endsWith('/events'))assert.equal('protectedEvidence' in call.body,false);else assert.deepEqual(call.body,{eventId:f.record.eventId,type:'download',payload:f.payload});}
});
test('403 discards evidence after source event; unavailable evidence service keeps record retryable',async()=>{
for(const status of [403,503]){const f=fixture(),routes=[];const task=deliverAuditRecord({record:f.record,getContext:()=>f.context,send:async route=>{routes.push(route);if(route.endsWith('/evidence'))throw Object.assign(Error('rejected'),{status});return {};}});
if(status===403)assert.deepEqual(await task,{eventDelivered:true,evidenceDiscarded:true});else await assert.rejects(task,/rejected/);assert.deepEqual(routes,['/api/device/events','/api/device/evidence']);assert.ok(f.record.protectedEvidence);}
});
test('device change, central disable and expired policy discard only evidence while preserving event',async()=>{
for(const change of [f=>f.context.deviceId='device-b',f=>f.context.policy.evidence.enabled=false,f=>f.context.policy.evidence.captureDownloads=false,f=>f.context.policy.expiresAt='2000-01-01T00:00:00Z']){const f=fixture(),routes=[];change(f);assert.deepEqual(await deliverAuditRecord({record:f.record,getContext:()=>f.context,send:async route=>{routes.push(route);return {};}}),{eventDelivered:true,evidenceDiscarded:true});assert.deepEqual(routes,['/api/device/events']);}
const f=fixture(),stripped=stripPendingEvidence([f.record]);assert.equal(stripped[0].eventId,f.record.eventId);assert.deepEqual(stripped[0].file,f.record.file);assert.equal('protectedEvidence' in stripped[0],false);assert.ok(f.record.protectedEvidence);
});
test('not-ready managed context pauses evidence after source event without losing tuple',async()=>{
const f=fixture(),routes=[];f.context.ready=false;await assert.rejects(deliverAuditRecord({record:f.record,getContext:()=>f.context,send:async route=>{routes.push(route);return {};}}),/paused/);assert.deepEqual(routes,['/api/device/events']);assert.ok(f.record.protectedEvidence);
});
test('invalid evidence acknowledgement and invalid payload never mark evidence delivered',async()=>{
for(const acknowledgement of [undefined,{}, {id:7,sourceEventId:1},{id:'id',sourceEventId:1.5},{id:'00000000-0000-4000-8000-000000000001',sourceEventId:0},{id:'',sourceEventId:1}]){const f=fixture();await assert.rejects(deliverAuditRecord({record:f.record,getContext:()=>f.context,send:async route=>route.endsWith('/evidence')?acknowledgement:{}}),/acknowledgement invalid/);}
const f=fixture(),routes=[];f.record.protectedEvidence.payload.secret='forbidden';await assert.rejects(deliverAuditRecord({record:f.record,getContext:()=>f.context,send:async route=>{routes.push(route);return {};}}),/Invalid protected/);assert.deepEqual(routes,['/api/device/events']);
});
test('collection requires enabled current ready context and validation returns independent clone',()=>{
const f=fixture();assert.equal(collectionAllowed(f.context),true);assert.equal(collectionAllowed({...f.context,ready:false}),false);assert.equal(collectionAllowed({...f.context,policy:{...f.context.policy,expiresAt:'invalid'}}),false);const clone=validatedEvidence(f.payload);clone.name='changed';assert.equal(f.payload.name,'report.pdf');
});
+3
View File
@@ -0,0 +1,3 @@
import test from 'node:test';import assert from 'node:assert/strict';import {mkdtempSync,writeFileSync,rmSync} from 'node:fs';import {join,resolve,sep} from 'node:path';import {tmpdir} from 'node:os';import {execFileSync} from 'node:child_process';import {randomBytes} from 'node:crypto';import {assertPrivateEvidenceKey} from '../apps/server/evidence-key.mjs';import {normalizePrincipal,principalForGroups} from '../apps/server/admin-access.mjs';
test('analyst identity grants evidence/triage but no policy or fleet administration',()=>{const p=normalizePrincipal({sub:'soc',role:'analyst'});assert.ok(p.permissions.includes('evidence.read'));for(const permission of ['policy.write','devices.manage','sessions.manage','integrations.manage'])assert.equal(p.permissions.includes(permission),false);assert.equal(principalForGroups({sub:'soc'},['soc-team'],{oidcAdminGroup:'admins',oidcAnalystGroup:'soc-team'}).role,'analyst');});
test('Windows evidence key rejects inherited shared read permissions',{skip:process.platform!=='win32'},t=>{const dir=mkdtempSync(join(tmpdir(),'safetynet-evidence-acl-')),file=join(dir,'evidence.key');try{writeFileSync(file,randomBytes(32));let rejected=false;try{assertPrivateEvidenceKey(file);}catch{rejected=true;}if(!rejected){t.skip('This Windows temp directory already supplies private inherited ACLs');return;}assert.equal(rejected,true);}finally{if(!resolve(dir).startsWith(resolve(tmpdir())+sep))throw Error('Unsafe cleanup');rmSync(dir,{recursive:true,force:true});}});
+27
View File
@@ -0,0 +1,27 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import vm from 'node:vm';
import {readFileSync} from 'node:fs';
function fixture({evidence,review=true}={}){
const fields=new Map(),get=id=>{if(!fields.has(id))fields.set(id,{checked:false,disabled:false,addEventListener(kind,fn){this[kind]=fn;}});return fields.get(id);};
let changes=0;
const context=vm.createContext({$:get,policy:{evidence},currentPrincipal:null,independentApprovalRequired:review,fill(){},draft:()=>({version:4,brand:{name:'Company'}}),applyOperatorPermissions(){},updateDraftStatus(){changes++;}});
vm.runInContext(readFileSync(new URL('../apps/server/public/evidence-policy-admin.js',import.meta.url),'utf8'),context);
return {context,get,changes:()=>changes};
}
test('evidence controls default off and preserve unrelated draft fields',()=>{
const {context,get}=fixture();assert.equal(get('evidence-policy-enabled').checked,false);assert.equal(get('evidence-policy-downloads').checked,false);
assert.equal(get('evidence-policy-enabled').disabled,true);assert.equal(get('evidence-policy-downloads').disabled,true);
const draft=context.draft();assert.equal(draft.version,4);assert.equal(draft.brand.name,'Company');assert.equal(draft.evidence.enabled,false);assert.equal(draft.evidence.captureDownloads,false);
});
test('independent preparation permission enables draft controls; global off disables child control',()=>{
const {context,get,changes}=fixture({evidence:{enabled:true,captureDownloads:true}});
context.applyOperatorPermissions({permissions:['policy.read']});assert.equal(get('evidence-policy-enabled').disabled,true);
context.applyOperatorPermissions({permissions:['policy.prepare']});assert.equal(get('evidence-policy-enabled').disabled,false);assert.equal(get('evidence-policy-downloads').disabled,false);
get('evidence-policy-enabled').checked=false;get('evidence-policy-enabled').change();assert.equal(get('evidence-policy-downloads').disabled,true);assert.equal(context.draft().evidence.enabled,false);assert.equal(changes(),1);
context.fill({evidence:{enabled:true,captureDownloads:false}});assert.equal(get('evidence-policy-downloads').checked,false);assert.equal(get('evidence-policy-downloads').disabled,false);
});
test('direct publication mode requires policy.write rather than preparation permission',()=>{
const {context,get}=fixture({review:false});context.applyOperatorPermissions({permissions:['policy.prepare']});assert.equal(get('evidence-policy-enabled').disabled,true);
context.applyOperatorPermissions({permissions:['policy.write']});assert.equal(get('evidence-policy-enabled').disabled,false);
});
+17
View File
@@ -0,0 +1,17 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {defaultPolicy,PolicySchema,signPolicy,verifyPolicy} from '../packages/core/policy.mjs';
import {generateKeyPairSync} from 'node:crypto';
import {openStore} from '../apps/server/store.mjs';
import {createApp} from '../apps/server/app.mjs';
import {mkdtempSync,rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
test('evidence controls default off, reject unknown settings and are bound to the policy signature',()=>{
const p=defaultPolicy();assert.deepEqual(p.evidence,{enabled:false,captureDownloads:false});assert.equal(PolicySchema.safeParse({...p,evidence:{enabled:true,captureDownloads:true,fullFiles:true}}).success,false);
const keys=generateKeyPairSync('ed25519'),publicKey=keys.publicKey.export({type:'spki',format:'pem'});p.evidence={enabled:true,captureDownloads:true};const envelope=signPolicy(p,keys.privateKey.export({type:'pkcs8',format:'pem'}));assert.equal(verifyPolicy(envelope,publicKey).evidence.captureDownloads,true);p.evidence.enabled=false;envelope.payload=Buffer.from(JSON.stringify(p)).toString('base64url');assert.throws(()=>verifyPolicy(envelope,publicKey));
});
test('central policy publication cannot enable unconfigured evidence storage',async t=>{
const dir=mkdtempSync(join(tmpdir(),'safetynet-evidence-policy-')),store=openStore(dir),adminToken='synthetic-evidence-policy-admin-123456789';const server=createApp(store,{publicUrl:'http://127.0.0.1',secure:false,adminToken}).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();assert.ok(dir.startsWith(join(tmpdir(),'safetynet-evidence-policy-')));rmSync(dir,{recursive:true,force:true});});
const p=store.get('policy');p.version++;p.evidence={enabled:true,captureDownloads:true};const response=await fetch(`http://127.0.0.1:${server.address().port}/api/admin/policy`,{method:'PUT',headers:{authorization:`Bearer ${adminToken}`,'content-type':'application/json'},body:JSON.stringify(p)});assert.equal(response.status,400);assert.equal(store.get('policy').evidence.enabled,false);assert.equal(store.get('policy').version,1);
});
+14
View File
@@ -0,0 +1,14 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
const {createEvidenceViewer}=createRequire(import.meta.url)('../apps/server/public/evidence-viewer.js');
function fixture(){let principal={sub:'analyst',role:'analyst',permissions:['evidence.read']},timer;const requests=[],renders=[];const view=createEvidenceViewer({principal:()=>principal,request:path=>new Promise((resolve,reject)=>requests.push({path,resolve,reject})),render:(...args)=>renders.push(args),clear:()=>renders.push(['clear']),setTimer:(fn,ms)=>{assert.equal(ms,60000);timer=fn;return 1;},cancelTimer:()=>{timer=null;}});return {view,requests,renders,revoke:()=>{principal={...principal,permissions:[]};view.permissions();},identity:()=>{principal={...principal,sub:'other'};view.permissions();},expire:()=>timer()};}
const id='11111111-1111-4111-8111-111111111111';
async function open(f,incident='one'){const result=f.view.open(incident);f.requests.at(-1).resolve({evidence:[{id,availability:'available'}]});await result;}
test('metadata alone does not retrieve plaintext; purpose required',async()=>{const f=fixture();await open(f);assert.equal(f.requests.length,1);await f.view.reveal(id,'');assert.equal(f.requests.length,1);const reveal=f.view.reveal(id,'investigation');assert.match(f.requests.at(-1).path,/reason=investigation$/);f.requests.at(-1).resolve({id,type:'interaction',payload:{content:'<script>synthetic</script>'}});await reveal;assert.equal(f.renders.at(-1)[0],'plaintext');assert.match(f.renders.at(-1)[1],/<script>/);f.expire();assert.deepEqual(f.renders.at(-1),['clear']);});
for(const action of ['reset','revoke','identity'])test(`in-flight plaintext cannot repopulate after ${action}`,async()=>{const f=fixture();await open(f);const reveal=f.view.reveal(id,'incident-response');if(action==='reset')f.view.reset();else f[action]();f.requests.at(-1).resolve({id,type:'download',payload:{name:'synthetic'}});await reveal;assert.deepEqual(f.renders.at(-1),['clear']);});
test('older incident metadata cannot replace new selection',async()=>{const f=fixture(),old=f.view.open('old'),newer=f.view.open('new');f.requests[1].resolve({evidence:[{id}]});await newer;const expected=f.renders.length;f.requests[0].resolve({evidence:[{id:'old'}]});await old;assert.equal(f.renders.length,expected);});
test('errors are sanitized and payloads bounded',async()=>{const f=fixture();await open(f);let reveal=f.view.reveal(id,'compliance');f.requests.at(-1).reject(Error('SECRET backend detail'));await reveal;assert.doesNotMatch(f.renders.at(-1)[1],/SECRET/);reveal=f.view.reveal(id,'compliance');f.requests.at(-1).resolve({id,type:'interaction',payload:{content:'x'.repeat(70000)}});await reveal;assert.equal(f.renders.at(-1)[0],'error');});
test('viewer cannot fetch evidence outside the displayed incident',async()=>{const f=fixture();await open(f);const count=f.requests.length;await f.view.reveal('22222222-2222-4222-8222-222222222222','investigation');assert.equal(f.requests.length,count);assert.equal(f.renders.at(-1)[0],'error');});
test('viewer suppresses a mismatched source event in a decrypted response',async()=>{const f=fixture(),opening=f.view.open('one');f.requests.at(-1).resolve({evidence:[{id,availability:'available',sourceEventId:42}]});await opening;const revealing=f.view.reveal(id,'investigation');f.requests.at(-1).resolve({id,sourceEventId:43,type:'interaction',payload:{content:'must not render'}});await revealing;assert.equal(f.renders.at(-1)[0],'error');assert.equal(f.renders.some(([kind,value])=>kind==='plaintext'&&value.includes('must not render')),false);});
+53 -4
View File
@@ -1,6 +1,44 @@
import test from 'node:test';import assert from 'node:assert/strict';import {mkdtempSync,writeFileSync,rmSync} from 'node:fs';import {tmpdir} from 'node:os';import {join} from 'node:path';import {randomBytes,randomUUID} from 'node:crypto';import {openStore,digest} from '../apps/server/store.mjs';import {createApp} from '../apps/server/app.mjs';
async function fixture(t,{configured=true}={}){const dir=mkdtempSync(join(tmpdir(),'safetynet-evidence-')),keyFile=join(dir,'evidence.key');writeFileSync(keyFile,randomBytes(32));const store=openStore(dir),deviceToken='synthetic-device-evidence-credential-1234567890',id=randomUUID();store.db.prepare('INSERT INTO devices(id,token_hash,revoked) VALUES(?,?,0)').run(id,digest(deviceToken));const config={publicUrl:'http://127.0.0.1:4310',...(configured?{evidenceKeyFile:keyFile}:{})},server=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));t.after(async()=>{await new Promise(r=>server.close(r));store.db.close();rmSync(dir,{recursive:true,force:true});});const api=async(path,method='GET',body,role='device')=>{const credential=`synthetic-${role}-evidence-credential-1234567890`;if(role!=='device')store.db.prepare('INSERT OR REPLACE INTO sessions VALUES(?,?,?)').run(digest(credential),Date.now()+60000,JSON.stringify({sub:role,role}));const r=await fetch(`http://127.0.0.1:${server.address().port}${path}`,{method,headers:{...(role==='device'?{authorization:`Bearer ${deviceToken}`}:{cookie:`sn_session=${credential}`}),origin:config.publicUrl,'content-type':'application/json'},body:body?JSON.stringify(body):undefined});return {status:r.status,body:await r.json()};};const eventId=randomUUID();store.event(id,'clipboard.copy',{eventId,policyVersion:1});return {store,api,id,eventId,config};}
import test from 'node:test';import assert from 'node:assert/strict';import {mkdtempSync,writeFileSync,rmSync} from 'node:fs';import {execFileSync} from 'node:child_process';import {tmpdir} from 'node:os';import {join} from 'node:path';import {randomBytes,randomUUID} from 'node:crypto';import {openStore,digest} from '../apps/server/store.mjs';import {createApp} from '../apps/server/app.mjs';
function protectFixtureKey(file){
if(process.platform!=='win32')return;
const script="$ErrorActionPreference='Stop';$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User;$acl=New-Object System.Security.AccessControl.FileSecurity;$acl.SetOwner($sid);$acl.SetAccessRuleProtection($true,$false);$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($sid,'FullControl','Allow')));Set-Acl -LiteralPath $env:TEST_EVIDENCE_KEY -AclObject $acl";
execFileSync(join(process.env.SystemRoot,'System32/WindowsPowerShell/v1.0/powershell.exe'),['-NoProfile','-NonInteractive','-Command',script],{windowsHide:true,env:{SystemRoot:process.env.SystemRoot,TEST_EVIDENCE_KEY:file},stdio:'pipe'});
}
async function fixture(t,{configured=true}={}){const dir=mkdtempSync(join(tmpdir(),'safetynet-evidence-')),keyFile=join(dir,'evidence.key');writeFileSync(keyFile,randomBytes(32),{mode:0o600});protectFixtureKey(keyFile);const store=openStore(dir);const evidencePolicy=store.get('policy');evidencePolicy.evidence={enabled:true,captureDownloads:true};store.put('policy',evidencePolicy);const deviceToken='synthetic-device-evidence-credential-1234567890',id=randomUUID();store.db.prepare('INSERT INTO devices(id,token_hash,revoked) VALUES(?,?,0)').run(id,digest(deviceToken));const config={publicUrl:'http://127.0.0.1:4310',...(configured?{evidenceKeyFile:keyFile}:{})},server=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));t.after(async()=>{await new Promise(r=>server.close(r));store.db.close();rmSync(dir,{recursive:true,force:true});});const api=async(path,method='GET',body,role='device')=>{const credential=`synthetic-${role}-evidence-credential-1234567890`;if(role!=='device')store.db.prepare('INSERT OR REPLACE INTO sessions VALUES(?,?,?)').run(digest(credential),Date.now()+60000,JSON.stringify({sub:role,role}));const r=await fetch(`http://127.0.0.1:${server.address().port}${path}`,{method,headers:{...(role==='device'?{authorization:`Bearer ${deviceToken}`}:{cookie:`sn_session=${credential}`}),origin:config.publicUrl,'content-type':'application/json'},body:body?JSON.stringify(body):undefined});return {status:r.status,body:await r.json()};};const eventId=randomUUID();store.event(id,'clipboard.copy',{eventId,policyVersion:1});return {store,api,id,eventId,config};}
const evidence=eventId=>({eventId,type:'interaction',payload:{pageUrl:'https://intranet.example/document',action:'copy',content:'Synthetic confidential engineering detail',sensitiveField:false}});
test('protected evidence cannot contradict source actions, decisions, hosts or file facts',async t=>{
const {api,store,id,eventId}=await fixture(t);
assert.equal((await api('/api/device/evidence','POST',{...evidence(eventId),payload:{...evidence(eventId).payload,action:'paste'}})).status,400);
assert.equal((await api('/api/device/evidence','POST',{...evidence(eventId),payload:{...evidence(eventId).payload,contentSha256:'0'.repeat(64)}})).status,400);
const download=randomUUID();store.event(id,'download.released',{eventId:download,host:'downloads.example',file:{sha256:'a'.repeat(64),bytes:10,detectedType:'text'}});
const payload={sourceUrl:'https://downloads.example/file.txt',name:'file.txt',sha256:'a'.repeat(64),bytes:10,mime:'text/plain',decision:'allow'};
for(const change of [{sha256:'b'.repeat(64)},{bytes:11},{decision:'block'},{sourceUrl:'https://other.example/file.txt'}])assert.equal((await api('/api/device/evidence','POST',{eventId:download,type:'download',payload:{...payload,...change}})).status,400);
assert.equal((await api('/api/device/evidence','POST',{eventId:download,type:'download',payload})).status,201);
});
test('incident discovery lists only linked metadata without decrypting or releasing content',async t=>{
const {api,store,eventId}=await fixture(t),created=await api('/api/device/evidence','POST',evidence(eventId));
const source=store.db.prepare('SELECT source_event_id FROM protected_evidence').get().source_event_id,incident=await api('/api/admin/incidents','POST',{eventId:source},'admin');
const path=`/api/admin/incidents/${incident.body.id}/evidence`;
for(const role of ['operator','auditor'])assert.equal((await api(path,'GET',undefined,role)).status,403);
const result=await api(path,'GET',undefined,'admin');assert.equal(result.status,200);assert.equal(result.body.incidentId,incident.body.id);
assert.equal((await api(path,'GET',undefined,'analyst')).status,200);
const analystRead=await api(`/api/admin/evidence/${created.body.id}?reason=investigation`,'GET',undefined,'analyst');assert.equal(analystRead.status,200);assert.equal(analystRead.body.payload.content,evidence(eventId).payload.content);
const access=store.db.prepare("SELECT metadata FROM events WHERE kind='evidence.accessed'").get();assert.equal(JSON.parse(access.metadata).actor,'analyst');
assert.deepEqual(Object.keys(result.body.evidence[0]).sort(),['id','sourceEventId','eventId','type','createdAt','expiresAt','retentionHold','availability'].sort());
assert.equal(result.body.evidence[0].id,created.body.id);assert.equal(result.body.evidence[0].eventId,eventId);assert.equal(result.body.evidence[0].availability,'available');assert.equal(result.body.evidence[0].retentionHold,true);
assert.equal(JSON.stringify(result.body).includes(evidence(eventId).payload.content),false);assert.equal(store.db.prepare("SELECT COUNT(*) n FROM events WHERE kind='evidence.accessed'").get().n,1);
// A damaged ciphertext does not prevent safe discovery; reveal still authenticates.
store.db.prepare('UPDATE protected_evidence SET ciphertext=?').run(Buffer.from('synthetic corruption'));
assert.equal((await api(path,'GET',undefined,'admin')).status,200);assert.equal((await api(`/api/admin/evidence/${created.body.id}?reason=investigation`,'GET',undefined,'admin')).status,500);
assert.equal((await api('/api/admin/incidents/not-a-uuid/evidence','GET',undefined,'admin')).status,400);assert.equal((await api(`/api/admin/incidents/${randomUUID()}/evidence`,'GET',undefined,'admin')).status,404);
});
test('incident discovery reports expiry, active hold and unavailable keys without exposing key identifiers',async t=>{
const now=Date.now();t.mock.timers.enable({apis:['Date'],now});const {api,store,eventId}=await fixture(t);await api('/api/device/evidence','POST',evidence(eventId));
const source=store.db.prepare('SELECT source_event_id FROM protected_evidence').get().source_event_id,incident=await api('/api/admin/incidents','POST',{eventId:source},'admin'),path=`/api/admin/incidents/${incident.body.id}/evidence`;
t.mock.timers.setTime(now+31*86400000);assert.equal((await api(path,'GET',undefined,'admin')).body.evidence[0].availability,'available');
await api(`/api/admin/incidents/${incident.body.id}`,'PATCH',{expectedRevision:1,status:'resolved'},'admin');assert.equal((await api(path,'GET',undefined,'admin')).body.evidence[0].availability,'expired');
t.mock.timers.setTime(now);store.db.prepare('UPDATE protected_evidence SET key_id=?').run('synthetic-other-key-id');const result=await api(path,'GET',undefined,'admin');assert.equal(result.body.evidence[0].availability,'key-unavailable');assert.equal(JSON.stringify(result.body).includes('synthetic-other-key-id'),false);
});
test('recognizable credentials in free text, target labels, filenames and encoded paths never persist',async t=>{
const {api,store,eventId}=await fixture(t);
const values=['-----BEGIN PRIVATE KEY-----\nsynthetic','Bearer syntheticAuthorizationCredential123','Basic c3ludGhldGljOnBhc3N3b3Jk','password="synthetic-secret"','passwd=synthetic-secret','api_key: synthetic-secret','access-token=synthetic-secret','refresh_token=synthetic-secret','client_secret=synthetic-secret','sk-syntheticCredential1234567890','ghp_syntheticCredential1234567890','AKIAABCDEFGHIJKLMNOP','eyJsyntheticHeader.syntheticPayload.syntheticSignature'];
@@ -11,11 +49,22 @@ test('recognizable credentials in free text, target labels, filenames and encode
assert.equal(store.db.prepare('SELECT COUNT(*) n FROM protected_evidence').get().n,0);
assert.equal((await api('/api/device/evidence','POST',evidence(eventId))).status,201);
});
test('encrypted evidence only decrypts for explicit admin permission and audited purpose',async t=>{const {api,store,eventId}=await fixture(t),body=evidence(eventId),created=await api('/api/device/evidence','POST',body);assert.equal(created.status,201);const row=store.db.prepare('SELECT * FROM protected_evidence').get();assert.equal(Buffer.from(row.ciphertext).includes(Buffer.from(body.payload.content)),false);for(const role of ['operator','auditor'])assert.equal((await api(`/api/admin/evidence/${created.body.id}?reason=investigation`,'GET',undefined,role)).status,403);assert.equal((await api(`/api/admin/evidence/${created.body.id}`,'GET',undefined,'admin')).status,400);const got=await api(`/api/admin/evidence/${created.body.id}?reason=incident-response`,'GET',undefined,'admin');assert.equal(got.status,200);assert.equal(got.body.payload.content,body.payload.content);const audit=store.db.prepare("SELECT metadata FROM events WHERE kind='evidence.accessed'").get();assert.equal(JSON.parse(audit.metadata).reason,'incident-response');assert.equal(store.db.prepare('SELECT metadata FROM events').all().some(r=>r.metadata.includes(body.payload.content)),false);assert.equal((await api('/api/device/evidence','POST',body)).status,409);});
test('encrypted evidence only decrypts for explicit admin permission and audited purpose',async t=>{const {api,store,eventId}=await fixture(t),body=evidence(eventId),created=await api('/api/device/evidence','POST',body);assert.equal(created.status,201);const row=store.db.prepare('SELECT * FROM protected_evidence').get();assert.equal(Buffer.from(row.ciphertext).includes(Buffer.from(body.payload.content)),false);for(const role of ['operator','auditor'])assert.equal((await api(`/api/admin/evidence/${created.body.id}?reason=investigation`,'GET',undefined,role)).status,403);assert.equal((await api(`/api/admin/evidence/${created.body.id}`,'GET',undefined,'admin')).status,400);const got=await api(`/api/admin/evidence/${created.body.id}?reason=incident-response`,'GET',undefined,'admin');assert.equal(got.status,200);assert.equal(got.body.payload.content,body.payload.content);const audit=store.db.prepare("SELECT metadata FROM events WHERE kind='evidence.accessed'").get();assert.equal(JSON.parse(audit.metadata).reason,'incident-response');assert.equal(store.db.prepare('SELECT metadata FROM events').all().some(r=>r.metadata.includes(body.payload.content)),false);assert.equal((await api('/api/device/evidence','POST',body)).status,200);});
test('unknown credentials, password fields, token fields, unowned events and credential URLs reject',async t=>{const {api,eventId}=await fixture(t);for(const extra of [{password:'synthetic'},{authorization:'Bearer synthetic'},{sensitiveField:true},{pageUrl:'https://user:[email protected]/'},{pageUrl:'https://example.test/?token=secret'}])assert.equal((await api('/api/device/evidence','POST',{...evidence(eventId),payload:{...evidence(eventId).payload,...extra}})).status,400);assert.equal((await api('/api/device/evidence','POST',evidence(randomUUID()))).status,404);assert.equal((await api('/api/device/evidence','POST',{...evidence(eventId),type:'download'})).status,400);});
test('missing key fails closed and ciphertext tampering fails authentication',async t=>{const disabled=await fixture(t,{configured:false});assert.equal((await disabled.api('/api/device/evidence','POST',evidence(disabled.eventId))).status,503);const {api,store,eventId}=await fixture(t),created=await api('/api/device/evidence','POST',evidence(eventId));store.db.prepare('UPDATE protected_evidence SET expires_at=?').run('2099-01-01T00:00:00.000Z');assert.equal((await api(`/api/admin/evidence/${created.body.id}?reason=investigation`,'GET',undefined,'admin')).status,500);});
test('audit failure rolls back evidence and prevents plaintext response',async t=>{const {api,store,eventId}=await fixture(t),emit=store.event;store.event=()=>{throw Error('synthetic audit failure');};assert.equal((await api('/api/device/evidence','POST',evidence(eventId))).status,500);assert.equal(store.db.prepare('SELECT COUNT(*) n FROM protected_evidence').get().n,0);store.event=emit;const created=await api('/api/device/evidence','POST',evidence(eventId));store.event=()=>{throw Error('synthetic audit failure');};const got=await api(`/api/admin/evidence/${created.body.id}?reason=investigation`,'GET',undefined,'admin');assert.equal(got.status,500);assert.equal(got.body.payload,undefined);store.event=emit;});
test('retention preserves encrypted evidence source events',async t=>{const {api,store,eventId}=await fixture(t);await api('/api/device/evidence','POST',evidence(eventId));store.db.prepare('UPDATE events SET time=?').run('2020-01-01T00:00:00.000Z');const {pruneAuditEvents}=await import('../apps/server/audit-retention.mjs');pruneAuditEvents(store,{days:1,exportedThrough:1000,dryRun:false});assert.ok(store.db.prepare("SELECT id FROM events WHERE kind='clipboard.copy'").get());});
test('retention preserves encrypted evidence source events',async t=>{const {api,store,eventId}=await fixture(t);await api('/api/device/evidence','POST',evidence(eventId));const {pruneAuditEvents}=await import('../apps/server/audit-retention.mjs');pruneAuditEvents(store,{now:Date.now()+60*86400000,days:1,exportedThrough:1000,dryRun:false});assert.ok(store.db.prepare("SELECT id FROM events WHERE kind='clipboard.copy'").get());});
test('download facts roundtrip while a different enrolled device cannot attach evidence',async t=>{const {api,store,id}=await fixture(t),eventId=randomUUID();store.event(id,'download.released',{eventId});const payload={sourceUrl:'https://downloads.example/package.exe',name:'package.exe',sha256:'a'.repeat(64),bytes:1024,mime:'application/octet-stream',decision:'allow'};const result=await api('/api/device/evidence','POST',{eventId,type:'download',payload});assert.equal(result.status,201);const detail=await api(`/api/admin/evidence/${result.body.id}?reason=compliance`,'GET',undefined,'admin');assert.deepEqual(detail.body.payload,payload);const other=randomUUID();store.db.prepare('INSERT INTO devices(id,revoked) VALUES(?,0)').run(other);const otherEvent=randomUUID();store.event(other,'clipboard.copy',{eventId:otherEvent});assert.equal((await api('/api/device/evidence','POST',evidence(otherEvent))).status,404);});
test('expired access is denied; incident legal hold does not bypass ciphertext integrity',async t=>{const {api,store,eventId}=await fixture(t),created=await api('/api/device/evidence','POST',evidence(eventId));store.db.prepare('UPDATE protected_evidence SET expires_at=?').run('2000-01-01T00:00:00.000Z');assert.equal((await api(`/api/admin/evidence/${created.body.id}?reason=investigation`,'GET',undefined,'admin')).status,410);const source=store.db.prepare('SELECT source_event_id FROM protected_evidence').get().source_event_id,incident=await api('/api/admin/incidents','POST',{eventId:source},'admin');assert.equal(incident.status,201);assert.equal((await api(`/api/admin/evidence/${created.body.id}?reason=incident-response`,'GET',undefined,'admin')).status,500);});
test('genuine expiry preserves readability only while the linked incident is active',async t=>{const now=Date.now();t.mock.timers.enable({apis:['Date'],now});const {api,store,eventId}=await fixture(t),created=await api('/api/device/evidence','POST',evidence(eventId));t.mock.timers.setTime(now+31*86400000);assert.equal((await api(`/api/admin/evidence/${created.body.id}?reason=investigation`,'GET',undefined,'admin')).status,410);const source=store.db.prepare('SELECT source_event_id FROM protected_evidence').get().source_event_id,incident=await api('/api/admin/incidents','POST',{eventId:source},'admin');assert.equal(incident.status,201);assert.equal((await api(`/api/admin/evidence/${created.body.id}?reason=incident-response`,'GET',undefined,'admin')).status,200);assert.equal((await api(`/api/admin/incidents/${incident.body.id}`,'PATCH',{expectedRevision:1,status:'resolved'},'admin')).status,200);assert.equal((await api(`/api/admin/evidence/${created.body.id}?reason=investigation`,'GET',undefined,'admin')).status,410);});
test('centrally disabled and expired evidence policies reject new capture while retaining prior access',async t=>{
const {api,store,eventId}=await fixture(t),body=evidence(eventId),created=await api('/api/device/evidence','POST',body);assert.equal(created.status,201);
const p=store.get('policy');p.evidence.enabled=false;store.put('policy',p);assert.equal((await api('/api/device/evidence','POST',body)).status,403);
assert.equal((await api(`/api/admin/evidence/${created.body.id}?reason=investigation`,'GET',undefined,'analyst')).status,200);
p.evidence.enabled=true;p.expiresAt=new Date(Date.now()-1000).toISOString();store.put('policy',p);assert.equal((await api('/api/device/evidence','POST',body)).status,403);
});
test('exact evidence replay is idempotent but a changed payload remains a conflict',async t=>{
const {api,store,eventId}=await fixture(t),body=evidence(eventId),created=await api('/api/device/evidence','POST',body),replayed=await api('/api/device/evidence','POST',body);assert.equal(replayed.status,200);assert.equal(replayed.body.id,created.body.id);assert.equal(replayed.body.replayed,true);
assert.equal((await api('/api/device/evidence','POST',{...body,payload:{...body.payload,content:'Different synthetic content'}})).status,409);
assert.equal(store.db.prepare('SELECT count(*) n FROM protected_evidence').get().n,1);assert.equal(store.db.prepare("SELECT count(*) n FROM events WHERE kind='evidence.recorded'").get().n,1);
});
+52
View File
@@ -0,0 +1,52 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
const {createFileUploadGate}=createRequire(import.meta.url)('../apps/browser/file-upload-gate.cjs');
function fixture(inspectPart=async()=>({decision:'allow',coverage:{complete:true}})){
let current=true,alive=true,releases=0,acquires=0,lost;
const aborted=[];const context={};
const gate=createFileUploadGate({maxBytes:()=>100,isCurrent:()=>current,abort:id=>aborted.push(id),inspectPart,
createLease:async({paths,onLost})=>{acquires++;lost=onLost;return {files:paths.map(path=>({path,bytes:Buffer.from('file')})),isAlive:()=>alive,release:async()=>{releases++;}};}});
const details={id:1,url:'https://example.test/upload',uploadData:[{type:'rawData',bytes:Buffer.from('prefix')},{type:'file',file:'C:/synthetic',offset:0,length:-1},{type:'rawData',bytes:Buffer.from('suffix')}]};
return {gate,context,details,aborted,get releases(){return releases;},get acquires(){return acquires;},change(){current=false;},lose(){alive=false;lost();}};
}
test('approved full-file multipart holds leases through terminal and returns hash-free facts',async()=>{
const f=fixture();const result=await f.gate.inspect(f.details,f.context);assert.equal(result.decision,'allow');assert.deepEqual(result.request,{bytes:16,detectedType:'unknown'});assert.equal(f.releases,0);
assert.deepEqual(await f.gate.inspect(f.details,f.context),result);assert.equal(f.acquires,1);await f.gate.terminal(1);assert.equal(f.releases,1);
});
test('denied scan aborts but retains lock until native terminal',async()=>{
const f=fixture(async()=>({decision:'block',ruleId:'secret'}));assert.equal((await f.gate.inspect(f.details,f.context)).ruleId,'secret');assert.deepEqual(f.aborted,[1]);assert.equal(f.releases,0);await f.gate.terminal(1);assert.equal(f.releases,1);
});
test('ranges and data-pipe shapes fail before lease acquisition',async()=>{
for(const part of [{type:'file',file:'C:/synthetic',offset:1,length:-1},{type:'blob',blobUUID:'x'},{type:'rawData',bytes:Buffer.from('x'),file:'C:/x'}]){
const f=fixture();f.details.uploadData=[part];assert.equal((await f.gate.inspect(f.details,f.context)).decision,'block');assert.equal(f.acquires,0);await f.gate.terminal(1);
}
});
test('joined raw segments detect boundary split and never produce generic request hash',async()=>{
const f=fixture(async bytes=>bytes.toString()==='prefixsuffix'?{decision:'block',ruleId:'joined'}:{decision:'allow',coverage:{complete:true}});
assert.equal((await f.gate.inspect(f.details,f.context)).ruleId,'joined');await f.gate.terminal(1);
});
test('policy change during scan and helper loss retain lease until terminal',async()=>{
let f;f=fixture(async()=>{f.change();return {decision:'allow',coverage:{complete:true}};});assert.equal((await f.gate.inspect(f.details,f.context)).decision,'block');assert.equal(f.releases,0);await f.gate.terminal(1);
const g=fixture();await g.gate.inspect(g.details,g.context);g.lose();assert.deepEqual(g.aborted,[1]);assert.equal(g.releases,0);await g.gate.terminal(1);
});
test('missing complete coverage denies and concurrency includes denied pending leases',async()=>{
const f=fixture(async()=>({decision:'allow'}));for(let id=1;id<=8;id++)assert.equal((await f.gate.inspect({...f.details,id},f.context)).decision,'block');assert.equal((await f.gate.inspect({...f.details,id:9},f.context)).ruleId,'upload-capacity');f.gate.dispose();assert.equal(f.releases,0);for(let id=1;id<=8;id++)await f.gate.terminal(id);assert.equal(f.releases,8);
});
test('same native id cannot reuse approval for a changed body or method',async()=>{
const f=fixture();await f.gate.inspect(f.details,f.context);f.details.uploadData[0].bytes[0]=88;
assert.equal((await f.gate.inspect(f.details,f.context)).ruleId,'request-changed');assert.equal(f.releases,0);await f.gate.terminal(1);
const g=fixture();await g.gate.inspect(g.details,g.context);assert.equal((await g.gate.inspect({...g.details,method:'PUT'},g.context)).ruleId,'request-changed');await g.gate.terminal(1);
});
test('raw snapshots precede asynchronous acquisition and file scan receives exact path',async()=>{
const raw=Buffer.from('original'),seen=[];let released=0;
const gate=createFileUploadGate({maxBytes:()=>100,isCurrent:()=>true,abort:()=>{},
createLease:async({paths})=>{raw.fill(88);return {files:[{path:paths[0],bytes:Buffer.from('file')}],isAlive:()=>true,release:async()=>{released++;}};},
inspectPart:async(bytes,options)=>{seen.push({text:bytes.toString(),filename:options.filename});return {decision:'allow',coverage:{complete:true}};}});
assert.equal((await gate.inspect({id:1,url:'https://example.test',method:'POST',uploadData:[{type:'rawData',bytes:raw},{type:'file',file:'C:/specific.xls',offset:0,length:-1}]},{})).decision,'allow');
assert.equal(seen[0].text,'original');assert.equal(seen[1].filename,'C:/specific.xls');assert.equal(seen[2].text,'original');await gate.terminal(1);assert.equal(released,1);
});
test('terminal lease cleanup failures propagate without pretending success',async()=>{
const gate=createFileUploadGate({maxBytes:()=>100,isCurrent:()=>true,abort:()=>{},createLease:async({paths})=>({files:[{path:paths[0],bytes:Buffer.from('file')}],isAlive:()=>true,release:async()=>{throw Error('cleanup-failed');}}),inspectPart:async()=>({decision:'allow',coverage:{complete:true}})});
await gate.inspect({id:1,url:'https://example.test',uploadData:[{type:'file',file:'C:/specific.xls',offset:0,length:-1}]},{});await assert.rejects(gate.terminal(1),/cleanup-failed/);
});
+25
View File
@@ -0,0 +1,25 @@
BSD 2-Clause License
Copyright (c) 2013-2025, John McNamara <[email protected]>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+1
View File
@@ -0,0 +1 @@
Benign say_hello VBA fixture from XlsxWriter examples/vbaProject.bin, revision 5d4606d89a955226d2d0825a0f44309043ae7251. SHA-256: 0ced1464b3677e98f5e3a8c5d80135e18dc98dca39299f1a8cfd2a00999fbf9f. Base64 stores binary data transparently; tests never execute Office or macros. Original BSD-2-Clause license included.
+1
View File
@@ -0,0 +1 @@
0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAPgADAP7/CQAGAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAEAAAAgAAAAIAAAD+////AAAAAAAAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9////BwAAABQAAAAEAAAABQAAAAYAAAAIAAAADQAAAAkAAAAKAAAACwAAAAwAAAAOAAAAGwAAAA8AAAAQAAAAEQAAABIAAAATAAAAFQAAAP7///8WAAAAFwAAABgAAAAZAAAAGgAAABwAAAD+////HQAAAP7//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////1IAbwBvAHQAIABFAG4AdAByAHkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWAAUA//////////8OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALCnnEHE988BAwAAAMAsAAAAAAAAVgBCAEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAQD//////////wQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALCnnEHE988BsKecQcT3zwEAAAAAAAAAAAAAAABUAGgAaQBzAFcAbwByAGsAYgBvAG8AawAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGgACAP///////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADhAwAAAAAAAFMAaABlAGUAdAAxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAIBCgAAAAgAAAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAANoDAAAAAAAAAQAAAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAAMAAAADQAAAA4AAAAPAAAA/v///xEAAAASAAAAEwAAABQAAAAVAAAAFgAAABcAAAAYAAAAGQAAABoAAAAbAAAAHAAAAB0AAAAeAAAAHwAAAP7///8hAAAAIgAAACMAAAAkAAAAJQAAACYAAAAnAAAAKAAAACkAAAAqAAAAKwAAACwAAAAtAAAALgAAAC8AAAAwAAAA/v///zIAAAAzAAAANAAAAP7///82AAAA/v///zgAAAA5AAAAOgAAADsAAAA8AAAAPQAAAD4AAAA/AAAAQAAAAEEAAABCAAAAQwAAAEQAAABFAAAARgAAAP7///9IAAAASQAAAEoAAABLAAAATAAAAE0AAABOAAAATwAAAFAAAABRAAAAUgAAAFMAAABUAAAAVQAAAFYAAAD+////WAAAAFkAAABaAAAAWwAAAFwAAABdAAAAXgAAAF8AAABgAAAAYQAAAGIAAABjAAAAZAAAAGUAAABmAAAAZwAAAGgAAABpAAAAagAAAGsAAABsAAAAbQAAAG4AAABvAAAAcAAAAHEAAAByAAAAcwAAAHQAAAB1AAAAdgAAAHcAAAB4AAAAeQAAAHoAAAB7AAAAfAAAAH0AAAB+AAAAfwAAAIAAAAABFgEAAfAAAADMAgAA1AAAAAACAAD/////0wIAACcDAAAAAAAAAQAAANqdJiAAAP//IwEAAIgAAAC2AP//AQEAAAAA/////wAAAAD///////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAwAAAAUAAAAHAAAA//////////8BAQgAAAD/////eAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAAAAE1FAAD///////8AAAAA//8AAAAA//8BAQAAAADfAP//AAAAAAwA//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8oAAAAAgBTTP////8AAAEAUxD/////AAABAFOU/////wAAAAACPP////8AAP//AQEAAAAAAQBOADAAewAwADAAMAAyADAAOAAxADkALQAwADAAMAAwAC0AMAAwADAAMAAtAEMAMAAwADAALQAwADAAMAAwADAAMAAwADAAMAAwADQANgB9AAYAAAD/////AQFAAAAAAoD+//////8QAP//KAAAAAIB//8AAAAAAAAAAP//////////AAAAAB0AAAAlAAAA/////0gAAAD/////QAAAAAAAAAAAAAEAAAAAAAAAAAD///////////////8AAAAA//////////////////////////8AAAAA//////////////////////////8AAAAAAQAAAP//////////AAAAAP///////////////////////////////wEAMAAAAFEfZ1IgAN8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+ygEAAAD/////AQEIAAAA/////3gAAAD/////AAABsLAAQXR0cmlidXQAZSBWQl9OYW0AZSA9ICJUaGkAc1dvcmtib28QayINCgqMQmFzAQKMMHswMDAyMFA4MTktABAwAwhDBwAUAhIBJDAwNDZ9gQ18R2xvYmFsAdAQU3BhYwGSRmFsBHNlDGRDcmVhdAhhYmwVH1ByZWSQZWNsYQAGSWQAsQhUcnUNQkV4cG8Ec2UUHFRlbXBsAGF0ZURlcml2AwISkkJ1c3RvbWkGegREAzIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARYBAAHwAAAAzAIAANQAAAAAAgAA/////9MCAAAnAwAAAAAAAAEAAADanYU6AAD//yMBAACIAAAAtgD//wEBAAAAAP////8AAAAA////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAMAAAAFAAAABwAAAP//////////AQEIAAAA/////3gAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wAAAABNRQAA////////AAAAAP//AAAAAP//AQEAAAAA3wD//wAAAAAMAP//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////KAAAAAIAU0z/////AAABAFMQ/////wAAAQBTlP////8AAAAAAjz/////AAD//wEBAAAAAAEATgAwAHsAMAAwADAAMgAwADgAMQA5AC0AMAAwADAAMAAtADAAMAAwADAALQBDADAAMAAwAC0AMAAwADAAMAAwADAAMAAwADAAMAA0ADYAfQAGAAAA/////wEBQAAAAAKA/v//////EAD//ygAAAACAf//AAAAAAAAAAD//////////wAAAAAdAAAAJQAAAP////9IAAAA/////0AAAAAAAAAAAAABAAAAAAAAAAAA////////////////AAAAAP//////////////////////////AAAAAP//////////////////////////AAAAAAEAAAD////Line truncated
+38
View File
@@ -0,0 +1,38 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import vm from 'node:vm';
import {readFileSync} from 'node:fs';
import {createRequire} from 'node:module';
const require=createRequire(import.meta.url);
const source=readFileSync(new URL('../apps/browser/main.cjs',import.meta.url),'utf8');
const hook=source.slice(source.indexOf(" browserSession.webRequest.onBeforeSendHeaders("),source.indexOf(' // Session request, header and permission gates'));
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,
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(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}})};
}
const settle=async()=>{for(let n=0;n<8;n++)await Promise.resolve();};
test('native header deadline cancels once, records static audit reason and refuses late scanner approval',async()=>{
const f=fixture();await settle();assert.equal(f.callbacks.length,0);
f.timers[0]();await settle();assert.equal(f.callbacks.length,1);assert.equal(f.callbacks[0].cancel,true);
assert.deepEqual(f.events,[['dlp.block','headers-inspection-timeout','example.test']]);
f.timers[0]();f.finish();await settle();assert.equal(f.callbacks.length,1);assert.equal(f.events.length,1);
});
test('audit failure cannot release a timed-out request or create unhandled rejection',async()=>{
const f=fixture({auditFails:true});await settle();f.timers[0]();await settle();f.finish();await settle();
assert.equal(f.callbacks.length,1);assert.equal(f.callbacks[0].cancel,true);assert.equal(f.events.length,1);
});
test('completed header approval is not later relabelled as a timeout',async()=>{
const f=fixture();await settle();f.finish();await settle();assert.equal(f.callbacks.length,1);assert.equal(f.callbacks[0].cancel,false);
f.timers[0]();await settle();assert.equal(f.callbacks.length,1);assert.equal(f.events.length,0);
});
+53
View File
@@ -0,0 +1,53 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
import {createHash} from 'node:crypto';
import {defaultPolicy,inspect} from '../packages/core/policy.mjs';
const {snapshotHeaders,inspectHeaders}=createRequire(import.meta.url)('../apps/browser/header-inspection.cjs');
function policy(){const p=defaultPolicy();p.dlp.rules=[{id:'roadmap',label:'Roadmap',kind:'literal',value:'Secret Roadmap',action:'block'}];return p;}
async function check(headers,p=policy(),overrides={}){return inspectHeaders(snapshotHeaders(headers,p),{policy:p,inspect,isCurrent:()=>true,...overrides});}
test('custom, credential and browser metadata header values obey the same central DLP',async()=>{
for(const name of ['X-Data','Cookie','Authorization','Proxy-Authorization','Referer','User-Agent'])assert.equal((await check({[name]:'Secret Roadmap'})).ruleId,'roadmap');
assert.equal((await check({'X-Data':'Secret%20Roadmap'})).ruleId,'roadmap');
assert.equal((await check({'X-First':'Secret ','X-Second':'Roadmap'})).ruleId,'roadmap');
assert.equal((await check({'Secret-Roadmap':'public'})).decision,'allow');
assert.equal((await check({Authorization:'Bearer sk-123456789012345678901234'})).ruleId,'api-secret');
assert.equal((await check({Authorization:'Bearer eyJhbGciOiJIUzI1NiJ9.opaque.signature',Cookie:'session=ordinaryOpaqueValue'})).decision,'allow');
});
test('per-header exact hashes and registered content phrases remain effective',async()=>{
const p=policy();p.dlp.rules=[{id:'reference',label:'Reference',kind:'sha256',value:createHash('sha256').update('known exact reference').digest('hex'),action:'block'}];
assert.equal((await check({'X-Data':'known exact reference'},p)).ruleId,'reference');
p.security.content.blockedPhrases=['Internal planning'];
assert.equal((await check({Cookie:'x=Internal+planning'},p)).ruleId,'content-blocklist');
});
test('snapshot rejects malformed, inherited, accessor and oversized input without executing getters',()=>{
const p=policy();let getterCalled=false;const input={};Object.defineProperty(input,'X',{enumerable:true,get(){getterCalled=true;return 'public';}});
assert.throws(()=>snapshotHeaders(input,p),/uninspectable/);assert.equal(getterCalled,false);
for(const malformed of [null,[],Object.create({X:'secret'}),{'Bad Header':'public'},{X:['public']},{X:'bad\r\nheader'},{X:'bad\0header'}])assert.throws(()=>snapshotHeaders(malformed,p),/uninspectable/);
assert.throws(()=>snapshotHeaders(Object.fromEntries(Array.from({length:129},(_,i)=>[`X-${i}`,'ok'])),p),/size-limit/);
assert.throws(()=>snapshotHeaders({X:'a'.repeat(65536)},p),/size-limit/);
p.dlp.maxBytes=1024;assert.throws(()=>snapshotHeaders({X:'a'.repeat(1024)},p),/size-limit/);
});
test('approved snapshot cannot be changed by input mutations or scanners; buffers wiped',async()=>{
const p=policy(),input={Cookie:'ordinary=session',X:'public'},snapshot=snapshotHeaders(input,p);input.X='Secret Roadmap';
let held;const result=await inspectHeaders(snapshot,{policy:p,inspect,isCurrent:()=>true,inspectEnterprise:async bytes=>{held=bytes;return {decision:'allow',coverage:{complete:true}};}});
assert.equal(result.decision,'allow');assert.equal(snapshot.headers.X,'public');assert.throws(()=>{snapshot.headers.X='Secret Roadmap';},TypeError);assert.ok(held.every(byte=>byte===0));
assert.deepEqual(Object.keys(result).sort(),['decision','ruleId']);
});
test('local and enterprise inspection require complete allow and current policy',async()=>{
const p=policy();p.dlp.semantic.enabled=true;
assert.equal((await check({X:'public'},p)).ruleId,'inspector-unavailable');
assert.equal((await check({X:'public'},p,{inspectLocal:async()=>({decision:'allow'})})).ruleId,'inspection-incomplete');
let enterpriseCalls=0;
assert.equal((await check({X:'public'},p,{inspectLocal:async()=>({decision:'block',ruleId:'semantic-secret'}),inspectEnterprise:async()=>{enterpriseCalls++;}})).ruleId,'semantic-secret');assert.equal(enterpriseCalls,0);
let current=true;
assert.equal((await check({X:'public'},p,{isCurrent:()=>current,inspectLocal:async()=>{current=false;return {decision:'allow',coverage:{complete:true}};}})).ruleId,'policy-changed');
p.dlp.semantic.enabled=false;
assert.equal((await check({X:'public'},p,{inspectEnterprise:async()=>({decision:'allow'})})).ruleId,'inspection-incomplete');
assert.equal((await check({X:'public'},p,{inspectEnterprise:async()=>({decision:'block',ruleId:'icap-malware'})})).ruleId,'icap-malware');
});
test('exceptions wipe full-header scan buffers and do not turn failure into allow',async()=>{
let held;
await assert.rejects(check({Cookie:'ordinary=session'},policy(),{inspectEnterprise:async bytes=>{held=bytes;throw Error('fixture scanner unavailable');}}),/unavailable/);
assert.ok(held.every(byte=>byte===0));
});
+21
View File
@@ -0,0 +1,21 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {EventEmitter} from 'node:events';
import {createRequire} from 'node:module';
const {performHistory}=createRequire(import.meta.url)('../apps/browser/history-navigation.cjs');
function fixture(entries=['https://example.test/one','https://example.test/two'],index=1){
const c=new EventEmitter();c.url=entries[index];c.stops=0;c.calls=0;c.index=index;c.isDestroyed=()=>false;c.getURL=()=>c.url;c.stop=()=>c.stops++;c.navigationHistory={getActiveIndex:()=>c.index,getEntryAtIndex:i=>entries[i]?{url:entries[i]}:null,goToIndex:i=>{c.calls++;c.index=i;c.url=entries[i];}};c.reload=()=>c.calls++;return c;
}
const run=(contents,extra={})=>performHistory({contents,action:'back',allowed:()=>true,current:()=>true,timeoutMs:100,...extra});
const dispatched=async c=>{for(let i=0;i<20&&!c.calls;i++)await new Promise(r=>setImmediate(r));assert.equal(c.calls,1);};
test('native back/forward target and main-frame in-page completion',async()=>{const c=fixture();let p=run(c);await dispatched(c);c.emit('did-navigate-in-page',{},c.url,true);assert.deepEqual(await p,{completed:true,url:'https://example.test/one'});c.calls=0;p=run(c,{action:'forward'});await dispatched(c);c.emit('did-finish-load');assert.equal((await p).url,'https://example.test/two');assert.equal(c.eventNames().length,0);});
test('missing history is a no-op',async()=>{const c=fixture(['https://example.test/'],0);assert.deepEqual(await run(c),{completed:false,url:c.url});assert.equal(c.calls,0);});
test('blocked protocols, credentials and policy reject before side effects',async()=>{for(const url of ['file:///secret','https://user:[email protected]/','https://example.test/']){const c=fixture([url,'https://example.test/two']);await assert.rejects(run(c,{allowed:()=>false}),/denied/);assert.equal(c.calls,0);}});
test('history races while awaiting policy cannot change inspected destination',async()=>{const c=fixture();await assert.rejects(run(c,{allowed:async()=>{c.index=0;return true;}}),/destination changed/);assert.equal(c.calls,0);});
test('stale context and policy revocation after dispatch stop navigation',async()=>{for(const mode of ['context','policy']){const c=fixture();let valid=true;const p=run(c,{current:()=>mode==='context'?valid:true,allowed:()=>mode==='policy'?valid:true});await dispatched(c);valid=false;c.emit('did-finish-load');await assert.rejects(p,/context changed|destination denied/);assert.equal(c.stops,1);assert.equal(c.eventNames().length,0);}});
test('abort before and after dispatch has bounded cleanup',async()=>{const c=fixture();const controller=new AbortController();controller.abort();await assert.rejects(run(c,{signal:controller.signal}),/cancelled/);assert.equal(c.calls,0);const next=new AbortController();const p=run(c,{signal:next.signal});await dispatched(c);next.abort();await assert.rejects(p,/cancelled/);assert.equal(c.stops,1);assert.equal(c.eventNames().length,0);});
test('same-URL reload awaits actual event, subframe events ignored',async()=>{const c=fixture();let done=false;const p=run(c,{action:'reload'}).then(v=>{done=true;return v;});await dispatched(c);c.emit('did-navigate-in-page',{},c.url,false);c.emit('did-fail-load',{},-1,'private error',c.url,false);await new Promise(r=>setImmediate(r));assert.equal(done,false);c.emit('did-finish-load');assert.equal((await p).completed,true);});
test('unexpected final URL and mainframe load failure reject; timeout stops',async()=>{for(const mode of ['redirect','failure','timeout']){const c=fixture();const p=run(c,{timeoutMs:30});await dispatched(c);if(mode==='redirect'){c.url='https://other.test/';c.emit('did-finish-load');}if(mode==='failure')c.emit('did-fail-load',{},-1,'secret',c.url,true);await assert.rejects(p,/destination changed|navigation failed|timed out/);assert.equal(c.stops,1);assert.equal(c.eventNames().length,0);}});
test('missing positive native context proof denies history before any side effect',async()=>{const c=fixture();await assert.rejects(run(c,{current:async()=>undefined}),/context changed/);assert.equal(c.calls,0);});
test('context guards distinguish captured state before dispatch from intentional navigation after',async()=>{const c=fixture(),phases=[];const p=run(c,{current:phase=>{phases.push(phase);return phase==='before'?c.calls===0:c.calls===1;}});await dispatched(c);c.emit('did-finish-load');assert.equal((await p).completed,true);assert.ok(phases.includes('before')&&phases.includes('after'));});
+1 -1
View File
@@ -6,4 +6,4 @@ test('auditor cannot triage and unrecognized routes never gain permissions',asyn
test('filters and pagination are bounded and deterministic',async t=>{const {store,api}=await fixture(t);for(let i=0;i<3;i++){store.event(null,'upload.blocked',{});const source=store.db.prepare("SELECT id FROM events WHERE kind='upload.blocked' ORDER BY id DESC LIMIT 1").get();await api('','POST',{eventId:source.id,severity:i===0?'low':'high'});}const first=await api('?limit=1&severity=high');assert.equal(first.body.incidents.length,1);assert.ok(first.body.nextBefore);const next=await api('?limit=1&severity=high&before='+first.body.nextBefore);assert.equal(next.body.incidents.length,1);assert.notEqual(first.body.incidents[0].id,next.body.incidents[0].id);assert.equal(next.body.nextBefore,null);assert.equal((await api('?status=resolved')).body.incidents.length,0);assert.equal((await api('?limit=101')).status,400);assert.equal((await api('?status=bogus')).status,400);});
test('audit failure rolls back case creation and triage history atomically',async t=>{const {store,api}=await fixture(t);store.event(null,'upload.blocked',{});const emit=store.event;store.event=()=>{throw Error('synthetic audit unavailable');};assert.equal((await api('','POST',{eventId:1})).status,500);assert.equal(store.db.prepare('SELECT COUNT(*) n FROM incident_cases').get().n,0);assert.equal(store.db.prepare('SELECT COUNT(*) n FROM incident_changes').get().n,0);store.event=emit;const created=await api('','POST',{eventId:1});store.event=()=>{throw Error('synthetic audit unavailable');};assert.equal((await api('/'+created.body.id,'PATCH',{expectedRevision:1,status:'resolved'})).status,500);assert.equal(store.db.prepare('SELECT revision,status FROM incident_cases').get().revision,1);assert.equal(store.db.prepare('SELECT COUNT(*) n FROM incident_changes').get().n,1);store.event=emit;});
test('real resolved incident retains source and immutable history through event retention',async t=>{const {store,api}=await fixture(t);store.event(null,'upload.blocked',{ruleId:'confidential'});const source=store.db.prepare('SELECT id FROM events ORDER BY id DESC LIMIT 1').get().id;const created=await api('','POST',{eventId:source});const resolved=await api('/'+created.body.id,'PATCH',{expectedRevision:1,status:'resolved',comment:'Synthetic resolution note'});assert.equal(resolved.status,200);store.db.prepare('UPDATE events SET time=?').run('2020-01-01T00:00:00.000Z');const {pruneAuditEvents}=await import('../apps/server/audit-retention.mjs');const result=pruneAuditEvents(store,{days:30,exportedThrough:100,dryRun:false});assert.ok(result.eligible>0);const detail=await api('/'+created.body.id);assert.equal(detail.status,200);assert.equal(detail.body.status,'resolved');assert.equal(detail.body.event.id,source);assert.equal(detail.body.history.length,2);assert.equal(detail.body.history[1].comment,'Synthetic resolution note');assert.equal(store.db.prepare('SELECT kind FROM events WHERE id=?').get(source).kind,'upload.blocked');});
test('real resolved incident retains source and immutable history through event retention',async t=>{const {store,api}=await fixture(t);store.event(null,'upload.blocked',{ruleId:'confidential'});const source=store.db.prepare('SELECT id FROM events ORDER BY id DESC LIMIT 1').get().id;const created=await api('','POST',{eventId:source});const resolved=await api('/'+created.body.id,'PATCH',{expectedRevision:1,status:'resolved',comment:'Synthetic resolution note'});assert.equal(resolved.status,200);const {pruneAuditEvents}=await import('../apps/server/audit-retention.mjs');const result=pruneAuditEvents(store,{now:Date.now()+60*86400000,days:30,exportedThrough:100,dryRun:false});assert.ok(result.eligible>0);const detail=await api('/'+created.body.id);assert.equal(detail.status,200);assert.equal(detail.body.status,'resolved');assert.equal(detail.body.event.id,source);assert.equal(detail.body.history.length,2);assert.equal(detail.body.history[1].comment,'Synthetic resolution note');assert.equal(store.db.prepare('SELECT kind FROM events WHERE id=?').get(source).kind,'upload.blocked');});
@@ -0,0 +1,18 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
const {createLibraryOrganization}=createRequire(import.meta.url)('../apps/browser/library-organization.cjs');
function fixture(){
const snapshot={bookmarks:[{id:'b1'}],tabs:[{id:'t1'}]},plan={bookmarks:[{id:'b1',folder:'Work'}],tabs:[{id:'t1',group:'Research'}]};
let current=true,auto=false,commits=0,applied=0;
const f={capture:async()=>snapshot,assertCurrent:()=>{if(!current)throw Error('stale');},inspect:async()=>{},propose:async()=>({proposalId:'p1',plan}),commit:async()=>{commits++;return {revision:2};},applyTabs:()=>{applied++;},audit:async()=>{},autoAllowed:()=>auto};
return {f,plan,stale:()=>current=false,auto:()=>auto=true,counts:()=>({commits,applied})};
}
test('native organization requires review and pins authoritative plan',async()=>{const x=fixture(),o=createLibraryOrganization(x.f);const result=await o.plan();result.plan.tabs[0].group='Hostile';await assert.rejects(o.apply({proposalId:'p1'}),/Review/);await o.apply({proposalId:'p1',confirm:true});assert.deepEqual(x.counts(),{commits:1,applied:1});await assert.rejects(o.apply({proposalId:'p1',confirm:true}),/expired/);});
test('native stale context blocks before central commit',async()=>{const x=fixture(),o=createLibraryOrganization(x.f);await o.plan();x.stale();await assert.rejects(o.apply({proposalId:'p1',confirm:true}),/stale/);assert.deepEqual(x.counts(),{commits:0,applied:0});});
test('native postcommit context change explicitly leaves tabs untouched',async()=>{const x=fixture();x.f.commit=async()=>{x.stale();return {revision:2};};const o=createLibraryOrganization(x.f);await o.plan();await assert.rejects(o.apply({proposalId:'p1',confirm:true}),/Bookmarks were organized centrally; tabs were unchanged/);assert.equal(x.counts().applied,0);});
test('model cannot introduce URL, unknown ID or arbitrary properties',async()=>{const x=fixture();x.f.propose=async()=>({proposalId:'p1',plan:{bookmarks:[{id:'b1',folder:'Work',url:'https://evil.test'}],tabs:[{id:'t1',group:'R'}]}});await assert.rejects(createLibraryOrganization(x.f).plan());x.f.propose=async()=>({proposalId:'p1',plan:{bookmarks:[{id:'unknown',folder:'Work'}],tabs:[{id:'t1',group:'R'}]}});await assert.rejects(createLibraryOrganization(x.f).plan());});
test('automatic apply requires central local-only permission',async()=>{const x=fixture(),o=createLibraryOrganization(x.f);await o.plan();await assert.rejects(o.apply({proposalId:'p1',autoApply:true}),/Review/);x.auto();await o.apply({proposalId:'p1',autoApply:true});assert.equal(x.counts().applied,1);});
test('pending organization serializes library operations and releases reservation on failure',async()=>{const x=fixture();let release;x.f.propose=()=>new Promise(resolve=>release=resolve);const o=createLibraryOrganization(x.f),pending=o.plan();await new Promise(resolve=>setImmediate(resolve));assert.equal(o.isBusy(),true);await assert.rejects(o.plan(),/already running/);release({proposalId:'p1',plan:x.plan});await pending;assert.equal(o.isBusy(),false);});
test('aborted organization cannot commit or apply native groups',async()=>{const x=fixture(),o=createLibraryOrganization(x.f),controller=new AbortController();await o.plan();controller.abort();await assert.rejects(o.apply({proposalId:'p1',confirm:true},{signal:controller.signal}),/cancelled/);assert.deepEqual(x.counts(),{commits:0,applied:0});});
test('proposal binding cannot be replaced by an unrelated snapshot',async()=>{const x=fixture();x.f.capture=async()=>({bookmarks:[{id:'b1'}],tabs:[{id:'t1'}],snapshotHash:'a'.repeat(64),policyVersion:1,bookmarkRevision:1});x.f.propose=async()=>({proposalId:'p1',plan:x.plan,binding:{snapshotHash:'b'.repeat(64),policyVersion:1,bookmarkRevision:1}});await assert.rejects(createLibraryOrganization(x.f).plan(),/binding/);});
+73
View File
@@ -0,0 +1,73 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createServer} from 'node:http';
import {mkdtempSync,rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {openStore} from '../apps/server/store.mjs';
import {createApp} from '../apps/server/app.mjs';
import {librarySnapshotHash,validateLibraryPlan,LibraryPolicySchema} from '../packages/core/library.mjs';
import {verifyBookmarkSnapshot} from '../packages/core/bookmarks.mjs';
const bookmark={id:'personal',title:'Public project',url:'https://example.com/project',folder:''};
const tab={id:'tab-1',title:'Public documentation',url:'https://docs.example.com/path?session=private#secret'};
const plan={bookmarks:[{id:'personal',folder:'Projects'}],tabs:[{id:'tab-1',group:'Research'}]};
async function fixture(t){
const dir=mkdtempSync(join(tmpdir(),'library-test-')),store=openStore(dir),adminToken='library-fixture-admin';let output=JSON.stringify(plan),calls=0,prompt='',gate=null;
const provider=createServer(async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);prompt=JSON.parse(Buffer.concat(chunks).toString()).messages[0].content;calls++;if(gate)await gate;res.setHeader('content-type','application/json');res.end(JSON.stringify({choices:[{message:{content:output}}]}));});
await new Promise(r=>provider.listen(0,'127.0.0.1',r));
const config={adminToken,publicUrl:'http://127.0.0.1',secure:false,aiBaseUrl:`http://127.0.0.1:${provider.address().port}`,aiModel:'fixture',aiEndpointId:'default'};
const server=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>server.on('listening',r));
t.after(async()=>{await Promise.all([new Promise(r=>server.close(r)),new Promise(r=>provider.close(r))]);store.db.close();rmSync(dir,{recursive:true,force:true});});
async function api(route,body,token=adminToken){const r=await fetch(`http://127.0.0.1:${server.address().port}${route}`,{method:body?'POST':'GET',headers:{authorization:`Bearer ${token}`,...(body?{'content-type':'application/json'}:{})},body:body?JSON.stringify(body):undefined});return {status:r.status,body:await r.json()};}
const code=(await api('/api/admin/enrollments',{user:'fixture-user'})).body.code,device=(await api('/api/device/enroll',{code,name:'Fixture',platform:'win32'},'')).body;
const p=store.get('policy');p.ai={...p.ai,enabled:true,trustMode:'local',model:'fixture'};p.library={aiSuggestionsEnabled:true,aiAutoApplyEnabled:false};store.put('policy',p);
await api('/api/device/bookmarks',{action:'add',expectedRevision:0,policyVersion:1,bookmark},device.token);
const request={policyVersion:1,bookmarkRevision:1,tabs:[tab],snapshotHash:librarySnapshotHash([tab])};
return {store,device,api,request,propose:()=>api('/api/device/library/propose',request,device.token),setOutput:v=>{output=v;},setGate:v=>{gate=v;},calls:()=>calls,prompt:()=>prompt};
}
test('AI library defaults disabled and strict organization permits only existing item IDs and labels',()=>{
assert.deepEqual(LibraryPolicySchema.parse({}),{aiSuggestionsEnabled:false,aiAutoApplyEnabled:false});assert.deepEqual(validateLibraryPlan(plan,{bookmarks:[bookmark],tabs:[tab]}),plan);
for(const v of [{...plan,bookmarks:[{...plan.bookmarks[0],url:'https://evil.test'}]},{...plan,tabs:[{id:'other',group:'Research'}]},{...plan,bookmarks:[]},{...plan,tabs:[{id:tab.id,group:'a'.repeat(41)}]},{...plan,bookmarks:[{id:bookmark.id,folder:'bad\nlabel'}]}])assert.throws(()=>validateLibraryPlan(v,{bookmarks:[bookmark],tabs:[tab]}));
});
test('approved gateway proposes metadata-only AI plan and atomically applies signed personal bookmarks',async t=>{
const f=await fixture(t),result=await f.propose();assert.equal(result.status,200);assert.ok(f.prompt().includes('docs.example.com'));assert.ok(!f.prompt().includes('/path'));assert.ok(!f.prompt().includes('session=private'));assert.ok(!f.prompt().includes('/project'));
const applied=await f.api('/api/device/library/apply',{proposalId:result.body.proposalId,policyVersion:1,expectedRevision:1,snapshotHash:f.request.snapshotHash},f.device.token);assert.equal(applied.status,200);assert.equal(applied.body.revision,2);assert.equal(applied.body.items[0].folder,'Projects');assert.deepEqual(applied.body.tabs,plan.tabs);
assert.equal(verifyBookmarkSnapshot(applied.body.bookmarks,f.device.publicKey,{deviceId:f.device.id,policyVersion:1,minimumRevision:2}).items[0].folder,'Projects');
assert.equal((await f.api('/api/device/library/apply',{proposalId:result.body.proposalId,policyVersion:1,expectedRevision:2,snapshotHash:f.request.snapshotHash},f.device.token)).status,404);
const events=f.store.db.prepare("SELECT metadata FROM events WHERE kind LIKE 'library.%'").all();assert.equal(events.length,2);assert.ok(events.every(e=>!e.metadata.includes(bookmark.title)&&!e.metadata.includes(bookmark.url)&&!e.metadata.includes('Projects')));
});
test('central disable, hidden library, expired policy, malformed snapshot and disabled automatic application fail closed',async t=>{
const f=await fixture(t),p=f.store.get('policy');p.library.aiSuggestionsEnabled=false;f.store.put('policy',p);assert.equal((await f.propose()).status,403);assert.equal(f.calls(),0);
p.library.aiSuggestionsEnabled=true;p.browser.settings.hiddenSections=['library'];f.store.put('policy',p);assert.equal((await f.propose()).status,403);p.browser.settings.hiddenSections=[];f.store.put('policy',p);
assert.equal((await f.api('/api/device/library/propose',{...f.request,snapshotHash:'0'.repeat(64)},f.device.token)).status,409);const r=await f.propose();assert.equal(r.status,200);
assert.equal((await f.api('/api/device/library/apply',{proposalId:r.body.proposalId,policyVersion:1,expectedRevision:1,snapshotHash:f.request.snapshotHash,mode:'automatic'},f.device.token)).status,403);
p.expiresAt=new Date(Date.now()-1000).toISOString();f.store.put('policy',p);assert.equal((await f.propose()).status,403);
});
test('malicious model output and protected metadata rejected before proposal or transmission',async t=>{
const f=await fixture(t);for(const bad of [{...plan,bookmarks:[{id:'managed',folder:'Work'}]},{...plan,delete:['personal']},{...plan,tabs:[{...plan.tabs[0],url:'https://evil.test'}]}]){f.setOutput(JSON.stringify(bad));assert.notEqual((await f.propose()).status,200);assert.equal(f.store.personalBookmarks(f.device.id).revision,1);}
const before=f.calls(),p=f.store.get('policy');p.dlp.rules.push({id:'confidential-title',label:'Protected title',kind:'literal',value:bookmark.title,action:'block'});f.store.put('policy',p);assert.equal((await f.propose()).status,403);assert.equal(f.calls(),before);
});
test('stale bookmark revision, policy generation and managed collisions cannot apply or partially change library',async t=>{
const f=await fixture(t),r=await f.propose();assert.equal(r.status,200);const apply={proposalId:r.body.proposalId,policyVersion:1,expectedRevision:1,snapshotHash:f.request.snapshotHash};
assert.equal((await f.api('/api/device/library/apply',{...apply,snapshotHash:'0'.repeat(64)},f.device.token)).status,409);
f.store.db.prepare('UPDATE bookmarks SET revision=2 WHERE device_id=?').run(f.device.id);assert.equal((await f.api('/api/device/library/apply',apply,f.device.token)).status,409);assert.equal(f.store.personalBookmarks(f.device.id).items[0].folder,'');
f.store.db.prepare('UPDATE bookmarks SET revision=1 WHERE device_id=?').run(f.device.id);const p=f.store.get('policy');p.version=2;f.store.put('policy',p);assert.equal((await f.api('/api/device/library/apply',{...apply,policyVersion:2},f.device.token)).status,409);
p.version=1;p.browser.bookmarks.managed=[bookmark];f.store.put('policy',p);assert.equal((await f.api('/api/device/library/apply',apply,f.device.token)).status,403);assert.equal(f.store.personalBookmarks(f.device.id).revision,1);
});
test('automatic organization is separately enabled and existing proposals cannot survive a remote disable',async t=>{
const f=await fixture(t),p=f.store.get('policy');p.library.aiAutoApplyEnabled=true;f.store.put('policy',p);const r=await f.propose();assert.equal(r.status,200);
const body={proposalId:r.body.proposalId,policyVersion:1,expectedRevision:1,snapshotHash:f.request.snapshotHash,mode:'automatic'};
p.library.aiSuggestionsEnabled=false;f.store.put('policy',p);assert.equal((await f.api('/api/device/library/apply',body,f.device.token)).status,403);assert.equal(f.store.personalBookmarks(f.device.id).revision,1);
p.library.aiSuggestionsEnabled=true;f.store.put('policy',p);const applied=await f.api('/api/device/library/apply',body,f.device.token);assert.equal(applied.status,200);assert.equal(applied.body.revision,2);
});
test('model output DLP and pending proposal bounds are enforced without storing rejected model data',async t=>{
const f=await fixture(t),p=f.store.get('policy');p.dlp.rules.push({id:'protected-output',label:'Protected',kind:'literal',value:'Projects',action:'block'});f.store.put('policy',p);assert.equal((await f.propose()).status,403);assert.equal(f.store.personalBookmarks(f.device.id).revision,1);
p.dlp.rules=[];f.store.put('policy',p);for(let n=0;n<3;n++)assert.equal((await f.propose()).status,200);const before=f.calls();assert.equal((await f.propose()).status,409);assert.equal(f.calls(),before);
});
test('concurrent proposal requests are bounded and policy changes during provider execution invalidate results',async t=>{
const f=await fixture(t);let release;f.setGate(new Promise(r=>{release=r;}));const pending=f.propose();
for(let n=0;n<100&&f.calls()===0;n++)await new Promise(r=>setTimeout(r,10));assert.equal(f.calls(),1);
assert.equal((await f.propose()).status,409);assert.equal(f.calls(),1);
const p=f.store.get('policy');p.version++;f.store.put('policy',p);release();assert.equal((await pending).status,409);assert.equal(f.store.personalBookmarks(f.device.id).revision,1);
});
+28
View File
@@ -0,0 +1,28 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {mkdtempSync,rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {randomUUID} from 'node:crypto';
import {openStore,digest} from '../apps/server/store.mjs';
import {createApp} from '../apps/server/app.mjs';
import {toECS,toCEF} from '../packages/core/siem.mjs';
test('central ingestion strips legacy upload body hashes and retains safe request facts',async t=>{
const dir=mkdtempSync(join(tmpdir(),'safetynet-request-audit-')),store=openStore(dir),credential='synthetic-device-request-audit-token';
store.db.prepare('INSERT INTO devices(id,name,token_hash) VALUES(?,?,?)').run('device','Synthetic',digest(credential));
const server=createApp(store,{publicUrl:'http://127.0.0.1',adminToken:'synthetic-administrator-token-123456789',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();assert.ok(dir.startsWith(join(tmpdir(),'safetynet-request-audit-')));rmSync(dir,{recursive:true,force:true});});
const send=body=>fetch(`http://127.0.0.1:${server.address().port}/api/device/events`,{method:'POST',headers:{authorization:`Bearer ${credential}`,'content-type':'application/json'},body:JSON.stringify({eventId:randomUUID(),policyVersion:1,...body})});
const hash=digest('synthetic-low-entropy-password');let response=await send({kind:'upload.authorized',file:{bytes:32,detectedType:'text',sha256:hash}});assert.equal(response.status,200);
let row=store.db.prepare('SELECT * FROM events').get(),metadata=JSON.parse(row.metadata);assert.equal(metadata.file,undefined);assert.deepEqual(metadata.request,{bytes:32,detectedType:'text'});assert.equal(row.metadata.includes(hash),false);
response=await send({kind:'upload.block',request:{bytes:50,detectedType:'unknown'}});assert.equal(response.status,200);
response=await send({kind:'clipboard.copy',request:{bytes:50,detectedType:'text'}});assert.equal(response.status,400);
response=await send({kind:'upload.authorized',request:{bytes:50,detectedType:'text',sha256:hash}});assert.equal(response.status,400);
response=await send({kind:'download.scan',file:{bytes:32,detectedType:'text',sha256:hash}});assert.equal(response.status,200);row=store.db.prepare("SELECT * FROM events WHERE kind='download.scan'").get();assert.equal(JSON.parse(row.metadata).file.sha256,hash);assert.equal(store.auditIntegrity.verify().valid,true);
});
test('SIEM suppresses historical upload hashes while preserving transfer amounts and download hashes',()=>{
const hash=digest('synthetic-password'),legacy={id:1,time:new Date().toISOString(),kind:'upload.authorized',metadata:{file:{bytes:18,detectedType:'text',sha256:hash}}};
assert.equal(toECS(legacy).file,undefined);assert.deepEqual(toECS(legacy).safetynet.request,{bytes:18,detected_type:'text'});assert.equal(toCEF(legacy).includes(hash),false);
const current={...legacy,metadata:{request:{bytes:18,detectedType:'text'}}};assert.deepEqual(toECS(current).safetynet.request,{bytes:18,detected_type:'text'});assert.match(toCEF(current),/cn3=18/);assert.equal(JSON.stringify(toECS(current)).includes(hash),false);
legacy.kind='download.scan';assert.equal(toECS(legacy).file.hash.sha256,hash);
});
+30
View File
@@ -0,0 +1,30 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import vm from 'node:vm';
import {readFileSync} from 'node:fs';
import {createRequire} from 'node:module';
import {defaultPolicy} from '../packages/core/policy.mjs';
const require=createRequire(import.meta.url),source=readFileSync(new URL('../apps/browser/main.cjs',import.meta.url),'utf8');
const hook=source.slice(source.indexOf(" browserSession.webRequest.onBeforeRequest("),source.indexOf(" browserSession.webRequest.onBeforeSendHeaders("));
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,
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(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()};
}
const settle=async()=>{for(let n=0;n<8;n++)await Promise.resolve();};
test('native request deadline records a fixed reason and never authorizes a late result',async()=>{
const f=fixture();await settle();f.timers[0]();await settle();assert.equal(f.callbacks.length,1);assert.equal(f.callbacks[0].cancel,true);
assert.deepEqual(f.events,[['navigation.block','request-inspection-timeout','example.test']]);f.complete();await settle();f.timers[0]();await settle();
assert.equal(f.callbacks.length,1);assert.equal(f.events.length,1);
});
test('a completed native request cannot subsequently produce a timeout event',async()=>{
const f=fixture();await settle();f.complete();await settle();assert.equal(f.callbacks.length,1);assert.equal(f.callbacks[0].cancel,false);
f.timers[0]();await settle();assert.equal(f.callbacks.length,1);assert.equal(f.events.length,0);
});
+26
View File
@@ -0,0 +1,26 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
const require=createRequire(import.meta.url),{requestSourceCurrent}=require('../apps/browser/request-source.cjs'),{createTabRegistry}=require('../apps/browser/tab-registry.cjs');
function fixture(){
let url='https://old.example/',destroyed=false;
const contents={id:7,isDestroyed:()=>destroyed,getURL:()=>url},registry=createTabRegistry({createView:()=>({webContents:contents,setVisible(){}})}),source=registry.create();
source.guarded=true;registry.resume(1);const token=registry.context(source.id,{foreground:false});
const input={details:{webContentsId:7,resourceType:'mainFrame',method:'GET'},source,token,registry,currentDocument:t=>registry.current(t)};
return {input,registry,source,setURL:value=>url=value,destroy:()=>destroyed=true};
}
test('top-level empty GET retains native ownership across the old document revision and URL transition',()=>{
const f=fixture();f.registry.navigation(f.source.id);f.setURL('https://new.example/');
assert.equal(f.registry.current(f.input.token),false);assert.equal(requestSourceCurrent(f.input),true);
});
test('POST, XHR, worker and body-bearing requests retain exact document generation checks',()=>{
for(const change of [{method:'POST'},{resourceType:'xhr'},{resourceType:'other'},{uploadData:[{bytes:Buffer.from('data')}]},{method:'HEAD'}]){
const f=fixture();Object.assign(f.input.details,change);f.registry.navigation(f.source.id);assert.equal(requestSourceCurrent(f.input),false);
}
});
test('top-level GET cannot substitute, remove, destroy or unguard its native owner',()=>{
const f=fixture();f.source.guarded=false;assert.equal(requestSourceCurrent(f.input),false);f.source.guarded=true;f.destroy();assert.equal(requestSourceCurrent(f.input),false);
const g=fixture();g.input.registry={fromContents:()=>({view:g.source.view,guarded:true})};assert.equal(requestSourceCurrent(g.input),false);
const h=fixture();h.input.source={...h.source,view:{webContents:{isDestroyed:()=>false}}};assert.equal(requestSourceCurrent(h.input),false);
const missing=fixture();missing.input.token=null;assert.equal(requestSourceCurrent(missing.input),false);
});
+49
View File
@@ -0,0 +1,49 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import facts from '../packages/core/request-transfer-facts.cjs';
import transfers from '../packages/core/file-transfer.cjs';
for(const [name,body] of [
['form password',Buffer.from('username=alice&password=short-secret')],
['JSON token',Buffer.from('{"token":"short-secret"}')],
['binary credential envelope',Buffer.concat([Buffer.from('89504e470d0a1a0a','hex'),Buffer.from('password=short-secret')])],
['multipart secret',Buffer.from('--boundary\r\nContent-Disposition: form-data; name="password"\r\n\r\nshort-secret\r\n--boundary--')]
])test(`request audit excludes reversible fingerprint for ${name}`,()=>{
const transfer=transfers.inspectFileTransfer(body,{},'upload');
const result=facts.requestFactsForTransfer(transfer);
assert.deepEqual(Object.keys(result).sort(),['bytes','detectedType']);
assert.equal(result.bytes,body.length);
assert.equal(JSON.stringify(result).includes(transfer.sha256),false);
assert.equal(JSON.stringify(result).includes('short-secret'),false);
assert.throws(()=>facts.requestTransferFacts({...result,sha256:transfer.sha256}));
});
test('request facts reject arbitrary content, unknown types and invalid sizes',()=>{
for(const value of [{bytes:-1,detectedType:'text'},{bytes:1,detectedType:'password'},{bytes:1,detectedType:'text',content:'secret'}])assert.throws(()=>facts.requestTransferFacts(value));
});
test('privileged request gate routes upload audit through request facts without modifying file inspection',()=>{
const source=readFileSync(new URL('../apps/browser/main.cjs',import.meta.url),'utf8');
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\)/);
assert.equal(source.includes('sha256:transfer.sha256'),false);
});
test('offline legacy upload queue is scrubbed before replay without changing delivery identity or downloads',()=>{
const download={eventId:'download-id',kind:'download.released',file:{bytes:9,detectedType:'text',sha256:'b'.repeat(64)}};
const legacy={eventId:'upload-id',kind:'upload.authorized',host:'example.test',policyVersion:7,file:{bytes:9,detectedType:'text',sha256:'a'.repeat(64)}};
const migrated=facts.migrateQueuedRequestFacts([legacy,download]);
assert.deepEqual(migrated[0],{eventId:'upload-id',kind:'upload.authorized',host:'example.test',policyVersion:7,request:{bytes:9,detectedType:'text'}});
assert.equal(migrated[1],download);
assert.equal(legacy.file.sha256,'a'.repeat(64));
assert.deepEqual(facts.migrateQueuedRequestFacts(migrated),migrated);
const invalid=facts.migrateQueuedRequestFacts([{kind:'upload.block',eventId:'blocked-id',file:{bytes:-1,detectedType:'secret',sha256:'secret'}}]);
assert.deepEqual(invalid,[{kind:'upload.block',eventId:'blocked-id'}]);
assert.throws(()=>facts.migrateQueuedRequestFacts({}));
const source=readFileSync(new URL('../apps/browser/main.cjs',import.meta.url),'utf8');
assert.match(source,/migrateQueuedRequestFacts\(JSON.parse\(safeStorage.decryptString\(fs.readFileSync\(queuePath\)\)\)\);persistAudit\(\)/);
});
+1 -1
View File
@@ -85,7 +85,7 @@ test('local Ollama needs no API key, blocks secrets before provider, and Sigma a
p.dlp.rules=[{id:'company-roadmap',label:'Roadmap',kind:'literal',value:'confidential roadmap',action:'block'}];store.put('policy',p);assert.equal((await send('/api/device/ai',{prompt:'confidential roadmap'})).status,403);assert.equal(calls,1);
p.ai.allowConfidentialLocal=true;store.put('policy',p);assert.equal((await send('/api/device/ai',{prompt:'confidential roadmap'})).status,200);assert.equal(calls,2);assert.equal((await send('/api/device/ai',{prompt:'confidential roadmap -----BEGIN PRIVATE KEY-----'})).status,403);assert.equal(calls,2);
const allowed=store.db.prepare("SELECT metadata FROM events WHERE kind='ai.decision' ORDER BY id DESC").all().map(v=>JSON.parse(v.metadata)).find(v=>v.classification==='local-only');assert.equal(allowed.ruleId,'company-roadmap');assert.equal(allowed.decision,'allow');
config.aiBaseUrl='http://192.0.2.1';assert.equal((await send('/api/device/ai',{prompt:'confidential roadmap'})).status,500);assert.equal(calls,2);
config.aiBaseUrl='http://192.0.2.1';assert.equal((await send('/api/device/ai',{prompt:'confidential roadmap'})).status,403);assert.equal(calls,2);
assert.equal((await send('/api/device/events',{eventId:randomUUID(),kind:'agent.denied',ruleId:'agent:read:blocked',policyVersion:1})).status,200);assert.equal(store.db.prepare('SELECT count(*) AS n FROM alerts').get().n,1);
for(const extra of [{username:'alice'},{password:'private'},{credentialId:'identifier'},{host:'login.example'},{ruleId:'credential-id'}])assert.equal((await send('/api/device/events',{eventId:randomUUID(),kind:'vault.fill-requested',policyVersion:1,...extra})).status,400);
for(const kind of ['vault.created','vault.unlocked','vault.locked','vault.saved','vault.deleted','vault.filled','vault.fill-requested'])assert.equal((await send('/api/device/events',{eventId:randomUUID(),kind,policyVersion:1})).status,200);
+49
View File
@@ -38,3 +38,52 @@ test('denied agent actions and correlated Sigma alerts export durably without ra
exporter=startSIEM(store,directory,{});await exporter.flush();const rows=readFileSync(join(directory,'security.ndjson'),'utf8').trim().split('\n').map(JSON.parse);assert.equal(rows.length,2);assert.equal(rows[1].event.action,'sigma.alert');assert.equal(rows[1].safetynet.source_event_id,rows[0].event.id);assert.equal(JSON.stringify(rows).includes('excluded raw content'),false);
store.db.exec('BEGIN IMMEDIATE');store.event('device','agent.denied',{ruleId:'agent:read:blocked'});store.db.exec('ROLLBACK');assert.equal(store.db.prepare('SELECT count(*) AS n FROM events').get().n,2);assert.equal(store.db.prepare('SELECT count(*) AS n FROM alerts').get().n,1);
});
test('administrator forwarding toggle stops HTTPS export, preserves audit, resumes cursor after restart',async t=>{
const {createServer}=await import('node:https');
const certPath=new URL('./fixtures/icap/cert.pem',import.meta.url),keyPath=new URL('./fixtures/icap/key.pem',import.meta.url);
const batches=[];let exporter;
const receiver=createServer({cert:readFileSync(certPath),key:readFileSync(keyPath)},(req,res)=>{let body='';req.on('data',chunk=>body+=chunk);req.on('end',()=>{batches.push(body.trim().split('\n').map(JSON.parse));res.end('{}');});});
await new Promise(resolve=>receiver.listen(0,'127.0.0.1',resolve));
const directory=mkdtempSync(join(tmpdir(),'safetynet-siem-toggle-')),store=openStore(directory);
t.after(async()=>{exporter?.stop();await new Promise(resolve=>receiver.close(resolve));store.db.close();rmSync(directory,{recursive:true,force:true});});
const env={SIEM_HTTP_URL:`https://127.0.0.1:${receiver.address().port}/events`,SIEM_CA_FILE:certPath};
store.event('device','download.scan',{ruleId:'synthetic'});exporter=startSIEM(store,directory,env);await exporter.flush();assert.equal(exporter.status().cursor,1);
const disabled=exporter.setEnabled({enabled:false,expectedRevision:1},'admin');assert.equal(disabled.enabled,false);
store.event('device','clipboard.copy',{});await exporter.flush();assert.equal(batches.length,1);assert.equal(exporter.status().cursor,1);assert.equal(exporter.status().pending,2);
assert.throws(()=>exporter.setEnabled({enabled:true,expectedRevision:1},'admin'),/revision conflict/);
exporter.stop();exporter=startSIEM(store,directory,env);await exporter.flush();assert.equal(exporter.status().enabled,false);assert.equal(batches.length,1);
exporter.setEnabled({enabled:true,expectedRevision:2},'admin');await exporter.flush();assert.equal(batches.length,2);assert.equal(exporter.status().pending,0);
assert.deepEqual(batches.flat().map(e=>e.event.id),[1,2,3,4].map(id=>`${store.get('installationId')}:${id}`));
assert.equal(store.db.prepare("SELECT count(*) AS n FROM events WHERE kind='siem.forwarding-changed'").get().n,2);
assert.equal(JSON.stringify(exporter.status()).includes('/events'),false);
});
test('disabling forwarding aborts an unacknowledged HTTPS request without advancing cursor',async t=>{
const {createServer}=await import('node:https');let arrived,closed;const arrival=new Promise(resolve=>arrived=resolve),closure=new Promise(resolve=>closed=resolve);
const receiver=createServer({cert:readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url)),key:readFileSync(new URL('./fixtures/icap/key.pem',import.meta.url))},(req,res)=>{req.resume();res.on('close',closed);req.on('end',arrived);});
await new Promise(resolve=>receiver.listen(0,'127.0.0.1',resolve));
const directory=mkdtempSync(join(tmpdir(),'safetynet-siem-abort-')),store=openStore(directory);let exporter;
t.after(async()=>{exporter?.stop();receiver.closeAllConnections();await new Promise(resolve=>receiver.close(resolve));store.db.close();rmSync(directory,{recursive:true,force:true});});
store.event('device','download.scan',{});exporter=startSIEM(store,directory,{SIEM_HTTP_URL:`https://127.0.0.1:${receiver.address().port}`,SIEM_CA_FILE:new URL('./fixtures/icap/cert.pem',import.meta.url)});
await arrival;const active=exporter.flush();exporter.setEnabled({enabled:false,expectedRevision:1},'admin');await active;await closure;
assert.equal(exporter.status().cursor,0);assert.equal(exporter.status().failures,0);assert.equal(exporter.status().pending,2);
});
test('SIEM management API enforces authentication, administrator role, origin, strict settings and CAS',async t=>{
const {createApp}=await import('../apps/server/app.mjs');const {digest}=await import('../apps/server/store.mjs');
const directory=mkdtempSync(join(tmpdir(),'safetynet-siem-api-')),store=openStore(directory),exporter=startSIEM(store,directory,{SIEM_ENABLED:'false'});
const adminToken='synthetic-admin-credential-012345678901234567890';const config={publicUrl:'http://127.0.0.1:4310',adminToken,secure:false,siemControl:exporter};
const server=createApp(store,config).listen(0,'127.0.0.1');await new Promise(resolve=>server.on('listening',resolve));
t.after(async()=>{exporter.stop();await new Promise(resolve=>server.close(resolve));store.db.close();rmSync(directory,{recursive:true,force:true});});
const base=`http://127.0.0.1:${server.address().port}`;
async function api(method,body,headers={authorization:`Bearer ${adminToken}`}){const r=await fetch(base+'/api/admin/siem',{method,headers:{...headers,...(body?{'content-type':'application/json'}:{})},body:body?JSON.stringify(body):undefined});return {status:r.status,body:await r.json()};}
assert.equal((await api('GET',null,{})).status,401);const initial=await api('GET');assert.equal(initial.status,200);assert.equal(initial.body.enabled,false);
for(const role of ['operator','auditor']){const session=`synthetic-${role}-session-012345678901234567890`;store.db.prepare('INSERT INTO sessions VALUES(?,?,?)').run(digest(session),Date.now()+60000,JSON.stringify({sub:role,role}));const headers={cookie:`sn_session=${session}`,origin:config.publicUrl};assert.equal((await api('GET',null,headers)).status,200);assert.equal((await api('PUT',{enabled:true,expectedRevision:1},headers)).status,403);}
assert.equal((await api('PUT',{enabled:true,expectedRevision:1},{authorization:`Bearer ${adminToken}`,origin:'https://untrusted.example'})).status,403);
assert.equal((await api('PUT',{enabled:true,expectedRevision:1,endpoint:'https://untrusted.example'})).status,400);
const enabled=await api('PUT',{enabled:true,expectedRevision:1});assert.equal(enabled.status,200);assert.equal(enabled.body.revision,2);
assert.equal((await api('PUT',{enabled:false,expectedRevision:1})).status,409);assert.equal((await api('GET')).body.enabled,true);
assert.equal(store.db.prepare("SELECT count(*) AS n FROM events WHERE kind='siem.forwarding-changed'").get().n,1);
config.siemControl=null;assert.equal((await api('GET')).status,503);assert.equal((await api('PUT',{enabled:false,expectedRevision:2})).status,503);
});
+39
View File
@@ -0,0 +1,39 @@
import unittest
import sys
from pathlib import Path
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'apps'/'inspector'))
from stage_timings import collect, stage, metadata, validated
class StageTimingsTests(unittest.TestCase):
def test_nested_debug(self):
with collect({'diagnostics':{'enabled':True,'mode':'debug'}}) as state:
with stage('inspect'):
with stage('parse'): pass
value=metadata(state)
self.assertIs(validated(value),value)
self.assertEqual(value['records'][1]['parentId'],1)
self.assertGreaterEqual(value['records'][0]['durationMs'],value['records'][1]['durationMs'])
def test_off_and_operational(self):
for policy in ({},{'diagnostics':{'enabled':False,'mode':'debug'}},{'diagnostics':{'enabled':True,'mode':'operational'}}):
with collect(policy) as state:
with stage('inspect'): pass
self.assertIsNone(metadata(state))
def test_error_preserved_and_bounded(self):
with collect({'diagnostics':{'enabled':True,'mode':'debug'}}) as state:
for _ in range(100):
with stage('parse'): pass
self.assertEqual(len(metadata(state)['records']),64)
with collect({'diagnostics':{'enabled':True,'mode':'debug'}}) as state:
with self.assertRaisesRegex(ValueError,'secret'):
with stage('inspect'): raise ValueError('secret')
self.assertEqual(state['records'][0]['status'],'error')
self.assertNotIn('secret',str(metadata(state)))
def test_invalid_metadata(self):
for value in ({}, {'baseUnixMs':1,'records':[{'content':'secret'}]}, {'baseUnixMs':True,'records':[]}):
self.assertIsNone(validated(value))
with collect({'diagnostics':{'enabled':True,'mode':'debug'}}) as state:
with stage('parse'): pass
value=metadata(state); value['records'][0]['durationMs']=float('nan')
self.assertIsNone(validated(value))
if __name__=='__main__': unittest.main()
+13 -2
View File
@@ -1,14 +1,25 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createServer } from 'node:http';
import { startTelemetry,tracer,recordSecurityEvent } from '../apps/server/telemetry.mjs';
import express from 'express';
import { startTelemetry,tracer,recordSecurityEvent,installRequestTelemetry,traceOperation,importInspectionStages } from '../apps/server/telemetry.mjs';
test('OpenTelemetry exports traces, metrics and safe logs through OTLP HTTP',{timeout:20000},async t=>{
const requests=[];
const server=createServer((req,res)=>{const chunks=[];req.on('data',c=>chunks.push(c));req.on('end',()=>{requests.push({url:req.url,data:Buffer.concat(chunks)});res.setHeader('content-type','application/json');res.end('{}');});}).listen(0,'127.0.0.1');
await new Promise(r=>server.on('listening',r));process.env.OTEL_EXPORTER_OTLP_ENDPOINT=`http://127.0.0.1:${server.address().port}`;
t.after(async()=>{delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;server.closeAllConnections();await new Promise(r=>server.close(r));});
const sdk=startTelemetry();t.after(()=>sdk.shutdown());tracer.startActiveSpan('inspection.test',span=>{span.setAttribute('dlp.decision','block');span.end();});
const app=express();installRequestTelemetry(app);app.post('/api/inspect/:id',async(req,res)=>{await traceOperation('ai.inspect.input',{bytes:17,prompt:'HTTP_SECRET_MUST_NOT_APPEAR'},async()=>new Promise(r=>setTimeout(r,15)));importInspectionStages({baseUnixMs:Date.now()-5,records:[{id:1,parentId:null,stage:'inspect',startOffsetMs:0,durationMs:4,status:'ok'},{id:2,parentId:1,stage:'parse',startOffsetMs:1,durationMs:2,status:'ok'}]},{diagnostics:{enabled:true,mode:'debug'}});res.json({ok:true});});
const application=app.listen(0,'127.0.0.1');await new Promise(r=>application.once('listening',r));t.after(async()=>{application.closeAllConnections();await new Promise(r=>application.close(r));});
const remoteTrace='1234567890abcdef1234567890abcdef',remoteParent='abcdef1234567890';
await fetch(`http://127.0.0.1:${application.address().port}/api/inspect/HTTP_SECRET_MUST_NOT_APPEAR?token=HTTP_SECRET_MUST_NOT_APPEAR`,{method:'POST',headers:{traceparent:`00-${remoteTrace}-${remoteParent}-01`,authorization:'Bearer HTTP_SECRET_MUST_NOT_APPEAR'},body:'HTTP_SECRET_MUST_NOT_APPEAR'});
await fetch(`http://127.0.0.1:${application.address().port}/api/inspect/invalid`,{method:'POST',headers:{traceparent:'00-'+ '0'.repeat(32)+'-'+ '0'.repeat(16)+'-01'}});
recordSecurityEvent('dlp.block',{ruleId:'test',policyVersion:1,prompt:'THIS_SECRET_MUST_NOT_APPEAR'},'event-1');await sdk.shutdown();
for(const path of ['/v1/traces','/v1/metrics','/v1/logs'])assert.ok(requests.some(r=>r.url===path),`Missing ${path}`);
assert.equal(requests.some(r=>r.data.includes(Buffer.from('THIS_SECRET_MUST_NOT_APPEAR'))),false);
for(const secret of ['THIS_SECRET_MUST_NOT_APPEAR','HTTP_SECRET_MUST_NOT_APPEAR'])assert.equal(requests.some(r=>r.data.includes(Buffer.from(secret))),false);
const spans=requests.filter(r=>r.url==='/v1/traces').flatMap(r=>JSON.parse(r.data).resourceSpans||[]).flatMap(r=>r.scopeSpans||[]).flatMap(r=>r.spans||[]);
const request=spans.find(s=>s.name==='server.request'&&s.traceId===remoteTrace);assert.ok(request);assert.equal(request.parentSpanId,remoteParent);assert.ok(request.attributes.some(a=>a.key==='http.route'&&a.value.stringValue==='/api/inspect/:id'));
const stage=spans.find(s=>s.name==='ai.inspect.input'&&s.traceId===remoteTrace);assert.ok(stage);assert.equal(stage.parentSpanId,request.spanId);assert.ok(stage.attributes.some(a=>a.key==='safetynet.duration.ms'&&a.value.doubleValue>=10));
const internal=spans.find(s=>s.name==='inspector.inspect'&&s.traceId===remoteTrace),parse=spans.find(s=>s.name==='inspector.parse'&&s.traceId===remoteTrace);assert.ok(internal&&parse);assert.equal(internal.parentSpanId,request.spanId);assert.equal(parse.parentSpanId,internal.spanId);
assert.ok(spans.some(s=>s.name==='server.request'&&s.traceId!==remoteTrace&&!s.parentSpanId),'Invalid remote context must start a new root trace');
});
+2
View File
@@ -11,3 +11,5 @@ test('security file facts carry exact bytes and detected type without arbitrary
e.metadata.file={bytes:123,detectedType:'password=secret',sha256:'a'.repeat(64)};
assert.equal(toECS(e).file,undefined);assert.equal(toCEF(e).includes('password=secret'),false);
});
test('OLE Office magic facts remain representable in durable SIEM records',()=>{const file=facts.transferFacts({bytes:512,detectedType:'ole',sha256:'b'.repeat(64)});assert.equal(toECS({id:9,time:new Date().toISOString(),kind:'download.scan',metadata:{file}}).file.type,'ole');});
+55
View File
@@ -0,0 +1,55 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import {spawn} from 'node:child_process';
import {once} from 'node:events';
import {createRequire} from 'node:module';
const require=createRequire(import.meta.url);
const {acquireUploadLease}=require('../apps/browser/upload-lease.cjs');
const options={pythonPath:path.resolve('.runtime/windows/python/python.exe'),helperPath:path.resolve('apps/inspector/upload_lease.py')};
test('Windows immutable upload snapshot locks write, rename and hard-link overwrite until explicit release',{skip:process.platform!=='win32'},async()=>{
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'safetynet-upload-lease-'));
const file=path.join(dir,'safe.bin'),alias=path.join(dir,'alias.bin');
fs.writeFileSync(file,Buffer.from('synthetic-upload'));fs.linkSync(file,alias);
let lease;
try{
lease=await acquireUploadLease({...options,paths:[file],maxBytes:4096});
assert.equal(lease.files[0].bytes.toString(),'synthetic-upload');assert.equal(lease.isAlive(),true);
assert.throws(()=>fs.writeFileSync(file,'changed'));assert.throws(()=>fs.writeFileSync(alias,'changed'));assert.throws(()=>fs.renameSync(file,path.join(dir,'moved.bin')));
await lease.release();lease=null;fs.writeFileSync(file,'released');assert.equal(fs.readFileSync(alias,'utf8'),'released');
}finally{if(lease)await lease.release();assert.ok(path.resolve(dir).startsWith(path.resolve(os.tmpdir())+path.sep+'safetynet-upload-lease-'));fs.rmSync(dir,{recursive:true,force:true});}
});
test('broker refuses UNC, device, ADS and noncanonical upload paths before spawn',async()=>{
for(const value of ['\\\\server\\share\\a','\\\\?\\C:\\file','C:\\file:stream','C:\\folder\\..\\file'])await assert.rejects(acquireUploadLease({...options,paths:[value]}),{code:'upload-lease-refused'});
});
test('parent handle copies survive helper death and trusted explicit cleanup releases them',{skip:process.platform!=='win32'},async()=>{
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'safetynet-upload-lease-death-')),file=path.join(dir,'safe.bin');fs.writeFileSync(file,'snapshot');
const start=()=>spawn(options.pythonPath,['-I','-B',options.helperPath],{windowsHide:true,stdio:['pipe','pipe','pipe'],env:{SystemRoot:process.env.SystemRoot,WINDIR:process.env.WINDIR}});
const child=start();let text='';
const ready=new Promise((resolve,reject)=>{child.stdout.on('data',c=>{text+=c;if(text.includes('\n'))resolve(JSON.parse(text.split('\n')[0]));});child.on('error',reject);});
child.stdin.write(JSON.stringify({version:'safetynet-upload-lease-v1',paths:[file],maxBytes:1024,parentPid:process.pid})+'\n');
const receipt=await ready;assert.equal(receipt.ok,true);const exited=once(child,'exit');child.kill();await exited;
assert.throws(()=>fs.writeFileSync(file,'changed'));assert.throws(()=>fs.renameSync(file,path.join(dir,'moved.bin')));
const cleanup=start();let output='';cleanup.stdout.on('data',c=>output+=c);const cleaned=once(cleanup,'exit');cleanup.stdin.end(JSON.stringify({version:'safetynet-upload-lease-v1',parentPid:process.pid,closeHandles:receipt.handles})+'\n');
const [code]=await cleaned;assert.equal(code,0);assert.equal(JSON.parse(output).released,true);fs.writeFileSync(file,'released');
assert.ok(path.resolve(dir).startsWith(path.resolve(os.tmpdir())+path.sep+'safetynet-upload-lease-death-'));fs.rmSync(dir,{recursive:true,force:true});
});
test('existing writable memory mapping is refused even after creator file descriptor closes',{skip:process.platform!=='win32'},async()=>{
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'safetynet-upload-lease-mapping-')),file=path.join(dir,'safe.bin');fs.writeFileSync(file,'snapshot');
const code="import sys,json,mmap\np=json.loads(sys.stdin.readline())\nf=open(p,'r+b')\nm=mmap.mmap(f.fileno(),0,access=mmap.ACCESS_WRITE)\nf.close()\nprint('ready',flush=True)\nsys.stdin.readline()\nm.close()";
const mapper=spawn(options.pythonPath,['-I','-B','-c',code],{windowsHide:true,stdio:['pipe','pipe','pipe']});
const ready=once(mapper.stdout,'data');mapper.stdin.write(JSON.stringify(file)+'\n');await ready;
try{await assert.rejects(acquireUploadLease({...options,paths:[file],maxBytes:1024}),{code:'upload-lease-refused'});}finally{const exited=once(mapper,'exit');mapper.stdin.end('release\n');await exited;assert.ok(path.resolve(dir).startsWith(path.resolve(os.tmpdir())+path.sep+'safetynet-upload-lease-mapping-'));fs.rmSync(dir,{recursive:true,force:true});}
const lease=await acquireUploadLease({...options,paths:[path.resolve('README.md')],maxBytes:1048576});await lease.release();
});
test('junction ancestor, oversize and preexisting writable descriptor are refused without disabling later valid leases',{skip:process.platform!=='win32'},async()=>{
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'safetynet-upload-lease-negative-')),target=path.join(dir,'target'),junction=path.join(dir,'junction');fs.mkdirSync(target);const file=path.join(target,'safe.bin');fs.writeFileSync(file,'snapshot');fs.symlinkSync(target,junction,'junction');
try{
await assert.rejects(acquireUploadLease({...options,paths:[path.join(junction,'safe.bin')],maxBytes:1024}),{code:'upload-lease-refused'});
await assert.rejects(acquireUploadLease({...options,paths:[file],maxBytes:1}),{code:'upload-lease-refused'});
const fd=fs.openSync(file,'r+');try{await assert.rejects(acquireUploadLease({...options,paths:[file],maxBytes:1024}),{code:'upload-lease-refused'});}finally{fs.closeSync(fd);}
const lease=await acquireUploadLease({...options,paths:[file],maxBytes:1024});await lease.release();await assert.rejects(lease.release(),{code:'upload-lease-refused'});
}finally{fs.unlinkSync(junction);assert.ok(path.resolve(dir).startsWith(path.resolve(os.tmpdir())+path.sep+'safetynet-upload-lease-negative-'));fs.rmSync(dir,{recursive:true,force:true});}
});