mission-control/src/lib/security-events.ts

172 lines
5.0 KiB
TypeScript

/**
* Security Events — structured security event logging and agent trust scoring.
*
* Persists events to the security_events table and broadcasts via the event bus.
* Trust scores are recalculated on each security event using weighted factors.
*/
import { getDatabase } from '@/lib/db'
import { eventBus, type EventType } from '@/lib/event-bus'
import { logger } from '@/lib/logger'
export type SecuritySeverity = 'info' | 'warning' | 'critical'
export interface SecurityEvent {
event_type: string
severity?: SecuritySeverity
source?: string
agent_name?: string
detail?: string
ip_address?: string
workspace_id?: number
tenant_id?: number
}
export interface SecurityPosture {
score: number
totalEvents: number
criticalEvents: number
warningEvents: number
avgTrustScore: number
recentIncidents: number
}
const TRUST_WEIGHTS: Record<string, { field: string; delta: number }> = {
'auth.failure': { field: 'auth_failures', delta: -0.05 },
'injection.attempt': { field: 'injection_attempts', delta: -0.15 },
'rate_limit.hit': { field: 'rate_limit_hits', delta: -0.03 },
'secret.exposure': { field: 'secret_exposures', delta: -0.20 },
'task.success': { field: 'successful_tasks', delta: 0.02 },
'task.failure': { field: 'failed_tasks', delta: -0.01 },
}
export function logSecurityEvent(event: SecurityEvent): number {
const db = getDatabase()
const severity = event.severity ?? 'info'
const workspaceId = event.workspace_id ?? 1
const tenantId = event.tenant_id ?? 1
const result = db.prepare(`
INSERT INTO security_events (event_type, severity, source, agent_name, detail, ip_address, workspace_id, tenant_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
event.event_type,
severity,
event.source ?? null,
event.agent_name ?? null,
event.detail ?? null,
event.ip_address ?? null,
workspaceId,
tenantId,
)
const id = result.lastInsertRowid as number
eventBus.broadcast('security.event' as EventType, {
id,
...event,
severity,
workspace_id: workspaceId,
timestamp: Math.floor(Date.now() / 1000),
})
return id
}
export function updateAgentTrustScore(
agentName: string,
eventType: string,
workspaceId: number = 1,
): void {
const db = getDatabase()
const weight = TRUST_WEIGHTS[eventType]
// Ensure row exists
db.prepare(`
INSERT OR IGNORE INTO agent_trust_scores (agent_name, workspace_id)
VALUES (?, ?)
`).run(agentName, workspaceId)
if (weight) {
// Increment the counter field
db.prepare(`
UPDATE agent_trust_scores
SET ${weight.field} = ${weight.field} + 1,
updated_at = unixepoch()
WHERE agent_name = ? AND workspace_id = ?
`).run(agentName, workspaceId)
// Recalculate trust score (clamped 0..1)
const row = db.prepare(`
SELECT * FROM agent_trust_scores WHERE agent_name = ? AND workspace_id = ?
`).get(agentName, workspaceId) as any
if (row) {
let score = 1.0
score += (row.auth_failures || 0) * -0.05
score += (row.injection_attempts || 0) * -0.15
score += (row.rate_limit_hits || 0) * -0.03
score += (row.secret_exposures || 0) * -0.20
score += (row.successful_tasks || 0) * 0.02
score += (row.failed_tasks || 0) * -0.01
score = Math.max(0, Math.min(1, score))
const isAnomaly = weight.delta < 0
db.prepare(`
UPDATE agent_trust_scores
SET trust_score = ?,
last_anomaly_at = CASE WHEN ? THEN unixepoch() ELSE last_anomaly_at END,
updated_at = unixepoch()
WHERE agent_name = ? AND workspace_id = ?
`).run(score, isAnomaly ? 1 : 0, agentName, workspaceId)
}
}
}
export function getSecurityPosture(workspaceId: number = 1): SecurityPosture {
const db = getDatabase()
const oneDayAgo = Math.floor(Date.now() / 1000) - 86400
const totals = db.prepare(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN severity = 'critical' THEN 1 ELSE 0 END) as critical,
SUM(CASE WHEN severity = 'warning' THEN 1 ELSE 0 END) as warning
FROM security_events
WHERE workspace_id = ?
`).get(workspaceId) as any
const recent = db.prepare(`
SELECT COUNT(*) as count
FROM security_events
WHERE workspace_id = ? AND severity IN ('warning', 'critical') AND created_at > ?
`).get(workspaceId, oneDayAgo) as any
const trustAvg = db.prepare(`
SELECT AVG(trust_score) as avg_trust
FROM agent_trust_scores
WHERE workspace_id = ?
`).get(workspaceId) as any
const avgTrust = trustAvg?.avg_trust ?? 1.0
const criticalCount = totals?.critical ?? 0
const warningCount = totals?.warning ?? 0
const recentCount = recent?.count ?? 0
// Score: start at 100, deduct for incidents
let score = 100
score -= criticalCount * 10
score -= warningCount * 3
score -= recentCount * 2
score = Math.round(Math.max(0, Math.min(100, score * avgTrust)))
return {
score,
totalEvents: totals?.total ?? 0,
criticalEvents: criticalCount,
warningEvents: warningCount,
avgTrustScore: Math.round(avgTrust * 100) / 100,
recentIncidents: recentCount,
}
}