import express from 'express';
import winston from 'winston';
import nodemailer from 'nodemailer';
const app = express();
app.use(express.json());
// Configure email transporter
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER || 'your-email@gmail.com',
pass: process.env.EMAIL_PASSWORD || 'your-email-password'
}
});
// Class to handle alerts based on log patterns
class LogAlerter {
private alertThresholds: Map<string, {
count: number,
timeWindow: number,
lastAlert: number
}> = new Map();
private logger: winston.Logger;
constructor(logger: winston.Logger) {
this.logger = logger;
// Set up alert thresholds for different event types
this.alertThresholds.set('potential_ssrf_attempt', {
count: 3, // Alert after 3 attempts
timeWindow: 10 * 60 * 1000, // Within 10 minutes
lastAlert: 0
});
this.alertThresholds.set('authentication_failure', {
count: 5, // Alert after 5 failures
timeWindow: 15 * 60 * 1000, // Within 15 minutes
lastAlert: 0
});
// Set up event listener for log events
this.setupEventListener();
}
private setupEventListener() {
// Count events by type within time windows
const eventCounts: Map<string, { timestamp: number, ip: string }[]> = new Map();
this.logger.on('logged', (info) => {
if (info.security_event_type) {
const eventType = info.security_event_type;
// Initialize event count array if needed
if (!eventCounts.has(eventType)) {
eventCounts.set(eventType, []);
}
// Add event to the count
const events = eventCounts.get(eventType)!;
events.push({
timestamp: Date.now(),
ip: info.ip || 'unknown'
});
// Check if we need to send an alert
this.checkAlertThreshold(eventType, events);
}
});
}
private checkAlertThreshold(eventType: string, events: { timestamp: number, ip: string }[]) {
const threshold = this.alertThresholds.get(eventType);
if (!threshold) return;
const now = Date.now();
// Filter events within the time window
const recentEvents = events.filter(e => e.timestamp > now - threshold.timeWindow);
// Update event list to only include recent events
events.length = 0;
events.push(...recentEvents);
// Check if we've hit the threshold and it's been long enough since the last alert
if (recentEvents.length >= threshold.count &&
now - threshold.lastAlert > threshold.timeWindow) {
// Group by IP address to see if there's a pattern
const ipCounts = new Map<string, number>();
recentEvents.forEach(e => {
ipCounts.set(e.ip, (ipCounts.get(e.ip) || 0) + 1);
});
// Find the IP with the most events
let maxIP = '';
let maxCount = 0;
ipCounts.forEach((count, ip) => {
if (count > maxCount) {
maxCount = count;
maxIP = ip;
}
});
// Send the alert
this.sendAlert(eventType, recentEvents.length, maxIP, maxCount);
// Update last alert timestamp
threshold.lastAlert = now;
}
}
private sendAlert(eventType: string, totalCount: number, mainIP: string, ipCount: number) {
const subject = `SECURITY ALERT: ${this.formatEventType(eventType)}`;
const message = `
We detected ${totalCount} instances of ${this.formatEventType(eventType)} in the last few minutes.
The primary source appears to be IP: ${mainIP} (${ipCount} events).
Please investigate immediately.
`;
// Log the alert
this.logger.error(subject, { totalCount, mainIP, ipCount, eventType });
// Send email alert
transporter.sendMail({
from: process.env.EMAIL_USER || 'your-email@gmail.com',
to: process.env.ADMIN_EMAIL || 'admin@example.com',
subject,
text: message
}, (error, info) => {
if (error) {
this.logger.error(`Failed to send alert email: ${error.message}`);
} else {
this.logger.info(`Alert email sent: ${info.response}`);
}
});
}
private formatEventType(eventType: string): string {
return eventType
.replace(/_/g, ' ')
.replace(/\b\w/g, char => char.toUpperCase());
}
}
// Create logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'security.log' })
]
});
// Initialize the log alerter
const alerter = new LogAlerter(logger);
// Example route with security logging
app.post('/api/fetch', (req, res) => {
const { url } = req.body;
if (url && isSuspiciousUrl(url)) {
logger.warn('Potential SSRF attempt detected', {
security_event_type: 'potential_ssrf_attempt',
ip: req.ip,
url,
userAgent: req.headers['user-agent']
});
return res.status(403).json({ error: 'Access denied' });
}
res.json({ success: true });
});
function isSuspiciousUrl(url: string): boolean {
return url.includes('internal') || url.includes('localhost');
}
app.listen(3000, () => {
logger.info('Server running on port 3000');
});