Files
safetynet/tests/download-gate.test.mjs
T

52 lines
7.2 KiB
JavaScript
Raw Normal View History

2026-10-13 10:33:31 +02:00
import test from 'node:test';
import assert from 'node:assert/strict';
import {createHash} from 'node:crypto';
import {createRequire} from 'node:module';
const {createDownloadGate,filename}=createRequire(import.meta.url)('../apps/browser/download-gate.cjs');
function setup(overrides={}){
const calls=[],bytes=Buffer.from('benign download'),context={ready:true,webContentsId:5,policy:{version:1,browser:{allowDownloads:true},security:{yara:{enabled:true,rules:[{}],maxScanBytes:1024}}}};
const gate=createDownloadGate({getContext:()=>context,isCurrent:()=>true,fetchResponse:async()=>new Response(bytes),scan:async b=>{calls.push('scan');return{decision:'allow',coverage:{complete:true},sha256:createHash('sha256').update(b).digest('hex')};},choosePath:async()=>{calls.push('dialog');return 'selected';},writeFile:async(_path,data)=>{calls.push('write');assert.deepEqual(data,bytes);},audit:async kind=>calls.push(kind),...overrides});
return{gate,calls,context,input:{url:'https://example.com/file',name:'safe.bin',method:'GET',webContentsId:5}};
}
test('downloads never ask or write before scan allows exact bytes',async()=>{const {gate,input,calls}=setup();assert.equal((await gate(input)).released,true);assert.ok(calls.indexOf('scan')<calls.indexOf('dialog'));assert.ok(calls.indexOf('dialog')<calls.indexOf('write'));});
test('matched malware cannot reach destination filesystem',async()=>{const {gate,input,calls}=setup({scan:async()=>({decision:'block',ruleId:'malware'})});assert.equal((await gate(input)).released,false);assert.equal(calls.includes('write'),false);assert.equal(calls.includes('dialog'),false);});
test('scanner absence, changed policy, wrong digest and POST fail closed',async()=>{
for(const overrides of [{scan:async()=>{throw Error('unavailable');}},{isCurrent:()=>false},{scan:async()=>({decision:'allow',coverage:{complete:true},sha256:'wrong'})}]){const {gate,input,calls}=setup(overrides);assert.equal((await gate(input)).released,false);assert.equal(calls.includes('write'),false);}
const {gate,input,calls}=setup();assert.equal((await gate({...input,method:'POST'})).released,false);assert.equal(calls.includes('write'),false);
});
test('oversized responses are denied before scanning and write',async()=>{const {gate,input,calls}=setup({fetchResponse:async()=>new Response(Buffer.alloc(2048))});assert.equal((await gate(input)).released,false);assert.equal(calls.includes('scan'),false);assert.equal(calls.includes('write'),false);});
test('filename cannot introduce device names, traversal or ADS',()=>{assert.equal(filename('CON'),'download.bin');assert.equal(filename('C:\\secret\\a:payload.exe'),'a_payload.exe');assert.equal(filename('../safe.txt'),'safe.txt');});
test('download captures the originating tab rather than the active tab',async()=>{
let requested;const f=setup({getContext:id=>{requested=id;return f.context;}});
assert.equal((await f.gate(f.input)).released,true);assert.equal(requested,5);
const missing=setup({getContext:()=>null,fetchResponse:async()=>{throw Error('Must not fetch unknown origin');}});
assert.equal((await missing.gate(missing.input)).reason,'unmanaged-download');
});
test('closing the originating tab while scanning prevents dialog and write',async()=>{
let current=true;const f=setup({isCurrent:()=>current,scan:async bytes=>{current=false;return {decision:'allow',coverage:{complete:true},sha256:createHash('sha256').update(bytes).digest('hex')};}});
assert.equal((await f.gate(f.input)).reason,'policy-changed');assert.equal(f.calls.includes('dialog'),false);assert.equal(f.calls.includes('write'),false);
});
test('configured ICAP can replace local antivirus while its bound size limit remains enforced',async()=>{const {gate,input,context,calls}=setup();context.policy.security.yara.enabled=false;context.policy.enterprise={icap:{enabled:true,scanDownloads:true,maxBytes:1024}};assert.equal((await gate(input)).released,true);assert.ok(calls.includes('write'));const f=setup();f.context.policy.security.yara.enabled=false;f.context.policy.enterprise={icap:{enabled:true,scanDownloads:true,maxBytes:5}};assert.equal((await f.gate(f.input)).released,false);assert.equal(f.calls.includes('scan'),false);});
test('magic-byte executable and mismatch blocks precede scanner, dialog and OS write',async()=>{
for(const setting of [{blockedTypes:['pe']},{blockExtensionMismatch:true},{blockMimeMismatch:true}]){
const f=setup({fetchResponse:async()=>new Response(Buffer.from('MZbinary'),{headers:{'content-type':'image/png'}})});f.context.policy.fileTransfers={download:setting};const v=await f.gate({...f.input,name:'photo.png'});assert.equal(v.released,false);assert.equal(f.calls.includes('scan'),false);assert.equal(f.calls.includes('write'),false);
}
});
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);
});