@ordojs/security
Version:
Security package for OrdoJS with XSS, CSRF, and injection protection
360 lines (315 loc) • 9.55 kB
text/typescript
export interface SecurityEvent {
id: string;
type: 'xss_attempt' | 'csrf_violation' | 'injection_attempt' | 'rate_limit_exceeded' | 'auth_failure' | 'suspicious_activity';
severity: 'low' | 'medium' | 'high' | 'critical';
timestamp: Date;
source: {
ip?: string;
userAgent?: string;
userId?: string;
sessionId?: string;
};
details: Record<string, any>;
blocked: boolean;
}
export interface SecurityMetrics {
totalEvents: number;
blockedEvents: number;
eventsByType: Record<string, number>;
eventsBySeverity: Record<string, number>;
topSources: Array<{ ip: string; count: number }>;
timeRange: {
start: Date;
end: Date;
};
}
export interface RuntimeMonitorOptions {
enableLogging?: boolean;
logLevel?: 'debug' | 'info' | 'warn' | 'error';
maxEvents?: number;
alertThresholds?: {
critical: number;
high: number;
medium: number;
};
onAlert?: (event: SecurityEvent) => void;
onMetricsUpdate?: (metrics: SecurityMetrics) => void;
}
export class RuntimeSecurityMonitor {
private events: SecurityEvent[] = [];
private options: RuntimeMonitorOptions;
private alertCounts: Record<string, number> = {};
private startTime: Date;
constructor(options: RuntimeMonitorOptions = {}) {
this.options = {
enableLogging: true,
logLevel: 'warn',
maxEvents: 10000,
alertThresholds: {
critical: 1,
high: 5,
medium: 10,
},
...options,
};
this.startTime = new Date();
}
recordEvent(event: Omit<SecurityEvent, 'id' | 'timestamp'>): void {
const securityEvent: SecurityEvent = {
id: this.generateEventId(),
timestamp: new Date(),
...event,
};
this.events.push(securityEvent);
// Maintain max events limit
if (this.events.length > this.options.maxEvents!) {
this.events.shift();
}
// Log event if enabled
if (this.options.enableLogging) {
this.logEvent(securityEvent);
}
// Check alert thresholds
this.checkAlertThresholds(securityEvent);
// Update metrics
if (this.options.onMetricsUpdate) {
this.options.onMetricsUpdate(this.getMetrics());
}
}
recordXSSAttempt(details: {
payload: string;
source: SecurityEvent['source'];
blocked: boolean;
context?: string;
}): void {
this.recordEvent({
type: 'xss_attempt',
severity: 'high',
source: details.source,
details: {
payload: details.payload,
context: details.context,
},
blocked: details.blocked,
});
}
recordCSRFViolation(details: {
expectedToken?: string;
receivedToken?: string;
source: SecurityEvent['source'];
endpoint: string;
}): void {
this.recordEvent({
type: 'csrf_violation',
severity: 'high',
source: details.source,
details: {
endpoint: details.endpoint,
hasExpectedToken: !!details.expectedToken,
hasReceivedToken: !!details.receivedToken,
},
blocked: true,
});
}
recordInjectionAttempt(details: {
type: 'sql' | 'nosql' | 'ldap' | 'command';
payload: string;
source: SecurityEvent['source'];
blocked: boolean;
query?: string;
}): void {
this.recordEvent({
type: 'injection_attempt',
severity: 'critical',
source: details.source,
details: {
injectionType: details.type,
payload: details.payload,
query: details.query,
},
blocked: details.blocked,
});
}
recordRateLimitExceeded(details: {
limit: number;
current: number;
window: string;
source: SecurityEvent['source'];
endpoint?: string;
}): void {
this.recordEvent({
type: 'rate_limit_exceeded',
severity: 'medium',
source: details.source,
details: {
limit: details.limit,
current: details.current,
window: details.window,
endpoint: details.endpoint,
},
blocked: true,
});
}
recordAuthFailure(details: {
reason: 'invalid_credentials' | 'account_locked' | 'token_expired' | 'insufficient_permissions';
source: SecurityEvent['source'];
username?: string;
endpoint?: string;
}): void {
this.recordEvent({
type: 'auth_failure',
severity: details.reason === 'insufficient_permissions' ? 'medium' : 'high',
source: details.source,
details: {
reason: details.reason,
username: details.username,
endpoint: details.endpoint,
},
blocked: true,
});
}
recordSuspiciousActivity(details: {
activity: string;
riskScore: number;
source: SecurityEvent['source'];
context?: Record<string, any>;
}): void {
const severity = details.riskScore >= 80 ? 'critical' :
details.riskScore >= 60 ? 'high' :
details.riskScore >= 40 ? 'medium' : 'low';
this.recordEvent({
type: 'suspicious_activity',
severity,
source: details.source,
details: {
activity: details.activity,
riskScore: details.riskScore,
context: details.context,
},
blocked: details.riskScore >= 80,
});
}
getMetrics(): SecurityMetrics {
const now = new Date();
const eventsByType: Record<string, number> = {};
const eventsBySeverity: Record<string, number> = {};
const sourceIpCounts: Record<string, number> = {};
let totalEvents = 0;
let blockedEvents = 0;
this.events.forEach(event => {
totalEvents++;
if (event.blocked) blockedEvents++;
eventsByType[event.type] = (eventsByType[event.type] || 0) + 1;
eventsBySeverity[event.severity] = (eventsBySeverity[event.severity] || 0) + 1;
if (event.source.ip) {
sourceIpCounts[event.source.ip] = (sourceIpCounts[event.source.ip] || 0) + 1;
}
});
const topSources = Object.entries(sourceIpCounts)
.map(([ip, count]) => ({ ip, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
return {
totalEvents,
blockedEvents,
eventsByType,
eventsBySeverity,
topSources,
timeRange: {
start: this.startTime,
end: now,
},
};
}
getEvents(filter?: {
type?: SecurityEvent['type'];
severity?: SecurityEvent['severity'];
since?: Date;
limit?: number;
}): SecurityEvent[] {
let filteredEvents = [...this.events];
if (filter?.type) {
filteredEvents = filteredEvents.filter(e => e.type === filter.type);
}
if (filter?.severity) {
filteredEvents = filteredEvents.filter(e => e.severity === filter.severity);
}
if (filter?.since) {
filteredEvents = filteredEvents.filter(e => e.timestamp >= filter.since!);
}
if (filter?.limit) {
filteredEvents = filteredEvents.slice(-filter.limit);
}
return filteredEvents.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}
clearEvents(): void {
this.events = [];
this.alertCounts = {};
}
exportEvents(format: 'json' | 'csv' = 'json'): string {
if (format === 'csv') {
const headers = ['id', 'type', 'severity', 'timestamp', 'blocked', 'source_ip', 'user_agent', 'details'];
const rows = this.events.map(event => [
event.id,
event.type,
event.severity,
event.timestamp.toISOString(),
event.blocked.toString(),
event.source.ip || '',
event.source.userAgent || '',
JSON.stringify(event.details),
]);
return [headers, ...rows].map(row => row.join(',')).join('\n');
}
return JSON.stringify(this.events, null, 2);
}
private generateEventId(): string {
return `sec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private logEvent(event: SecurityEvent): void {
const logLevel = this.getLogLevel(event.severity);
if (!this.shouldLog(logLevel)) return;
const message = `Security Event [${event.type}]: ${event.severity.toUpperCase()} - ${JSON.stringify(event.details)}`;
switch (logLevel) {
case 'error':
console.error(message);
break;
case 'warn':
console.warn(message);
break;
case 'info':
console.info(message);
break;
case 'debug':
console.debug(message);
break;
}
}
private getLogLevel(severity: SecurityEvent['severity']): 'debug' | 'info' | 'warn' | 'error' {
switch (severity) {
case 'critical': return 'error';
case 'high': return 'error';
case 'medium': return 'warn';
case 'low': return 'info';
default: return 'debug';
}
}
private shouldLog(level: 'debug' | 'info' | 'warn' | 'error'): boolean {
const levels = ['debug', 'info', 'warn', 'error'];
const currentLevelIndex = levels.indexOf(this.options.logLevel!);
const eventLevelIndex = levels.indexOf(level);
return eventLevelIndex >= currentLevelIndex;
}
private checkAlertThresholds(event: SecurityEvent): void {
const thresholds = this.options.alertThresholds!;
const threshold = event.severity === 'low' ? undefined : thresholds[event.severity as keyof typeof thresholds];
if (!threshold) return;
const key = `${event.severity}_${event.type}`;
this.alertCounts[key] = (this.alertCounts[key] || 0) + 1;
if (this.alertCounts[key] >= threshold && this.options.onAlert) {
this.options.onAlert(event);
}
}
}
// Singleton instance for global use
export const runtimeMonitor = new RuntimeSecurityMonitor();