10 lines
6.0 KiB
JavaScript
10 lines
6.0 KiB
JavaScript
import test from 'node:test';import assert from 'node:assert/strict';import {readFileSync} from 'node:fs';import {createRequire} from 'node:module';import vm from 'node:vm';import * as core from '../packages/core/policy.mjs';const require=createRequire(import.meta.url),source=readFileSync(new URL('../apps/browser/main.cjs',import.meta.url),'utf8');const wrapper=source.slice(source.indexOf('function inspectOutgoingText('),source.indexOf('function responsePolicyHeaders(')),headers=source.slice(source.indexOf('function responsePolicyHeaders('),source.indexOf('function hooks(){')),gate=source.slice(source.indexOf(" browserSession.webRequest.onBeforeRequest({urls:['<all_urls>']},(details,callback)=>{"),source.indexOf(" browserSession.webRequest.onBeforeSendHeaders({urls:['<all_urls>']},(details,callback)=>{"));
|
|
function fixture(){const policy=core.defaultPolicy();policy.security.visualPhishing.enabled=false;policy.dlp.semantic.enabled=true;const calls=[],events=[];let listener,current=true;const contents={getURL:()=> 'https://example.com/app',isDestroyed:()=>false},tab={id:'tab1',guarded:true,security:{},view:{webContents:contents}},context=vm.createContext({require:id=>id.startsWith('./')?require('../apps/browser/'+id.slice(2)):require(id.startsWith('../../')?'../'+id.slice(6):id),Buffer,URL,Date,setTimeout,clearTimeout,core,policy,ready:true,localInspector:{inspect:async data=>{const value=data.toString();calls.push({kind:'local',value});return value==='Synthetic confidential paraphrase'?{decision:'block',ruleId:'semantic-secret'}:{decision:'allow',coverage:{complete:true}};}},scanEnterprise:async data=>{calls.push({kind:'icap',value:data.toString()});return {decision:'allow',coverage:{complete:true}};},diagnosticSpan:(_name,_facts,fn)=>fn(),requestDiagnostic:(_details,_facts,fn)=>fn(),browserSession:{webRequest:{onBeforeRequest:(_filter,fn)=>listener=fn}},developerAssetRequestAllowed:()=>false,tabs:{fromContents:()=>tab,all:()=>[tab]},tabContext:()=>({webContents:contents}),currentTabContext:()=>current,agentRequestOwnerAllowed:()=>true,actorDestinationAllowed:()=>true,credentialBroker:null,recentRequests:new Map(),dnsGate:async()=>calls.push({kind:'dns'}),audit:async(kind,ruleId)=>events.push({kind,ruleId}),checkVisual:async()=>({decision:'allow'}),icapRequired:()=>true,emit:()=>{},message:'',uploadLeaseFatal:false,diagnosticCorrelation:()=>({}),credentialRequestCorrelations:new Map()});vm.runInContext(wrapper+gate,context);const run=details=>new Promise((resolve,reject)=>{const deadline=setTimeout(()=>reject(Error('Gate timeout')),3000);listener({...details,webContentsId:1,resourceType:details.resourceType||'xhr'},result=>{clearTimeout(deadline);resolve(result);});});return {context,policy,calls,events,run,invalidate:()=>current=false};}
|
|
test('actual main full URL scans decoded semantic and ICAP representations before DNS lookup',async()=>{const f=fixture(),encoded=Buffer.from('Synthetic confidential paraphrase').toString('base64'),r=await f.run({id:1,url:'https://example.com/?prompt='+encoded,method:'GET'});assert.equal(r.cancel,true);assert.ok(f.calls.some(c=>c.value==='Synthetic confidential paraphrase'));assert.equal(f.calls.some(c=>c.kind==='dns'),false);assert.ok(f.events.some(e=>e.ruleId==='semantic-secret'));});
|
|
test('actual main benign URL and encoded text body pass normal inspection while decoded secrets do not',async()=>{for(const secret of [false,true]){const f=fixture(),text=secret?'Synthetic confidential paraphrase':'PUBLIC_TEXT',bytes=Buffer.from(JSON.stringify({prompt:Buffer.from(text).toString('base64')})),result=await f.run({id:2,url:'https://example.com/upload',method:'POST',uploadData:[{bytes}]});assert.equal(result.cancel,secret);assert.ok(f.calls.some(c=>c.value===text));assert.ok(f.calls.some(c=>c.kind==='dns'));if(!secret)assert.ok(f.events.some(e=>e.kind==='upload.authorized'));}});
|
|
test('actual main URL scan policy change prevents DNS and network release',async()=>{const f=fixture();f.context.scanEnterprise=async()=>{f.invalidate();return {decision:'allow',coverage:{complete:true}};};assert.equal((await f.run({id:1,url:'https://example.com/public',method:'GET'})).cancel,true);assert.equal(f.calls.some(c=>c.kind==='dns'),false);});
|
|
test('native connection header retains existing site restrictions and forces WebRTC block on the first list',()=>{const context=vm.createContext({});vm.runInContext(headers,context);const input={'Content-Security-Policy':["default-src 'self'"],'connection-allowlist':['("https://restricted.example"); redirects=block; webrtc=allow, ("https://ignored.example"); webrtc=allow']},result=context.responsePolicyHeaders(input);assert.equal(result['Content-Security-Policy'],input['Content-Security-Policy']);assert.match(result['Connection-Allowlist'][0],/^\("https:\/\/restricted\.example"\); redirects=block; webrtc=allow; webrtc=block,/);assert.equal(input['connection-allowlist'][0].includes('; webrtc=block'),false);assert.match(context.responsePolicyHeaders({})['Connection-Allowlist'][0],/webrtc=block$/);});
|
|
|
|
test('header parser preserves quoted commas and parameters without weakening site endpoint restrictions',()=>{const context=vm.createContext({});vm.runInContext(headers,context);const original='("https://restricted.example/a,b?q=(x)"); note=";webrtc=allow"; webrtc=allow, ("https://ignored.example")';const result=context.responsePolicyHeaders({'Connection-Allowlist':[original]})['Connection-Allowlist'][0];assert.equal(result,'("https://restricted.example/a,b?q=(x)"); note=";webrtc=allow"; webrtc=allow; webrtc=block, ("https://ignored.example")');assert.throws(()=>context.responsePolicyHeaders({'Connection-Allowlist':['("unterminated)']}));});
|
|
test('registered secret hostname is blocked before any explicit DNS resolution',async()=>{const f=fixture();f.policy.dlp.rules=[{id:'hostname-secret',kind:'literal',value:'synthetic-company-secret'}];assert.equal((await f.run({id:3,url:'https://synthetic-company-secret.example/path',method:'GET'})).cancel,true);assert.equal(f.calls.some(c=>c.kind==='dns'),false);assert.ok(f.events.some(e=>e.ruleId==='hostname-secret'));});
|