Files
zclaw_openfang/desktop/src/lib/security-audit.ts
iven 185763868a 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>
2026-03-22 00:03:22 +08:00

565 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 } 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;
}