38 lines
5.1 KiB
JavaScript
38 lines
5.1 KiB
JavaScript
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);
|
||
|
|
});
|