16 lines
6.0 KiB
JavaScript
16 lines
6.0 KiB
JavaScript
import test from 'node:test';
|
|||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import {randomUUID} from 'node:crypto';
|
||
|
|
import {mkdtempSync,rmSync} from 'node:fs';
|
||
|
|
import {tmpdir} from 'node:os';
|
||
|
|
import {join,resolve,sep,basename} from 'node:path';
|
||
|
|
import {openStore,digest} from '../apps/server/store.mjs';
|
||
|
|
import {createApp} from '../apps/server/app.mjs';
|
||
|
|
import {toECS,toCEF} from '../packages/core/siem.mjs';
|
||
|
|
const kinds=['vault.submission-prepared','vault.submission-authorized','vault.submission-denied','vault.submission-completed','vault.submission.failure'];
|
||
|
|
async function fixture(t){const dir=mkdtempSync(join(tmpdir(),'safetynet-controlled-login-audit-')),store=openStore(dir),token='synthetic-controlled-login-device-token';store.db.prepare('INSERT INTO devices(id,name,token_hash) VALUES(?,?,?)').run('device','Synthetic',digest(token));for(const role of ['analyst','auditor','operator'])store.db.prepare('INSERT INTO sessions VALUES(?,?,?)').run(digest('synthetic-'+role),Date.now()+600000,JSON.stringify({sub:role,role}));store.db.prepare('INSERT INTO sessions VALUES(?,?,?)').run(digest('synthetic-employee'),Date.now()+600000,JSON.stringify({sub:'employee',role:'employee',permissions:['audit.read']}));const server=createApp(store,{publicUrl:'http://127.0.0.1',secure:false}).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));t.after(async()=>{server.closeAllConnections();await new Promise(r=>server.close(r));store.db.close();const target=resolve(dir);assert.ok(target.startsWith(resolve(tmpdir())+sep)&&basename(target).startsWith('safetynet-controlled-login-audit-'));rmSync(target,{recursive:true,force:true});});const base=`http://127.0.0.1:${server.address().port}`;return {store,base,send:(body,credential=token)=>fetch(base+'/api/device/events',{method:'POST',headers:{authorization:`Bearer ${credential}`,'content-type':'application/json'},body:JSON.stringify({eventId:randomUUID(),policyVersion:1,...body})})};}
|
||
|
|
test('authenticated controlled login lifecycle audit is deduplicated and rejects credential-bearing metadata',async t=>{const f=await fixture(t);const traceId='a'.repeat(32),spanId='b'.repeat(16);for(const kind of kinds){const eventId=randomUUID();assert.equal((await f.send({eventId,kind,traceId,spanId})).status,200);assert.equal((await f.send({eventId,kind,traceId,spanId})).status,200);}assert.equal((await f.send({kind:kinds[0]},'wrong-token')).status,401);for(const kind of kinds)for(const key of ['host','ruleId','username','password','formId','url','actionUrl','body','credentialRef','token','reason'])assert.equal((await f.send({kind,[key]:'PRIVATE_LOGIN_SENTINEL'})).status,400,kind+':'+key);for(const extra of [{file:{bytes:1,detectedType:'text',sha256:'c'.repeat(64)}},{request:{bytes:1,detectedType:'text'}},{agent:{action:'vault:prepare-submit',scope:'vault:prepare-submit',outcome:'completed'}},{interaction:{action:'submit',outcome:'observed',sessionId:randomUUID()}}])assert.equal((await f.send({kind:kinds[1],...extra})).status,400);const rows=f.store.db.prepare('SELECT * FROM events').all();assert.equal(rows.length,kinds.length);assert.equal(f.store.auditIntegrity.verify().valid,true);for(const row of rows){const m=JSON.parse(row.metadata);assert.equal(m.traceId,traceId);assert.equal(m.spanId,spanId);assert.ok(!row.metadata.includes('PRIVATE_LOGIN_SENTINEL'));}});
|
||
|
|
test('security analysts can export lifecycle ECS and CEF without credentials; employee self-claimed permissions cannot',async t=>{const f=await fixture(t);for(const kind of kinds)assert.equal((await f.send({kind,traceId:'a'.repeat(32),spanId:'b'.repeat(16)})).status,200);for(const role of ['analyst','auditor','operator'])for(const format of ['ecs','cef']){const res=await fetch(f.base+'/api/admin/events/export?format='+format,{headers:{cookie:'sn_session=synthetic-'+role}});assert.equal(res.status,200);const body=await res.text();for(const kind of kinds)assert.ok(body.includes(kind));for(const value of ['PRIVATE_LOGIN_SENTINEL','synthetic-controlled-login-device-token','passwordField','credentialRef'])assert.ok(!body.includes(value));if(format==='ecs'){const rows=body.trim().split('\n').map(s=>JSON.parse(s));assert.equal(rows.length,kinds.length);assert.equal(rows[0].trace.id,'a'.repeat(32));assert.ok(rows[0].safetynet.audit.hash);}}assert.ok([401,403].includes((await fetch(f.base+'/api/admin/events/export',{headers:{cookie:'sn_session=synthetic-employee'}})).status));assert.equal((await fetch(f.base+'/api/admin/events/export')).status,401);f.store.db.prepare('UPDATE devices SET revoked=1 WHERE id=?').run('device');assert.equal((await f.send({kind:kinds[0]})).status,401);});
|
||
|
|
test('denied credential submission exports denial rather than successful authorization',()=>{for(const kind of kinds){const event={id:1,time:new Date().toISOString(),kind,device_id:'device',metadata:{policyVersion:1,traceId:'a'.repeat(32)}};const ecs=toECS(event),cef=toCEF(event);assert.equal(ecs.event.action,kind);assert.ok(cef.includes('act='+kind));if(kind===kinds[2]){assert.deepEqual(ecs.event.type,['denied']);assert.equal(ecs.safetynet.decision,'block');assert.ok(cef.includes('|7|'));}}});
|
||
|
|
|
||
|
|
test('terminal HTTP lifecycle exports distinguish completion from transaction failure',()=>{for(const kind of ['vault.submission-completed','vault.submission.failure']){const event={id:5,time:new Date().toISOString(),kind,device_id:'device',metadata:{policyVersion:2,traceId:'c'.repeat(32),spanId:'d'.repeat(16)},audit:{hash:'e'.repeat(64),sequence:5}};const ecs=toECS(event),cef=toCEF(event);assert.equal(ecs.event.action,kind);assert.equal(ecs.event.category[0],'web');assert.equal(ecs.event.outcome,kind.endsWith('.failure')?'failure':'success');assert.deepEqual(ecs.event.type,[kind.endsWith('.failure')?'error':'allowed']);assert.equal(ecs.trace.id,event.metadata.traceId);assert.ok(cef.includes('|3|'));assert.ok(cef.includes('act='+kind));assert.ok(cef.includes('cs4Label=TraceId'));assert.equal(ecs.safetynet.agent,undefined);assert.equal(ecs.destination,undefined);assert.equal(ecs.rule,undefined);assert.ok(!JSON.stringify(ecs).includes('authenticated'));}});
|