Audit native transports and retain secure export diagnostics
This commit is contained in:
1 parent
8a253eda89
commit
0a5df7398e
28 files changed
+636
-53
No files matched your search
@@ -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
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user