Audit native transports and retain secure export diagnostics

This commit is contained in:
Karl committed 2026-10-14 00:37:32 +02:00
1 parent 8a253eda89
commit 0a5df7398e
28 files changed
+636 -53

No files matched your search

+6 -5
View File
@@ -16,18 +16,19 @@ export function approvedAIEndpoint(policy,config){
}
export async function runApprovedAI({store,config,deviceId,policy,prompt,signal}){
return traceOperation('ai.gateway',{policyVersion:policy.version,bytes:Buffer.byteLength(prompt)},async()=>{
const controller=new AbortController();
const cancel=()=>controller.abort();
const controller=new AbortController();let cancellationError;
const abortWith=error=>{cancellationError??=error;controller.abort();};
const cancel=()=>abortWith(rejected(403,'AI authorization changed during processing','ai-authorization-changed'));
signal?.addEventListener('abort',cancel,{once:true});
if(signal?.aborted)cancel();
function assertCurrent(){
const active=store.db.prepare('SELECT id FROM devices WHERE id=? AND revoked=0').get(deviceId);
const policyChanged=active&&!isDeepStrictEqual(store.effectivePolicy(deviceId),policy);
if(controller.signal.aborted||!active||Date.parse(policy.expiresAt)<=Date.now()||policyChanged){
controller.abort();throw rejected(policyChanged?409:403,'AI authorization changed during processing','ai-authorization-changed');
abortWith(rejected(policyChanged?409:403,'AI authorization changed during processing','ai-authorization-changed'));throw cancellationError;
}
}
const deadline=setTimeout(()=>controller.abort(),60000);
const deadline=setTimeout(()=>abortWith(rejected(504,'AI request timed out','ai-request-timeout')),60000);
const monitor=setInterval(()=>{try{assertCurrent();}catch{/* Abort pending provider work; release checks report the fixed failure. */}},100);
monitor.unref();deadline.unref();
try{
@@ -62,7 +63,7 @@ export async function runApprovedAI({store,config,deviceId,policy,prompt,signal}
const output=await traceOperation('ai.inspect.output',{bytes:Buffer.byteLength(answer),policyVersion:policy.version},()=>scan(answer));store.event(deviceId,'ai.output-decision',{...output,policyVersion:policy.version});
if(output.decision==='block'){store.event(deviceId,'ai.output-block',{ruleId:output.ruleId,policyVersion:policy.version});throw rejected(403,'AI response blocked by company policy',output.ruleId);}
assertCurrent();return answer;
}catch(e){store.event(deviceId,'ai.failure',{policyVersion:policy.version});throw e;}
}catch(e){store.event(deviceId,'ai.failure',{policyVersion:policy.version});throw cancellationError||e;}
finally{clearTimeout(deadline);clearInterval(monitor);signal?.removeEventListener('abort',cancel);controller.abort();}
});
}
+7 -2
View File
@@ -4,6 +4,7 @@ import transferFacts from '../../packages/core/transfer-facts.cjs';
import requestTransferFacts from '../../packages/core/request-transfer-facts.cjs';
import agentAudit from '../../packages/core/agent-audit.cjs';
import interactionAudit from '../../packages/core/interaction-facts.cjs';
import transportAudit from '../../packages/core/transport-facts.cjs';
import {installEvidence} from './evidence.mjs';
import {installIncidents} from './incidents.mjs';
import { installBookmarkRoutes,globalBookmarksValid } from './bookmarks.mjs';
@@ -107,7 +108,11 @@ export function createApp(store,config) {
store.db.prepare('UPDATE devices SET last_seen=?,policy_version=? WHERE id=?').run(new Date().toISOString(),b.policyVersion,req.device.id);res.json({ok:true});
});
app.post('/api/device/events',device,(req,res)=>{
const b=z.object({eventId:z.string().uuid(),kind:z.enum(['browser.interaction','browser.interaction.dropped','dlp.block','navigation.block','download.block','policy.applied','browser.started','agent.action','agent.denied','library.ai.proposed','library.ai.applied','library.ai.reconcile','devtools.open-requested','devtools.opened','devtools.closed','devtools.denied','download.scan','download.released','upload.authorized','upload.block','dns.block','content.block','vault.submission-completed','vault.submission.failure','vault.submission-prepared','vault.submission-authorized','vault.submission-denied','vault.created','vault.unlocked','vault.locked','vault.saved','vault.deleted','vault.filled','vault.fill-requested','phishing.block','phishing.error','phishing.scanned','clipboard.block','clipboard.copy','clipboard.cut','clipboard.paste']),ruleId:z.string().max(100).optional(),host:z.string().max(253).optional(),policyVersion:z.number().int().positive(),traceId:z.string().regex(/^[a-f0-9]{32}$/).optional(),spanId:z.string().regex(/^[a-f0-9]{16}$/).optional(),file:transferFacts.FileFactsSchema.optional(),request:requestTransferFacts.RequestFactsSchema.optional(),agent:agentAudit.AgentAuditSchema.optional(),interaction:interactionAudit.InteractionFactsSchema.optional()}).strict().parse(req.body);
const b=z.object({eventId:z.string().uuid(),kind:z.enum([...transportAudit.TRANSPORT_KINDS,'browser.interaction','browser.interaction.dropped','dlp.block','navigation.block','download.block','policy.applied','browser.started','agent.action','agent.denied','library.ai.proposed','library.ai.applied','library.ai.reconcile','devtools.open-requested','devtools.opened','devtools.closed','devtools.denied','download.scan','download.released','upload.authorized','upload.block','dns.block','content.block','vault.submission-completed','vault.submission.failure','vault.submission-prepared','vault.submission-authorized','vault.submission-denied','vault.created','vault.unlocked','vault.locked','vault.saved','vault.deleted','vault.filled','vault.fill-requested','phishing.block','phishing.error','phishing.scanned','clipboard.block','clipboard.copy','clipboard.cut','clipboard.paste']),ruleId:z.string().max(100).optional(),host:z.string().max(253).optional(),policyVersion:z.number().int().positive(),traceId:z.string().regex(/^[a-f0-9]{32}$/).optional(),spanId:z.string().regex(/^[a-f0-9]{16}$/).optional(),file:transferFacts.FileFactsSchema.optional(),request:requestTransferFacts.RequestFactsSchema.optional(),agent:agentAudit.AgentAuditSchema.optional(),interaction:interactionAudit.InteractionFactsSchema.optional(),transport:transportAudit.TransportFactsSchema.optional()}).strict().parse(req.body);
if(b.kind.startsWith('transport.')){
const unavailable=b.kind==='transport.monitor-unavailable';
if(b.file||b.request||b.agent||b.interaction|| (unavailable?(b.host!==undefined||b.transport!==undefined||!transportAudit.TRANSPORT_MONITOR_REASONS.includes(b.ruleId)):(b.ruleId!==undefined||!transportAudit.canonicalTransportHostname(b.host)||!transportAudit.transportFactsForEvent(b.kind,b.transport))))return res.status(400).json({error:'Transport audit events require matching lifecycle metadata'});
}else if(b.transport!==undefined)return res.status(400).json({error:'Transport facts require a matching transport event'});
if(b.interaction&&!interactionAudit.interactionFactsForEvent(b.kind,b.interaction))return res.status(400).json({error:'Interaction facts require a matching lifecycle event'});
if(b.agent&&!agentAudit.agentFactsForEvent(b.kind,b.agent))return res.status(400).json({error:'Agent facts require a matching agent lifecycle event'});
if(b.file&&!b.kind.startsWith('download.')&&!b.kind.startsWith('upload.'))return res.status(400).json({error:'File facts require a transfer event'});
@@ -269,7 +274,7 @@ export function createApp(store,config) {
app.use((error,_req,res,_next)=>{
config.onError?.(error);
const invalid=error instanceof z.ZodError || error.type==='entity.parse.failed';
res.status(invalid?400:error.type==='entity.too.large'?413:[400,403,404,409].includes(error.status)?error.status:500).json({error:invalid?'Request validation failed':'Request failed',...(error instanceof z.ZodError?{issues:error.issues.map(v=>({path:v.path.join('.'),message:v.message}))}:{})});
res.status(invalid?400:error.type==='entity.too.large'?413:[400,403,404,409,504].includes(error.status)?error.status:500).json({error:invalid?'Request validation failed':'Request failed',...(error instanceof z.ZodError?{issues:error.issues.map(v=>({path:v.path.join('.'),message:v.message}))}:{})});
});
return app;
}