import test from 'node:test'; import assert from 'node:assert/strict'; import { defaultPolicy,PolicySchema,destinationAllowed,inspect,inspectForAI,fragmentHashes,verifyPolicy } from '../packages/core/policy.mjs'; import { dnsAddressBlocked,evaluateSigma,VisualOriginSchema } from '../packages/core/security.mjs'; import { createApp } from '../apps/server/app.mjs'; import { openStore } from '../apps/server/store.mjs'; import { mkdtempSync,rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createServer } from 'node:http'; import { randomUUID } from 'node:crypto'; import { deflateSync } from 'node:zlib'; test('security defaults deny agent access and DNS/content policies enforce centrally',()=>{ const p=defaultPolicy();assert.equal(p.agents.enabled,false);p.security.dns.blockedHosts=['*.evil.test'];p.security.dns.blockedIPs=['2001:db8::1'];p.security.content.blockedPhrases=['restricted roadmap']; assert.equal(destinationAllowed('https://child.evil.test',p),false);assert.equal(destinationAllowed('https://evil.test.example',p),true);assert.equal(dnsAddressBlocked('2001:0db8:0:0:0:0:0:1',p),true);assert.equal(inspect(Buffer.from('Restricted Roadmap'),p).decision,'block'); p.security.dns.blockedIPs=['not-an-address'];assert.equal(PolicySchema.safeParse(p).success,false); }); test('password manager defaults and idle bounds reject insecure or unknown settings',()=>{ const p=defaultPolicy();assert.deepEqual(p.passwordManager,{enabled:true,allowSaving:true,allowAutofill:true,idleLockSeconds:300});p.passwordManager.idleLockSeconds=59;assert.equal(PolicySchema.safeParse(p).success,false);p.passwordManager.idleLockSeconds=901;assert.equal(PolicySchema.safeParse(p).success,false);p.passwordManager.idleLockSeconds=60;p.passwordManager.password='secret';assert.equal(PolicySchema.safeParse(p).success,false); }); test('visual references require explicit valid brands and canonical exact domain origins',()=>{ const p=defaultPolicy();assert.equal(p.security.visualPhishing.enabled,false);p.security.visualPhishing.enabled=true;assert.equal(PolicySchema.safeParse(p).success,false); for(const origin of ['http://login.example','https://*.example','https://login.example/','https://login.example?x=1','https://alice:secret@login.example','https://127.0.0.1','https://[::1]','https://login.example.','https://bücher.example'])assert.equal(VisualOriginSchema.safeParse(origin).success,false,origin); assert.equal(VisualOriginSchema.safeParse('https://xn--bcher-kva.example').success,true);assert.equal(VisualOriginSchema.safeParse('https://login.example:8443').success,true); p.security.visualPhishing.brands=[{id:'brand',label:'Brand',allowedOrigins:['https://login.example'],references:[{hash:'a'.repeat(16),algorithm:'phash-dct32-v1',scope:'login-form',viewport:{width:64,height:64}}]}];assert.equal(PolicySchema.safeParse(p).success,true);p.security.visualPhishing.brands[0].references[0].hash='A'.repeat(16);assert.equal(PolicySchema.safeParse(p).success,false); }); function referencePNG(flat=false){ const crc=bytes=>{let value=0xffffffff;for(const byte of bytes){value^=byte;for(let n=0;n<8;n++)value=(value>>>1)^((value&1)?0xedb88320:0);}return(value^0xffffffff)>>>0;}; const chunk=(kind,data)=>{const type=Buffer.from(kind),length=Buffer.alloc(4),sum=Buffer.alloc(4);length.writeUInt32BE(data.length);sum.writeUInt32BE(crc(Buffer.concat([type,data])));return Buffer.concat([length,type,data,sum]);}; const width=64,height=64,header=Buffer.alloc(13);header.writeUInt32BE(width);header.writeUInt32BE(height,4);header[8]=8;header[9]=6;const rows=Buffer.alloc(height*(1+width*4));for(let y=0;y{ const dir=mkdtempSync(join(tmpdir(),'security-visual-')),store=openStore(dir),adminToken='administrator-token-at-least-thirty-two-characters';const server=createApp(store,{adminToken,publicUrl:'http://127.0.0.1:4310',secure:false}).listen(0,'127.0.0.1');await new Promise(r=>server.on('listening',r));t.after(async()=>{await new Promise(r=>server.close(r));store.db.close();rmSync(dir,{recursive:true,force:true});}); const base=`http://127.0.0.1:${server.address().port}`,data=referencePNG().toString('base64'),body={expectedVersion:1,brandId:'corporate-login',label:'Corporate login',allowedOrigins:['https://login.example'],data,scope:'login-form'}; const derive=(b,token=adminToken)=>fetch(base+'/api/admin/security/visual-reference',{method:'POST',headers:{authorization:`Bearer ${token}`,'content-type':'application/json'},body:JSON.stringify(b)}); assert.equal((await derive(body,'unauthorized')).status,401);assert.equal((await derive({...body,allowedOrigins:['https://*.example']})).status,400);assert.equal((await derive({...body,data:referencePNG(true).toString('base64')})).status,400);const response=await derive(body);assert.equal(response.status,200);const result=await response.json();assert.match(result.brand.references[0].hash,/^[a-f0-9]{16}$/);assert.deepEqual(result.brand.references[0].viewport,{width:64,height:64});assert.equal(result.quality.contrast>10,true);assert.equal(store.get('policy').version,1);assert.deepEqual(store.get('policy').security.visualPhishing.brands,[]); const values=store.db.prepare('SELECT value FROM settings').all().map(r=>r.value).join('')+store.db.prepare('SELECT metadata FROM events').all().map(r=>r.metadata).join('');assert.equal(values.includes(data),false);assert.equal(store.db.prepare('SELECT count(*) AS n FROM teaching').get().n,0); const policy=store.get('policy');policy.version=2;policy.security.visualPhishing={enabled:true,failClosed:true,brands:[result.brand]};const published=await fetch(base+'/api/admin/policy',{method:'PUT',headers:{authorization:`Bearer ${adminToken}`,'content-type':'application/json'},body:JSON.stringify(policy)});assert.equal(published.status,200);assert.equal(store.get('policy').security.visualPhishing.brands[0].id,'corporate-login'); }); test('fragment fingerprints catch partial document excerpts across capitalization',()=>{ const source='The secret engineering roadmap describes eight amazing new confidential customer features tomorrow';const value=[...fragmentHashes(source)][1];const p=defaultPolicy();p.dlp.rules=[{id:'excerpt',label:'Private',kind:'fragment-sha256',value,action:'block'}]; assert.equal(inspect(Buffer.from(source.toUpperCase()+' additional public text'),p).decision,'block');assert.equal(inspect(Buffer.from('unrelated public information'),p).decision,'allow'); }); test('confidential local AI override preserves ordinary DLP and never permits credentials',()=>{ const p=defaultPolicy();p.ai={...p.ai,enabled:true,allowConfidentialLocal:true,trustMode:'external'};p.dlp.rules=[{id:'company-roadmap',label:'Roadmap',kind:'literal',value:'confidential roadmap',action:'block'}];const content=Buffer.from('confidential roadmap');assert.equal(inspectForAI(content,p).decision,'block');p.ai.trustMode='local';assert.deepEqual(inspectForAI(content,p),{decision:'allow',ruleId:'company-roadmap',classification:'local-only'});assert.equal(inspect(content,p).decision,'block');assert.equal(inspectForAI(Buffer.from('confidential roadmap -----BEGIN PRIVATE KEY-----'),p).ruleId,'private-key');p.security.content.blockedPhrases=['confidential roadmap'];assert.equal(inspectForAI(content,p).decision,'block'); }); test('restricted Sigma rejects unsupported syntax and matches metadata only',()=>{ const p=defaultPolicy();const rule={id:'block-alert',title:'Blocked upload',level:'high',detection:{selection:{kind:'dlp.block',ruleId:'secret'},condition:'selection'}};p.security.sigma.rules=[rule];assert.equal(PolicySchema.safeParse(p).success,true); assert.equal(evaluateSigma({kind:'dlp.block',metadata:{ruleId:'secret',prompt:'not inspected'}},[rule]).length,1);rule.detection.selection={};assert.equal(PolicySchema.safeParse(p).success,false);rule.detection.condition='selection or all of them';assert.equal(PolicySchema.safeParse(p).success,false); }); test('authenticated blocklist publishing versions signed policy and teaching requires approval',async t=>{ const dir=mkdtempSync(join(tmpdir(),'security-')),store=openStore(dir),adminToken='unit-test-secure-admin-token-at-least-32-chars';const app=createApp(store,{adminToken,publicUrl:'http://127.0.0.1:4310',secure:false});const server=app.listen(0,'127.0.0.1');await new Promise(r=>server.on('listening',r));const base=`http://127.0.0.1:${server.address().port}`; t.after(async()=>{await new Promise(r=>server.close(r));store.db.close();rmSync(dir,{recursive:true,force:true});}); const update=(body,authorized=true)=>fetch(base+'/api/admin/security/blocklists',{method:'PUT',headers:{'content-type':'application/json',...(authorized?{authorization:`Bearer ${adminToken}`}:{})},body:JSON.stringify(body)}); const body={expectedVersion:1,blockedHosts:['bad.test'],blockedIPs:['127.0.0.2']};assert.equal((await update(body,false)).status,401);assert.equal((await update({...body,blockedIPs:['bad']})).status,400);assert.equal((await update(body)).status,200);assert.equal(store.get('policy').version,2);assert.equal((await update(body)).status,409); store.db.prepare('INSERT INTO teaching VALUES(?,?,?,?,?,?,?,?)').run('draft','now','owner','Secret','draft',JSON.stringify({sha256:'a'.repeat(64),fragmentHashes:[],registrationReady:true}),null,null); const promote=b=>fetch(base+'/api/admin/teaching/draft/publish',{method:'POST',headers:{'content-type':'application/json',authorization:`Bearer ${adminToken}`},body:JSON.stringify(b)}); assert.equal((await promote({expectedVersion:2,approve:false})).status,400);assert.equal((await promote({expectedVersion:2,approve:true})).status,200);assert.equal(store.get('policy').dlp.rules[0].value,'a'.repeat(64));assert.equal((await promote({expectedVersion:3,approve:true})).status,404); store.db.prepare('INSERT INTO teaching VALUES(?,?,?,?,?,?,?,?)').run('partial','now','owner','Office partial','draft',JSON.stringify({sha256:'b'.repeat(64),fragmentHashes:['c'.repeat(64)],registrationReady:false,coverage:{complete:false}}),null,null); const partial=b=>fetch(base+'/api/admin/teaching/partial/publish',{method:'POST',headers:{'content-type':'application/json',authorization:`Bearer ${adminToken}`},body:JSON.stringify(b)}); assert.equal((await partial({expectedVersion:3,approve:true})).status,409);const promoted=await partial({expectedVersion:3,approve:true,mode:'available-fingerprints'});assert.equal(promoted.status,200);assert.equal((await promoted.json()).coverage,'partial');const final=store.get('policy');assert.equal(final.dlp.rules.length,3);assert.equal(final.dlp.rules[0].value,'a'.repeat(64));assert.equal(final.dlp.rules[2].kind,'fragment-sha256');assert.equal(store.db.prepare("SELECT metadata FROM events WHERE kind='teaching.approved' ORDER BY id DESC LIMIT 1").get().metadata.includes('partial'),true); }); test('existing old policy settings normalize missing security fields without data loss',()=>{ const dir=mkdtempSync(join(tmpdir(),'security-upgrade-')),store=openStore(dir);try{const old=defaultPolicy();delete old.security;delete old.agents;old.ai={enabled:false,model:''};store.put('policy',old);const updated=store.get('policy');assert.equal(updated.agents.enabled,false);assert.deepEqual(updated.security.dns.blockedHosts,[]);assert.equal(updated.version,old.version);assert.equal(updated.ai.provider,'openai-compatible');}finally{store.db.close();rmSync(dir,{recursive:true,force:true});} }); test('per-browser signed manual scan controls preserve mandatory protection and training stores derived metadata only',async t=>{ const raw='confidential full document sentinel must not persist',data=Buffer.from(raw).toString('base64');const inspector=createServer((req,res)=>{assert.equal(req.url,'/reference-document');assert.equal(req.headers.authorization,'Bearer private-inspector-test-token');res.setHeader('content-type','application/json');res.end(JSON.stringify({sha256:'d'.repeat(64),bytes:raw.length,contentType:'text',fragmentHashes:['e'.repeat(64)],registrationReady:true,coverage:{complete:true,textCharacters:raw.length,rawText:raw,fragmentHashesTotal:1010,fragmentHashesRetained:1000,fragmentHashesTruncated:true,extractionComplete:true,featuresComplete:false},data,text:raw}));});inspector.listen(0,'127.0.0.1');await new Promise(r=>inspector.on('listening',r)); const dir=mkdtempSync(join(tmpdir(),'security-manual-')),store=openStore(dir),adminToken='administrator-credential-at-least-thirty-two-characters';const server=createApp(store,{adminToken,publicUrl:'http://127.0.0.1:4310',secure:false,inspectorUrl:`http://127.0.0.1:${inspector.address().port}`,inspectorToken:'private-inspector-test-token'}).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=>inspector.close(r))]);store.db.close();rmSync(dir,{recursive:true,force:true});}); const base=`http://127.0.0.1:${server.address().port}`;async function api(path,body,method=body?'POST':'GET',credential=adminToken){return fetch(base+path,{method,headers:{authorization:`Bearer ${credential}`,...(body?{'content-type':'application/json'}:{})},body:body?JSON.stringify(body):undefined});} const enrollment=async name=>{const code=(await(await api('/api/admin/enrollments',{user:name})).json()).code;return(await api('/api/device/enroll',{code,name,platform:'win32'})).json();};const a=await enrollment('A'),b=await enrollment('B');assert.equal(verifyPolicy(a.policy,a.publicKey).inspection.documentAnalysisEnabled,true); const setting={expectedVersion:1,documentAnalysisEnabled:false};assert.equal((await api(`/api/admin/devices/${a.id}/inspection`,setting,'PATCH',a.token)).status,401);assert.equal((await api(`/api/admin/devices/${a.id}/inspection`,setting,'PATCH')).status,200); const pa=verifyPolicy(await(await api('/api/device/policy',undefined,'GET',a.token)).json(),a.publicKey),pb=verifyPolicy(await(await api('/api/device/policy',undefined,'GET',b.token)).json(),b.publicKey);assert.equal(pa.version,2);assert.equal(pa.inspection.documentAnalysisEnabled,false);assert.equal(pb.inspection.documentAnalysisEnabled,true);assert.equal(pa.dlp.blockUninspectable,true);assert.deepEqual(pa.security.yara,pb.security.yara);assert.equal((await api(`/api/admin/devices/${a.id}/inspection`,setting,'PATCH')).status,409); const response=await api('/api/admin/teaching',{label:'Reference',filename:'reference.txt',data});assert.equal(response.status,200);const stored=store.db.prepare('SELECT reference FROM teaching').get().reference;assert.equal(stored.includes(raw),false);assert.equal(stored.includes(data),false);assert.equal(stored.includes('rawText'),false);assert.equal(stored.includes('sha256'),true);const coverage=JSON.parse(stored).coverage;assert.equal(coverage.fragmentHashesTotal,1010);assert.equal(coverage.fragmentHashesRetained,1000);assert.equal(coverage.fragmentHashesTruncated,true);assert.equal(coverage.extractionComplete,true);assert.equal(coverage.featuresComplete,false);assert.equal(store.db.prepare('SELECT metadata FROM events').all().some(e=>e.metadata.includes(raw)||e.metadata.includes(data)),false); }); test('local Ollama needs no API key, blocks secrets before provider, and Sigma alerts exclude raw content',async t=>{ let calls=0;const model=createServer((req,res)=>{calls++;assert.equal(req.url,'/api/chat');assert.equal(req.headers.authorization,undefined);res.setHeader('content-type','application/json');res.end(JSON.stringify({message:{content:'Local answer'}}));});model.listen(0,'127.0.0.1');await new Promise(r=>model.on('listening',r)); const dir=mkdtempSync(join(tmpdir(),'security-ai-')),store=openStore(dir),p=defaultPolicy();p.ai={enabled:true,model:'local-model',provider:'ollama',trustMode:'local',endpointId:'default'};p.security.sigma.rules=[{id:'agent-denials',title:'Agent blocked',level:'high',detection:{selection:{kind:'agent.denied'},condition:'selection'}}];store.put('policy',p); const token='enrolled-device-test-credential';const {digest}=await import('../apps/server/store.mjs');store.db.prepare('INSERT INTO devices(id,name,platform,user,token_hash,last_seen) VALUES(?,?,?,?,?,?)').run('device','Test','win32','alice',digest(token),'now'); const config={adminToken:'administrator-token-at-least-thirty-two-characters',publicUrl:'http://127.0.0.1:4310',secure:false,aiBaseUrl:`http://127.0.0.1:${model.address().port}`,aiModel:'local-model'};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=>model.close(r))]);store.db.close();rmSync(dir,{recursive:true,force:true});}); const send=(path,body)=>fetch(`http://127.0.0.1:${server.address().port}${path}`,{method:'POST',headers:{authorization:`Bearer ${token}`,'content-type':'application/json'},body:JSON.stringify(body)}); const answer=await send('/api/device/ai',{prompt:'Hello local model'});assert.equal(answer.status,200);assert.equal((await answer.json()).answer,'Local answer');assert.equal(calls,1); assert.equal((await send('/api/device/ai',{prompt:'-----BEGIN PRIVATE KEY-----'})).status,403);assert.equal(calls,1); 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); 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); for(const kind of ['clipboard.copy','clipboard.cut','clipboard.paste','clipboard.block']){assert.equal((await send('/api/device/events',{eventId:randomUUID(),kind,policyVersion:1,ruleId:'clipboard-test'})).status,200);for(const extra of [{text:'private clipboard sentinel'},{clipboard:'private clipboard sentinel'},{password:'private clipboard sentinel'}])assert.equal((await send('/api/device/events',{eventId:randomUUID(),kind,policyVersion:1,...extra})).status,400);} const audit=store.db.prepare('SELECT metadata FROM events').all().map(v=>v.metadata).join('');assert.equal(audit.includes('Hello local model'),false);assert.equal(audit.includes('BEGIN PRIVATE KEY'),false);assert.equal(audit.includes('private clipboard sentinel'),false); });