172 lines
5.0 KiB
TypeScript
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,
|
|
}
|
|
}
|