← back to Handbag Auth Nextjs
src/lib/security-logger.ts
277 lines
/**
* Security Logger Module
* Implements secure logging for security events and anomaly detection
* OWASP: Logging and Monitoring (https://owasp.org/www-project-top-ten/2017/A10_2017-Insufficient_Logging%26Monitoring)
*/
import fs from 'fs'
import path from 'path'
export enum SecurityEventType {
AUTH_FAILURE = 'AUTH_FAILURE',
AUTH_SUCCESS = 'AUTH_SUCCESS',
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
INVALID_INPUT = 'INVALID_INPUT',
SQL_INJECTION_ATTEMPT = 'SQL_INJECTION_ATTEMPT',
XSS_ATTEMPT = 'XSS_ATTEMPT',
UNAUTHORIZED_ACCESS = 'UNAUTHORIZED_ACCESS',
ADMIN_ACTION = 'ADMIN_ACTION',
DATA_BREACH_ATTEMPT = 'DATA_BREACH_ATTEMPT',
SUSPICIOUS_ACTIVITY = 'SUSPICIOUS_ACTIVITY',
}
export interface SecurityEvent {
timestamp: string
type: SecurityEventType
ip: string
userId?: string
endpoint: string
method: string
details: Record<string, any>
severity: 'low' | 'medium' | 'high' | 'critical'
}
class SecurityLogger {
private logFile: string
private alertThresholds = {
AUTH_FAILURE: 5, // Alert after 5 failed auth attempts
RATE_LIMIT_EXCEEDED: 10, // Alert after 10 rate limit hits
INVALID_INPUT: 20, // Alert after 20 invalid inputs
}
private eventCounts = new Map<string, Map<SecurityEventType, number>>()
constructor() {
const logDir = path.join(process.cwd(), 'logs')
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true })
}
this.logFile = path.join(logDir, `security-${new Date().toISOString().split('T')[0]}.log`)
}
/**
* Log a security event
*/
log(event: Omit<SecurityEvent, 'timestamp'>): void {
const fullEvent: SecurityEvent = {
...event,
timestamp: new Date().toISOString(),
}
// Write to file (append mode)
const logLine = JSON.stringify(fullEvent) + '\n'
fs.appendFileSync(this.logFile, logLine)
// Track event counts for anomaly detection
this.trackEventCount(event.ip, event.type)
// Check for anomalies
this.checkForAnomalies(fullEvent)
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.log(`[SECURITY] ${event.type}:`, event)
}
}
/**
* Track event counts per IP
*/
private trackEventCount(ip: string, type: SecurityEventType): void {
if (!this.eventCounts.has(ip)) {
this.eventCounts.set(ip, new Map())
}
const ipEvents = this.eventCounts.get(ip)!
const currentCount = ipEvents.get(type) || 0
ipEvents.set(type, currentCount + 1)
// Clean up old entries periodically
if (Math.random() < 0.01) {
this.cleanupOldEntries()
}
}
/**
* Check for security anomalies
*/
private checkForAnomalies(event: SecurityEvent): void {
const ipEvents = this.eventCounts.get(event.ip)
if (!ipEvents) return
// Check if IP has exceeded thresholds
for (const [eventType, threshold] of Object.entries(this.alertThresholds)) {
const count = ipEvents.get(eventType as SecurityEventType) || 0
if (count >= threshold) {
this.raiseAlert({
type: 'THRESHOLD_EXCEEDED',
ip: event.ip,
eventType,
count,
threshold,
})
}
}
// Check for suspicious patterns
this.detectSuspiciousPatterns(event, ipEvents)
}
/**
* Detect suspicious activity patterns
*/
private detectSuspiciousPatterns(
event: SecurityEvent,
ipEvents: Map<SecurityEventType, number>
): void {
const totalEvents = Array.from(ipEvents.values()).reduce((sum, count) => sum + count, 0)
// Pattern 1: Multiple different attack types from same IP
const attackTypes = [
SecurityEventType.SQL_INJECTION_ATTEMPT,
SecurityEventType.XSS_ATTEMPT,
SecurityEventType.DATA_BREACH_ATTEMPT,
]
const attackCount = attackTypes.reduce(
(sum, type) => sum + (ipEvents.get(type) || 0),
0
)
if (attackCount >= 3) {
this.raiseAlert({
type: 'MULTI_VECTOR_ATTACK',
ip: event.ip,
attackCount,
details: 'Multiple attack vectors detected from same IP',
})
}
// Pattern 2: Rapid-fire requests
if (totalEvents > 100) {
this.raiseAlert({
type: 'EXCESSIVE_REQUESTS',
ip: event.ip,
totalEvents,
details: 'Abnormally high request volume',
})
}
}
/**
* Raise security alert
*/
private raiseAlert(alert: Record<string, any>): void {
const alertEvent: SecurityEvent = {
timestamp: new Date().toISOString(),
type: SecurityEventType.SUSPICIOUS_ACTIVITY,
ip: alert.ip || 'system',
endpoint: 'security-monitor',
method: 'ALERT',
details: alert,
severity: 'high',
}
// Write to alert log
const alertLogFile = path.join(
process.cwd(),
'logs',
`security-alerts-${new Date().toISOString().split('T')[0]}.log`
)
fs.appendFileSync(alertLogFile, JSON.stringify(alertEvent) + '\n')
// In production, you would send this to a monitoring service
console.error('[SECURITY ALERT]:', alert)
// TODO: Integrate with Slack webhook or monitoring service
if (process.env.SLACK_WEBHOOK_URL) {
// Send alert to Slack
}
}
/**
* Clean up old event counts (prevent memory leak)
*/
private cleanupOldEntries(): void {
// Remove IPs with low event counts (likely legitimate users)
for (const [ip, events] of this.eventCounts.entries()) {
const total = Array.from(events.values()).reduce((sum, count) => sum + count, 0)
if (total < 3) {
this.eventCounts.delete(ip)
}
}
// Reset counts for IPs that haven't had recent activity
// This is simplified - in production, you'd track timestamps
if (this.eventCounts.size > 1000) {
this.eventCounts.clear()
}
}
/**
* Get security stats for monitoring
*/
getStats(): Record<string, any> {
const stats: Record<string, any> = {
totalIPs: this.eventCounts.size,
topOffenders: [],
eventTypeCounts: {},
}
// Calculate top offenders
const ipScores = new Map<string, number>()
for (const [ip, events] of this.eventCounts.entries()) {
const score = Array.from(events.values()).reduce((sum, count) => sum + count, 0)
ipScores.set(ip, score)
}
// Sort and get top 10
stats.topOffenders = Array.from(ipScores.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([ip, score]) => ({ ip, score }))
// Count events by type
for (const events of this.eventCounts.values()) {
for (const [type, count] of events.entries()) {
stats.eventTypeCounts[type] = (stats.eventTypeCounts[type] || 0) + count
}
}
return stats
}
}
// Export singleton instance
export const securityLogger = new SecurityLogger()
/**
* Helper function to detect SQL injection attempts
*/
export function detectSQLInjection(input: string): boolean {
const sqlPatterns = [
/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE)\b)/gi,
/(\b(OR|AND)\b\s*\d+\s*=\s*\d+)/gi,
/(--|\||;|\/\*|\*\/|@@|@|xp_|exec|execute|sp_|0x)/gi,
/(\b(CAST|CONVERT|CHAR|NCHAR|VARCHAR|NVARCHAR)\b\s*\()/gi,
]
return sqlPatterns.some(pattern => pattern.test(input))
}
/**
* Helper function to detect XSS attempts
*/
export function detectXSS(input: string): boolean {
const xssPatterns = [
/<script[^>]*>.*?<\/script>/gi,
/<iframe[^>]*>.*?<\/iframe>/gi,
/javascript:/gi,
/on\w+\s*=/gi, // Event handlers like onclick=
/<img[^>]*onerror=/gi,
/<svg[^>]*onload=/gi,
/document\.(cookie|write|location)/gi,
/window\.(location|open)/gi,
]
return xssPatterns.some(pattern => pattern.test(input))
}