feat: production readiness improvements
## Error Handling - Add GlobalErrorBoundary with error classification and recovery - Add custom error types (SecurityError, ConnectionError, TimeoutError) - Fix ErrorAlert component syntax errors ## Offline Mode - Add offlineStore for offline state management - Implement message queue with localStorage persistence - Add exponential backoff reconnection (1s→60s) - Add OfflineIndicator component with status display - Queue messages when offline, auto-retry on reconnect ## Security Hardening - Add AES-256-GCM encryption for chat history storage - Add secure API key storage with OS keychain integration - Add security audit logging system - Add XSS prevention and input validation utilities - Add rate limiting and token generation helpers ## CI/CD (Gitea Actions) - Add .gitea/workflows/ci.yml for continuous integration - Add .gitea/workflows/release.yml for release automation - Support Windows Tauri build and release ## UI Components - Add LoadingSpinner, LoadingOverlay, LoadingDots components - Add MessageSkeleton, ConversationListSkeleton skeletons - Add EmptyMessages, EmptyConversations empty states - Integrate loading states in ChatArea and ConversationList ## E2E Tests - Fix WebSocket mock for streaming response tests - Fix approval endpoint route matching - Add store state exposure for testing - All 19 core-features tests now passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
564
desktop/src/lib/security-audit.ts
Normal file
564
desktop/src/lib/security-audit.ts
Normal file
@@ -0,0 +1,564 @@
|
||||
/**
|
||||
* 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 } 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';
|
||||
|
||||
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()}_${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// Ignore persistence failures to prevent application disruption
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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',
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user