Files
safetynet/tests/policy-stage-diagnostics.test.mjs
T

61 lines
8.0 KiB
JavaScript

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 {createServer} from 'node:http';
import {generateKeyPairSync} from 'node:crypto';
import {defaultPolicy,signPolicy,verifyPolicy} from '../packages/core/policy.mjs';
import {signBookmarkSnapshot,verifyBookmarkSnapshot} from '../packages/core/bookmarks.mjs';
import {DiagnosticsSchema} from '../packages/core/diagnostics.mjs';
const require=createRequire(import.meta.url),{createDiagnostics}=require('../apps/browser/diagnostics.cjs');
const source=readFileSync(new URL('../apps/browser/main.cjs',import.meta.url),'utf8');
// Substitute only the dynamic module-loader boundary; execute the actual shipped
// application function and the real OTLP diagnostics helper.
const extracted=source.slice(source.indexOf('function applyPolicy(envelope)'),source.indexOf('async function sync()')).replace("import('../../packages/core/bookmarks.mjs')","Promise.resolve(bookmarkModule)");
async function fixture(t){
const payloads=[],server=createServer((req,res)=>{const chunks=[];req.on('data',v=>chunks.push(v));req.on('end',()=>{payloads.push(JSON.parse(Buffer.concat(chunks).toString()));res.setHeader('content-type','application/json');res.end('{}');});}).listen(0,'127.0.0.1');await new Promise(resolve=>server.once('listening',resolve));
const diagnostics=createDiagnostics(),config=DiagnosticsSchema.parse({enabled:true,mode:'debug',endpoint:`http://127.0.0.1:${server.address().port}`,sampleRate:1,debugSampleRate:1,flushIntervalMs:10000});
t.after(async()=>{await diagnostics.shutdown();server.closeAllConnections();await new Promise(resolve=>server.close(resolve));});diagnostics.configure(config,{policyVersion:1,deviceId:'00000000-0000-4000-8000-000000000001'});
const calls=[],next=defaultPolicy();next.diagnostics=config;const keys=generateKeyPairSync('ed25519'),privateKey=keys.privateKey.export({type:'pkcs8',format:'pem'}),publicKey=keys.publicKey.export({type:'spki',format:'pem'}),deviceId='00000000-0000-4000-8000-000000000001';
const envelope=()=>({...signPolicy(next,privateKey),bookmarks:signBookmarkSnapshot({deviceId,policyVersion:next.version,revision:0,expiresAt:next.expiresAt,items:[]},privateKey)});
const tracked=name=>async()=>{calls.push({name,enabled:diagnostics.status().enabled});};
const c=vm.createContext({require,diagnostics,diagnosticContext:extra=>({deviceId,policyVersion:c.policy?.version||0,...extra}),process:{env:{}},core:{verifyPolicy},assertPolicyAssignment:require('../apps/browser/policy-assignment.cjs').assertPolicyAssignment,credentialBroker:{revoke:()=>calls.push({name:'credential-revoke',enabled:diagnostics.status().enabled})},device:{id:deviceId,publicKey,minimumVersion:0},policy:next,personalBookmarks:{revision:0,items:[]},bookmarkSnapshotJSON:'',bookmarkModule:{verifyBookmarkSnapshot},saveDevice:()=>calls.push({name:'save',enabled:diagnostics.status().enabled}),diagnosticSpan:(name,attributes,fn)=>diagnostics.span(name,attributes,fn),configureDiagnostics:v=>{calls.push({name:'configure',enabled:v.enabled});diagnostics.configure(v,{deviceId,policyVersion:c.policy?.version||0});},reconcileAgentPolicy:tracked('agent'),enforceEvidencePolicy:()=>{},emit:()=>{},quarantine:()=>{},browserSession:{closeAllConnections:tracked('browser-close'),setProxy:tracked('browser-proxy'),clearHostResolverCache:tracked('browser-dns')},managementSession:{closeAllConnections:tracked('management-close'),setProxy:tracked('management-proxy'),clearHostResolverCache:tracked('management-dns')},app:{configureHostResolver:()=>calls.push({name:'resolver',enabled:diagnostics.status().enabled})},resolverOptions:v=>v,recentRequests:new Map(),tabs:{all:()=>[{security:{developerToolsGuard:{close:()=>{}}},view:{webContents:{closeDevTools:()=>{}}}}]},configureVisualPage:tracked('page'),audit:tracked('audit')});
c.localRelay={proxyRules:'127.0.0.1:45678',proxyBypassRules:'<-loopback>',invalidate:tracked('relay-invalidate')};
const originalSetProxy=c.browserSession.setProxy;c.browserSession.setProxy=async settings=>{assert.equal(settings.mode,'fixed_servers');assert.equal(settings.proxyRules,c.localRelay.proxyRules);assert.equal(settings.proxyBypassRules,'<-loopback>');await originalSetProxy(settings);};
vm.runInContext(extracted,c);return {c,next,envelope,calls,diagnostics,payloads,spans:()=>payloads.flatMap(v=>v.resourceSpans||[]).flatMap(v=>v.scopeSpans||[]).flatMap(v=>v.spans||[])};
}
test('actual unchanged policy exports correlated verification, bookmark and agent child stages',async t=>{
const f=await fixture(t);await f.c.applyPolicy(f.envelope());await f.diagnostics.flush();const spans=f.spans(),parent=spans.find(s=>!s.parentSpanId);assert.equal(parent.name,'browser.policy');
for(const operation of ['policy.verify','policy.bookmarks','policy.agent-reconcile']){const child=spans.find(s=>s.attributes.some(a=>a.key==='safetynet.operation'&&a.value.stringValue===operation));assert.ok(child,operation);assert.equal(child.traceId,parent.traceId);assert.equal(child.parentSpanId,parent.spanId);assert.ok(BigInt(child.endTimeUnixNano)>=BigInt(child.startTimeUnixNano));}
assert.equal(JSON.stringify(f.payloads).includes('fixture'),false);
});
test('new signed policy disables diagnostics throughout network transition and honors final disable',async t=>{
const f=await fixture(t);f.c.policy={...f.next,version:0};f.next.diagnostics={enabled:false};await f.c.applyPolicy(f.envelope());await f.diagnostics.flush();
for(const name of ['browser-close','browser-proxy','management-close','management-proxy','browser-dns','management-dns','credential-revoke','resolver','save','page','agent','audit'])assert.equal(f.calls.find(v=>v.name===name).enabled,false,name);
assert.deepEqual(f.calls.filter(v=>v.name==='configure').map(v=>v.enabled),[false,false]);assert.equal(f.diagnostics.status().enabled,false);assert.equal(f.spans().length,0,'Retired old generation must not export');
});
test('enabled new policy exports only post-configuration stages after protected network transition',async t=>{
const f=await fixture(t);f.c.policy={...f.next,version:0};await f.c.applyPolicy(f.envelope());await f.diagnostics.flush();
assert.equal(f.calls.find(v=>v.name==='browser-proxy').enabled,false);assert.equal(f.calls.find(v=>v.name==='page').enabled,true);
const operations=f.spans().map(s=>s.attributes.find(a=>a.key==='safetynet.operation')?.value.stringValue);assert.deepEqual(operations.sort(),['policy.page-configure','policy.agent-reconcile','policy.audit'].sort());
});
test('signed verification failure prevents bookmark, proxy and policy mutations',async t=>{
const f=await fixture(t);const tampered=f.envelope();tampered.payload+='A';await assert.rejects(f.c.applyPolicy(tampered),/signature rejected/);await f.diagnostics.flush();assert.equal(f.calls.length,0);assert.equal(f.c.policy.version,1);assert.ok(f.spans().some(s=>s.status.code===2));
});
test('verified policy-only update keeps identical telemetry authorization throughout the transition',async t=>{
const f=await fixture(t);f.c.policy=structuredClone(f.next);f.next.version=2;
await f.c.applyPolicy(f.envelope());await f.diagnostics.flush();
assert.ok(f.calls.filter(v=>v.name==='configure').every(v=>v.enabled===true));
for(const name of ['browser-close','browser-proxy','management-close','management-proxy','browser-dns','management-dns','credential-revoke','resolver','save','page','agent','audit'])assert.equal(f.calls.find(v=>v.name===name).enabled,true,name);
assert.equal(f.c.policy.version,2);assert.equal(f.diagnostics.status().lifetimeFailedExports,0);
});
test('expired previous policy cannot preserve telemetry authorization even with identical collector settings',async t=>{
const f=await fixture(t);f.c.policy={...structuredClone(f.next),expiresAt:new Date(Date.now()-1000).toISOString()};f.next.version=2;
await f.c.applyPolicy(f.envelope());await f.diagnostics.flush();
assert.equal(f.calls.find(v=>v.name==='browser-proxy').enabled,false);
assert.deepEqual(f.calls.filter(v=>v.name==='configure').map(v=>v.enabled),[false,true]);
});