Preserve broker audit causation in central ingestion and SIEM exports
This commit is contained in:
1 parent
9e7cb57093
commit
95d23f4e0b
6 files changed
+132
-3
No files matched your search
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
const {z}=require('zod');
|
||||
const {RequestFactsSchema}=require('./request-transfer-facts.cjs');
|
||||
const BROKER_REASONS=Object.freeze(['broker-approved','broker-policy-block','broker-inspection-fault','broker-authority-expired','broker-audit-failed','broker-connect-denied','broker-auth-denied','broker-framing-block','broker-size-block','broker-request-aborted','broker-request-timeout','broker-upstream-failed','broker-response-invalid']);
|
||||
const BrokerFactsSchema=z.object({requestId:z.string().uuid(),connectionId:z.string().uuid().optional(),phase:z.enum(['connect.block','request.authorized','request.block','request.fault','forward.fault']),reason:z.enum(BROKER_REASONS),authorizedEventId:z.string().uuid().optional()}).strict();
|
||||
const phaseReasons=Object.freeze({
|
||||
'connect.block':['broker-connect-denied','broker-auth-denied','broker-framing-block','broker-authority-expired','broker-audit-failed','broker-request-timeout','broker-size-block','broker-request-aborted'],
|
||||
'request.authorized':['broker-approved'],
|
||||
'request.block':['broker-policy-block','broker-authority-expired','broker-framing-block','broker-size-block','broker-request-aborted','broker-request-timeout'],
|
||||
'request.fault':['broker-inspection-fault','broker-audit-failed'],
|
||||
'forward.fault':['broker-upstream-failed','broker-response-invalid','broker-request-aborted','broker-request-timeout','broker-authority-expired','broker-audit-failed']
|
||||
});
|
||||
function validateBrokerSemantics(event,context){
|
||||
if(!event.broker)return;
|
||||
const value=event.broker;
|
||||
if(!event.request||event.file||event.kind!==(value.phase==='request.authorized'?'upload.authorized':'upload.block')||!phaseReasons[value.phase].includes(value.reason)||value.phase==='forward.fault'&&!value.authorizedEventId||value.phase!=='forward.fault'&&value.authorizedEventId||value.authorizedEventId===event.eventId){
|
||||
context.addIssue({code:'custom',path:['broker'],message:'Broker lifecycle semantics rejected'});
|
||||
}
|
||||
}
|
||||
const BrokerAuditEventSchema=z.object({eventId:z.string().uuid(),kind:z.enum(['upload.authorized','upload.block']),ruleId:z.string().min(1).max(100),policyVersion:z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),host:z.string().max(253).optional(),request:RequestFactsSchema,broker:BrokerFactsSchema.optional(),traceId:z.string().regex(/^[a-f0-9]{32}$/).optional(),spanId:z.string().regex(/^[a-f0-9]{16}$/).optional()}).strict().superRefine(validateBrokerSemantics);
|
||||
module.exports={BrokerAuditEventSchema,BrokerFactsSchema,BROKER_REASONS,validateBrokerSemantics};
|
||||
+16
-2
@@ -3,17 +3,24 @@ import transportAudit from './transport-facts.cjs';
|
||||
import agentAudit from './agent-audit.cjs';
|
||||
import transferFacts from './transfer-facts.cjs';
|
||||
import requestTransferFacts from './request-transfer-facts.cjs';
|
||||
import brokerAudit from './broker-audit.cjs';
|
||||
const brokerFacts=event=>{
|
||||
const m=event.metadata;if(!m.broker)return null;
|
||||
const parsed=brokerAudit.BrokerAuditEventSchema.safeParse({eventId:m.eventId,kind:event.kind,ruleId:m.ruleId,policyVersion:m.policyVersion,request:m.request,broker:m.broker,...(m.host?{host:m.host}:{}),...(m.file?{file:m.file}:{})});
|
||||
return parsed.success?{eventId:parsed.data.eventId,...parsed.data.broker}:null;
|
||||
};
|
||||
const requestFacts=(metadata,kind)=>{if(!kind?.startsWith('upload.'))return null;const old=metadata.file;const parsed=requestTransferFacts.RequestFactsSchema.safeParse(metadata.request||(old?{bytes:old.bytes,detectedType:old.detectedType}:undefined));return parsed.success?parsed.data:null;};
|
||||
const facts=metadata=>{const parsed=transferFacts.FileFactsSchema.safeParse(metadata.file);return parsed.success?parsed.data:null;};
|
||||
export function toECS(event,installationId='safetynet') {
|
||||
const m=event.metadata,transport=transportAudit.canonicalTransportHostname(m.host)?transportAudit.transportFactsForEvent(event.kind,event.metadata.transport):null,transportEvent=transportAudit.TRANSPORT_KINDS.includes(event.kind),interaction=interactionAudit.interactionFactsForEvent(event.kind,event.metadata.interaction),agent=agentAudit.agentFactsForEvent(event.kind,event.metadata.agent),block=event.kind.includes('block')||event.kind.endsWith('.denied')||event.kind==='vault.submission-denied'||m.decision==='block',failure=event.kind.endsWith('.failure')||agent?.outcome==='failed',alert=event.kind==='sigma.alert';
|
||||
const broker=brokerFacts(event),brokerFailure=broker&&['request.fault','forward.fault'].includes(broker.phase);
|
||||
const config=event.kind.startsWith('policy.')||event.kind.startsWith('enrollment.')||event.kind==='device.revoked';
|
||||
const auth=event.kind.startsWith('sso.'),clipboard=event.kind.startsWith('clipboard.');
|
||||
return {
|
||||
const output={
|
||||
'@timestamp':event.time,ecs:{version:'8.17.0'},
|
||||
event:{id:`${installationId}:${event.id}`,kind:alert?'alert':'event',module:'safetynet',dataset:'safetynet.security',
|
||||
action:event.kind,category:[alert?'intrusion_detection':auth?'authentication':config?'configuration':clipboard?'process':'web'],
|
||||
type:[transportEvent||alert||interaction?.outcome==='observed'?'info':failure?'error':auth?'start':config?'change':block?'denied':clipboard?'access':'allowed'],outcome:transportEvent||interaction?.outcome==='observed'?'unknown':failure?'failure':'success',...(alert?{severity:severity(m.level)}:{})},
|
||||
type:[transportEvent||alert||interaction?.outcome==='observed'?'info':failure||brokerFailure?'error':auth?'start':config?'change':block?'denied':clipboard?'access':'allowed'],outcome:transportEvent||interaction?.outcome==='observed'||broker?.phase==='request.authorized'?'unknown':failure||brokerFailure?'failure':'success',...(alert?{severity:severity(m.level)}:{})},
|
||||
observer:{vendor:'SafetyNet',product:'Managed Enterprise Browser',type:'dlp',version:'0.1.0'},
|
||||
...(event.device_id?{host:{id:event.device_id}}:{}),
|
||||
...(m.actor?{user:{id:m.actor}}:{}),...(m.host&&(!transportEvent||transport)?{destination:{domain:m.host}}:{}),
|
||||
@@ -25,12 +32,18 @@ export function toECS(event,installationId='safetynet') {
|
||||
content_type:m.contentType??null,model:m.model??null,coverage:m.coverage??null,classification:m.classification==='local-only'?'local-only':null,
|
||||
...(alert?{source_event_id:`${installationId}:${m.sourceEventId}`,severity:m.level}:{})}
|
||||
};
|
||||
if(broker)output.safetynet.broker={event_id:broker.eventId,request_id:broker.requestId,phase:broker.phase,reason:broker.reason,...(broker.connectionId?{connection_id:broker.connectionId}:{}),...(broker.authorizedEventId?{authorized_event_id:broker.authorizedEventId}:{})};
|
||||
return output;
|
||||
}
|
||||
const header=v=>String(v).replace(/\\/g,'\\\\').replace(/\|/g,'\\|').replace(/[\r\n]/g,' ');
|
||||
const ext=v=>String(v).replace(/\\/g,'\\\\').replace(/=/g,'\\=').replace(/\r/g,'\\r').replace(/\n/g,'\\n');
|
||||
const severity=level=>({low:3,medium:5,high:7,critical:10}[level]??3);
|
||||
export function toCEF(event,installationId='safetynet') {
|
||||
const m=event.metadata,transport=transportAudit.canonicalTransportHostname(m.host)?transportAudit.transportFactsForEvent(event.kind,event.metadata.transport):null,transportEvent=transportAudit.TRANSPORT_KINDS.includes(event.kind),interaction=interactionAudit.interactionFactsForEvent(event.kind,event.metadata.interaction),agent=agentAudit.agentFactsForEvent(event.kind,event.metadata.agent),blocked=event.kind.includes('block')||event.kind.endsWith('.denied')||event.kind==='vault.submission-denied'||m.decision==='block',alert=event.kind==='sigma.alert';
|
||||
const broker=brokerFacts(event);
|
||||
// CEF has six custom strings, already used for rule/trace/span/type metadata.
|
||||
// A bounded, schema-projected JSON value preserves every causal UUID in one
|
||||
// standard custom string without replacing audit integrity or file facts.
|
||||
// suid identifies the verified agent capability principal, not an employee account.
|
||||
// suser, when present, remains the independently supplied human actor.
|
||||
const fields={...(transport?{cs2Label:'TransportProtocol',cs2:transport.protocol,cs3Label:'TransportPhase',cs3:transport.phase,cs6Label:'TransportRealm',cs6:transport.realm,requestContext:transport.sessionId,cs1Label:'TransportId',cs1:transport.transportId,flexNumber2Label:'ConnectionEstablished',flexNumber2:transport.connectionEstablished?1:0}:{}),...(event.kind==='transport.monitor-unavailable'&&transportAudit.TRANSPORT_MONITOR_REASONS.includes(m.ruleId)?{reason:m.ruleId}:{}),...(interaction?{cs2Label:'InteractionAction',cs2:interaction.action,cs3Label:'InteractionOutcome',cs3:interaction.outcome,requestContext:interaction.sessionId}:{}),...(agent?{cs2Label:'AgentAction',cs2:agent.action,cs3Label:'AgentScope',cs3:agent.scope,cs6Label:'AgentOutcome',cs6:agent.outcome,...(agent.sessionId?{requestContext:agent.sessionId}:{}),...(agent.principalId?{suid:agent.principalId,flexNumber1Label:'AgentCredentialGeneration',flexNumber1:agent.credentialGeneration}:{})}:{}),...(requestFacts(m,event.kind)?{cn3Label:'RequestBytes',cn3:requestFacts(m,event.kind).bytes,cs6Label:'DetectedRequestType',cs6:requestFacts(m,event.kind).detectedType}:{}),...(event.audit?{flexString1Label:'AuditRecordHash',flexString1:event.audit.hash,flexString2Label:'AuditSequence',flexString2:event.audit.sequence}:{}),externalId:`${installationId}:${event.id}`,rt:Date.parse(event.time),act:event.kind,
|
||||
@@ -40,6 +53,7 @@ export function toCEF(event,installationId='safetynet') {
|
||||
...(event.kind.startsWith('download.')&&facts(m)?{cn2Label:'FileBytes',cn2:facts(m).bytes,cs6Label:'DetectedFileType',cs6:facts(m).detectedType,fileHash:facts(m).sha256}:{}),
|
||||
...(/^[a-f0-9]{32}$/.test(m.traceId||'')?{cs4Label:'TraceId',cs4:m.traceId}:{}),
|
||||
...(/^[a-f0-9]{16}$/.test(m.spanId||'')?{cs5Label:'SpanId',cs5:m.spanId}:{}),
|
||||
...(broker?{cs2Label:'BrokerLifecycle',cs2:JSON.stringify(broker),cs3Label:'BrokerEventId',cs3:broker.eventId,requestContext:broker.requestId,reason:broker.reason,outcome:broker.phase==='request.authorized'?'unknown':['request.fault','forward.fault'].includes(broker.phase)?'failure':'success'}:{}),
|
||||
...(alert?{cs2Label:'SourceEventId',cs2:`${installationId}:${m.sourceEventId}`,cs3Label:'AlertLevel',cs3:m.level}:{})};
|
||||
return `CEF:0|SafetyNet|Enterprise Browser|0.1.0|${header(event.kind)}|${header(event.kind)}|${alert?severity(m.level):!transportEvent&&blocked?7:3}|`+Object.entries(fields).map(([k,v])=>`${k}=${ext(v)}`).join(' ');
|
||||
}
|
||||
Reference in new issue
Block a user