Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
P2-24: Add content_hash column to memories table with index. Before INSERT, check for existing entry with same normalized content hash within agent scope; merge importance and bump access_count. P2-25: Add hand_executed/hand_approved/hand_denied/skill_executed event types to security-audit.ts. Insert audit logging calls in kernel-hands.ts triggerHand/approveHand and kernel-skills.ts executeSkill execution paths. P3-02: SceneRenderer now imports WhiteboardCanvas component instead of inline SVG rendering, gaining chart/latex support. Deleted 27 lines of duplicated renderWhiteboardItem code. Update DEFECT_LIST.md: P1-01 ✅ (Fantoccini confirmed), P3-02 ✅, add P2-24/P2-25 entries. Active count: 48→50 fixed, 3→1 remaining.
581 lines
15 KiB
TypeScript
581 lines
15 KiB
TypeScript
/**
|
|
* Security Audit Logging Module
|
|
*
|
|
* Provides comprehensive security event logging for ZCLAW application.
|
|
* All security-relevant events are logged with timestamps and details.
|
|
*
|
|
* Security events logged:
|
|
* - Authentication events (login, logout, failed attempts)
|
|
* - API key operations (access, rotation, deletion)
|
|
* - Data access events (encrypted data read/write)
|
|
* - Security violations (failed decryption, tampering attempts)
|
|
* - Configuration changes
|
|
*/
|
|
|
|
import { hashSha256, generateRandomString } from './crypto-utils';
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
export type SecurityEventType =
|
|
| 'auth_login'
|
|
| 'auth_logout'
|
|
| 'auth_failed'
|
|
| 'auth_token_refresh'
|
|
| 'key_accessed'
|
|
| 'key_stored'
|
|
| 'key_deleted'
|
|
| 'key_rotated'
|
|
| 'data_encrypted'
|
|
| 'data_decrypted'
|
|
| 'data_access'
|
|
| 'data_export'
|
|
| 'data_import'
|
|
| 'security_violation'
|
|
| 'decryption_failed'
|
|
| 'integrity_check_failed'
|
|
| 'config_changed'
|
|
| 'permission_granted'
|
|
| 'permission_denied'
|
|
| 'session_started'
|
|
| 'session_ended'
|
|
| 'rate_limit_exceeded'
|
|
| 'suspicious_activity'
|
|
| 'hand_executed'
|
|
| 'hand_approved'
|
|
| 'hand_denied'
|
|
| 'skill_executed';
|
|
|
|
export type SecurityEventSeverity = 'info' | 'warning' | 'error' | 'critical';
|
|
|
|
export interface SecurityEvent {
|
|
id: string;
|
|
type: SecurityEventType;
|
|
severity: SecurityEventSeverity;
|
|
timestamp: string;
|
|
message: string;
|
|
details: Record<string, unknown>;
|
|
userAgent?: string;
|
|
ip?: string;
|
|
sessionId?: string;
|
|
agentId?: string;
|
|
}
|
|
|
|
export interface SecurityAuditReport {
|
|
generatedAt: string;
|
|
totalEvents: number;
|
|
eventsByType: Record<SecurityEventType, number>;
|
|
eventsBySeverity: Record<SecurityEventSeverity, number>;
|
|
recentCriticalEvents: SecurityEvent[];
|
|
recommendations: string[];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Constants
|
|
// ============================================================================
|
|
|
|
const SECURITY_LOG_KEY = 'zclaw_security_audit_log';
|
|
const MAX_LOG_ENTRIES = 2000;
|
|
const AUDIT_VERSION = 1;
|
|
|
|
// ============================================================================
|
|
// Internal State
|
|
// ============================================================================
|
|
|
|
let isAuditEnabled: boolean = true;
|
|
let currentSessionId: string | null = null;
|
|
|
|
// ============================================================================
|
|
// Core Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Generate a unique event ID
|
|
*/
|
|
function generateEventId(): string {
|
|
return `evt_${Date.now()}_${generateRandomString(8)}`;
|
|
}
|
|
|
|
/**
|
|
* Get the current session ID
|
|
*/
|
|
export function getCurrentSessionId(): string | null {
|
|
return currentSessionId;
|
|
}
|
|
|
|
/**
|
|
* Set the current session ID
|
|
*/
|
|
export function setCurrentSessionId(sessionId: string | null): void {
|
|
currentSessionId = sessionId;
|
|
}
|
|
|
|
/**
|
|
* Enable or disable audit logging
|
|
*/
|
|
export function setAuditEnabled(enabled: boolean): void {
|
|
isAuditEnabled = enabled;
|
|
logSecurityEventInternal('config_changed', 'info', `Audit logging ${enabled ? 'enabled' : 'disabled'}`, {});
|
|
}
|
|
|
|
/**
|
|
* Check if audit logging is enabled
|
|
*/
|
|
export function isAuditEnabledState(): boolean {
|
|
return isAuditEnabled;
|
|
}
|
|
|
|
/**
|
|
* Internal function to persist security events
|
|
*/
|
|
function persistEvent(event: SecurityEvent): void {
|
|
try {
|
|
const events = getStoredEvents();
|
|
events.push(event);
|
|
|
|
// Trim old entries if needed
|
|
if (events.length > MAX_LOG_ENTRIES) {
|
|
events.splice(0, events.length - MAX_LOG_ENTRIES);
|
|
}
|
|
|
|
localStorage.setItem(SECURITY_LOG_KEY, JSON.stringify(events));
|
|
} catch (e) {
|
|
// Ignore persistence failures to prevent application disruption
|
|
// eslint-disable-next-line no-console
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.warn('[SecurityAudit] Failed to persist security event', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get stored security events
|
|
*/
|
|
function getStoredEvents(): SecurityEvent[] {
|
|
try {
|
|
const stored = localStorage.getItem(SECURITY_LOG_KEY);
|
|
if (!stored) return [];
|
|
return JSON.parse(stored) as SecurityEvent[];
|
|
} catch (e) {
|
|
// eslint-disable-next-line no-console
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.warn('[SecurityAudit] Failed to read security events', e);
|
|
}
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine severity based on event type
|
|
*/
|
|
function getDefaultSeverity(type: SecurityEventType): SecurityEventSeverity {
|
|
const severityMap: Record<SecurityEventType, SecurityEventSeverity> = {
|
|
auth_login: 'info',
|
|
auth_logout: 'info',
|
|
auth_failed: 'warning',
|
|
auth_token_refresh: 'info',
|
|
key_accessed: 'info',
|
|
key_stored: 'info',
|
|
key_deleted: 'warning',
|
|
key_rotated: 'info',
|
|
data_encrypted: 'info',
|
|
data_decrypted: 'info',
|
|
data_access: 'info',
|
|
data_export: 'warning',
|
|
data_import: 'warning',
|
|
security_violation: 'critical',
|
|
decryption_failed: 'error',
|
|
integrity_check_failed: 'critical',
|
|
config_changed: 'warning',
|
|
permission_granted: 'info',
|
|
permission_denied: 'warning',
|
|
session_started: 'info',
|
|
session_ended: 'info',
|
|
rate_limit_exceeded: 'warning',
|
|
suspicious_activity: 'critical',
|
|
hand_executed: 'info',
|
|
hand_approved: 'info',
|
|
hand_denied: 'warning',
|
|
skill_executed: 'info',
|
|
};
|
|
|
|
return severityMap[type] || 'info';
|
|
}
|
|
|
|
/**
|
|
* Internal function to log security events
|
|
*/
|
|
function logSecurityEventInternal(
|
|
type: SecurityEventType,
|
|
severity: SecurityEventSeverity,
|
|
message: string,
|
|
details: Record<string, unknown>
|
|
): void {
|
|
if (!isAuditEnabled && type !== 'config_changed') {
|
|
return;
|
|
}
|
|
|
|
const event: SecurityEvent = {
|
|
id: generateEventId(),
|
|
type,
|
|
severity,
|
|
timestamp: new Date().toISOString(),
|
|
message,
|
|
details,
|
|
sessionId: currentSessionId || undefined,
|
|
};
|
|
|
|
// Add user agent if in browser
|
|
if (typeof navigator !== 'undefined') {
|
|
event.userAgent = navigator.userAgent;
|
|
}
|
|
|
|
persistEvent(event);
|
|
|
|
// Log to console for development
|
|
if (process.env.NODE_ENV === 'development') {
|
|
const logMethod = severity === 'critical' || severity === 'error' ? 'error' :
|
|
severity === 'warning' ? 'warn' : 'log';
|
|
console[logMethod](`[SecurityAudit] ${type}: ${message}`, details);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Public API
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Log a security event
|
|
*/
|
|
export function logSecurityEvent(
|
|
type: SecurityEventType,
|
|
message: string,
|
|
details: Record<string, unknown> = {},
|
|
severity?: SecurityEventSeverity
|
|
): void {
|
|
const eventSeverity = severity || getDefaultSeverity(type);
|
|
logSecurityEventInternal(type, eventSeverity, message, details);
|
|
}
|
|
|
|
/**
|
|
* Log authentication event
|
|
*/
|
|
export function logAuthEvent(
|
|
type: 'auth_login' | 'auth_logout' | 'auth_failed' | 'auth_token_refresh',
|
|
message: string,
|
|
details: Record<string, unknown> = {}
|
|
): void {
|
|
logSecurityEvent(type, message, details);
|
|
}
|
|
|
|
/**
|
|
* Log key management event
|
|
*/
|
|
export function logKeyEvent(
|
|
type: 'key_accessed' | 'key_stored' | 'key_deleted' | 'key_rotated',
|
|
message: string,
|
|
details: Record<string, unknown> = {}
|
|
): void {
|
|
logSecurityEvent(type, message, details);
|
|
}
|
|
|
|
/**
|
|
* Log data access event
|
|
*/
|
|
export function logDataEvent(
|
|
type: 'data_encrypted' | 'data_decrypted' | 'data_access' | 'data_export' | 'data_import',
|
|
message: string,
|
|
details: Record<string, unknown> = {}
|
|
): void {
|
|
logSecurityEvent(type, message, details);
|
|
}
|
|
|
|
/**
|
|
* Log security violation
|
|
*/
|
|
export function logSecurityViolation(
|
|
message: string,
|
|
details: Record<string, unknown> = {}
|
|
): void {
|
|
logSecurityEvent('security_violation', message, details, 'critical');
|
|
}
|
|
|
|
/**
|
|
* Log decryption failure
|
|
*/
|
|
export function logDecryptionFailure(
|
|
message: string,
|
|
details: Record<string, unknown> = {}
|
|
): void {
|
|
logSecurityEvent('decryption_failed', message, details, 'error');
|
|
}
|
|
|
|
/**
|
|
* Log integrity check failure
|
|
*/
|
|
export function logIntegrityFailure(
|
|
message: string,
|
|
details: Record<string, unknown> = {}
|
|
): void {
|
|
logSecurityEvent('integrity_check_failed', message, details, 'critical');
|
|
}
|
|
|
|
/**
|
|
* Log permission event
|
|
*/
|
|
export function logPermissionEvent(
|
|
type: 'permission_granted' | 'permission_denied',
|
|
message: string,
|
|
details: Record<string, unknown> = {}
|
|
): void {
|
|
logSecurityEvent(type, message, details);
|
|
}
|
|
|
|
/**
|
|
* Log session event
|
|
*/
|
|
export function logSessionEvent(
|
|
type: 'session_started' | 'session_ended',
|
|
message: string,
|
|
details: Record<string, unknown> = {}
|
|
): void {
|
|
logSecurityEvent(type, message, details);
|
|
}
|
|
|
|
/**
|
|
* Log suspicious activity
|
|
*/
|
|
export function logSuspiciousActivity(
|
|
message: string,
|
|
details: Record<string, unknown> = {}
|
|
): void {
|
|
logSecurityEvent('suspicious_activity', message, details, 'critical');
|
|
}
|
|
|
|
/**
|
|
* Log rate limit event
|
|
*/
|
|
export function logRateLimitEvent(
|
|
message: string,
|
|
details: Record<string, unknown> = {}
|
|
): void {
|
|
logSecurityEvent('rate_limit_exceeded', message, details, 'warning');
|
|
}
|
|
|
|
// ============================================================================
|
|
// Query Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get all security events
|
|
*/
|
|
export function getSecurityEvents(): SecurityEvent[] {
|
|
return getStoredEvents();
|
|
}
|
|
|
|
/**
|
|
* Get security events by type
|
|
*/
|
|
export function getSecurityEventsByType(type: SecurityEventType): SecurityEvent[] {
|
|
return getStoredEvents().filter(event => event.type === type);
|
|
}
|
|
|
|
/**
|
|
* Get security events by severity
|
|
*/
|
|
export function getSecurityEventsBySeverity(severity: SecurityEventSeverity): SecurityEvent[] {
|
|
return getStoredEvents().filter(event => event.severity === severity);
|
|
}
|
|
|
|
/**
|
|
* Get security events within a time range
|
|
*/
|
|
export function getSecurityEventsByTimeRange(start: Date, end: Date): SecurityEvent[] {
|
|
const startTime = start.getTime();
|
|
const endTime = end.getTime();
|
|
|
|
return getStoredEvents().filter(event => {
|
|
const eventTime = new Date(event.timestamp).getTime();
|
|
return eventTime >= startTime && eventTime <= endTime;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get recent critical events
|
|
*/
|
|
export function getRecentCriticalEvents(count: number = 10): SecurityEvent[] {
|
|
return getStoredEvents()
|
|
.filter(event => event.severity === 'critical' || event.severity === 'error')
|
|
.slice(-count);
|
|
}
|
|
|
|
/**
|
|
* Get events for a specific session
|
|
*/
|
|
export function getSecurityEventsBySession(sessionId: string): SecurityEvent[] {
|
|
return getStoredEvents().filter(event => event.sessionId === sessionId);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Report Generation
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Generate a security audit report
|
|
*/
|
|
export function generateSecurityAuditReport(): SecurityAuditReport {
|
|
const events = getStoredEvents();
|
|
|
|
const eventsByType = Object.create(null) as Record<SecurityEventType, number>;
|
|
const eventsBySeverity: Record<SecurityEventSeverity, number> = {
|
|
info: 0,
|
|
warning: 0,
|
|
error: 0,
|
|
critical: 0,
|
|
};
|
|
|
|
for (const event of events) {
|
|
eventsByType[event.type] = (eventsByType[event.type] || 0) + 1;
|
|
eventsBySeverity[event.severity]++;
|
|
}
|
|
|
|
const recentCriticalEvents = getRecentCriticalEvents(10);
|
|
|
|
const recommendations: string[] = [];
|
|
|
|
// Generate recommendations based on findings
|
|
if (eventsBySeverity.critical > 0) {
|
|
recommendations.push('Investigate critical security events immediately');
|
|
}
|
|
|
|
if ((eventsByType.auth_failed || 0) > 5) {
|
|
recommendations.push('Multiple failed authentication attempts detected - consider rate limiting');
|
|
}
|
|
|
|
if ((eventsByType.decryption_failed || 0) > 3) {
|
|
recommendations.push('Multiple decryption failures - check key integrity');
|
|
}
|
|
|
|
if ((eventsByType.suspicious_activity || 0) > 0) {
|
|
recommendations.push('Suspicious activity detected - review access logs');
|
|
}
|
|
|
|
if (events.length === 0) {
|
|
recommendations.push('No security events recorded - ensure audit logging is enabled');
|
|
}
|
|
|
|
return {
|
|
generatedAt: new Date().toISOString(),
|
|
totalEvents: events.length,
|
|
eventsByType,
|
|
eventsBySeverity,
|
|
recentCriticalEvents,
|
|
recommendations,
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Maintenance Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Clear all security events
|
|
*/
|
|
export function clearSecurityAuditLog(): void {
|
|
localStorage.removeItem(SECURITY_LOG_KEY);
|
|
logSecurityEventInternal('config_changed', 'warning', 'Security audit log cleared', {});
|
|
}
|
|
|
|
/**
|
|
* Export security events for external analysis
|
|
*/
|
|
export function exportSecurityEvents(): string {
|
|
const events = getStoredEvents();
|
|
return JSON.stringify({
|
|
version: AUDIT_VERSION,
|
|
exportedAt: new Date().toISOString(),
|
|
events,
|
|
}, null, 2);
|
|
}
|
|
|
|
/**
|
|
* Import security events from external source
|
|
*/
|
|
export function importSecurityEvents(jsonData: string, merge: boolean = false): void {
|
|
try {
|
|
const data = JSON.parse(jsonData);
|
|
const importedEvents = data.events as SecurityEvent[];
|
|
|
|
if (!importedEvents || !Array.isArray(importedEvents)) {
|
|
throw new Error('Invalid import data format');
|
|
}
|
|
|
|
if (merge) {
|
|
const existingEvents = getStoredEvents();
|
|
const mergedEvents = [...existingEvents, ...importedEvents];
|
|
localStorage.setItem(SECURITY_LOG_KEY, JSON.stringify(mergedEvents.slice(-MAX_LOG_ENTRIES)));
|
|
} else {
|
|
localStorage.setItem(SECURITY_LOG_KEY, JSON.stringify(importedEvents.slice(-MAX_LOG_ENTRIES)));
|
|
}
|
|
|
|
logSecurityEventInternal('data_import', 'warning', `Imported ${importedEvents.length} security events`, {
|
|
merge,
|
|
sourceVersion: data.version,
|
|
});
|
|
} catch (error) {
|
|
logSecurityEventInternal('security_violation', 'error', 'Failed to import security events', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify audit log integrity
|
|
*/
|
|
export async function verifyAuditLogIntegrity(): Promise<{
|
|
valid: boolean;
|
|
eventCount: number;
|
|
hash: string;
|
|
}> {
|
|
const events = getStoredEvents();
|
|
const data = JSON.stringify(events);
|
|
const hash = await hashSha256(data);
|
|
|
|
return {
|
|
valid: events.length > 0,
|
|
eventCount: events.length,
|
|
hash,
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Initialization
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Initialize the security audit module
|
|
*/
|
|
export function initializeSecurityAudit(sessionId?: string): void {
|
|
if (sessionId) {
|
|
currentSessionId = sessionId;
|
|
}
|
|
|
|
logSecurityEventInternal('session_started', 'info', 'Security audit session started', {
|
|
sessionId: currentSessionId,
|
|
auditEnabled: isAuditEnabled,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Shutdown the security audit module
|
|
*/
|
|
export function shutdownSecurityAudit(): void {
|
|
logSecurityEventInternal('session_ended', 'info', 'Security audit session ended', {
|
|
sessionId: currentSessionId,
|
|
});
|
|
|
|
currentSessionId = null;
|
|
}
|