Files
drone-detector/server/services/alertService.js
2025-08-18 05:42:33 +02:00

625 lines
21 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const twilio = require('twilio');
const { AlertRule, AlertLog, User, Device } = require('../models');
const { Op } = require('sequelize');
class AlertService {
constructor() {
this.twilioClient = null;
this.twilioPhone = null;
this.twilioEnabled = false;
this.activeAlerts = new Map(); // Track active alerts for clear notifications
this.initializeTwilio();
}
// RSSI-based threat assessment for security installations
assessThreatLevel(rssi, droneType) {
// RSSI typically ranges from -30 (very close) to -100 (very far)
// For 15km range detection, we need to establish threat zones for sensitive facilities
let threatLevel = 'low';
let estimatedDistance = 0;
let description = '';
let actionRequired = false;
// Convert RSSI to estimated distance (rough calculation)
// Formula: Distance (m) = 10^((RSSI_at_1m - RSSI) / (10 * n))
// Where n = path loss exponent (typically 2-4 for outdoor environments)
const rssiAt1m = -30; // Typical RSSI at 1 meter
const pathLossExponent = 3; // Outdoor environment with obstacles
estimatedDistance = Math.pow(10, (rssiAt1m - rssi) / (10 * pathLossExponent));
// Threat level assessment based on distance zones for sensitive facilities
if (rssi >= -40) {
// Very close: 0-50 meters - CRITICAL THREAT
threatLevel = 'critical';
description = 'IMMEDIATE THREAT: Drone within security perimeter (0-50m)';
actionRequired = true;
} else if (rssi >= -55) {
// Close: 50-200 meters - HIGH THREAT
threatLevel = 'high';
description = 'HIGH THREAT: Drone approaching facility (50-200m)';
actionRequired = true;
} else if (rssi >= -70) {
// Medium: 200-1000 meters - MEDIUM THREAT
threatLevel = 'medium';
description = 'MEDIUM THREAT: Drone in facility vicinity (200m-1km)';
actionRequired = false;
} else if (rssi >= -85) {
// Far: 1-5 kilometers - LOW THREAT
threatLevel = 'low';
description = 'LOW THREAT: Drone detected at distance (1-5km)';
actionRequired = false;
} else {
// Very far: 5-15 kilometers - MONITORING ONLY
threatLevel = 'monitoring';
description = 'MONITORING: Drone detected at long range (5-15km)';
actionRequired = false;
}
// Adjust threat level based on drone type (if classified)
const droneTypes = {
0: 'Consumer/Hobby',
1: 'Professional/Military',
2: 'Racing/High-speed',
3: 'Unknown/Custom'
};
if (droneType === 1) {
// Military/Professional drone - escalate threat
if (threatLevel === 'low') threatLevel = 'medium';
if (threatLevel === 'medium') threatLevel = 'high';
if (threatLevel === 'high') threatLevel = 'critical';
description += ' - PROFESSIONAL/MILITARY DRONE DETECTED';
actionRequired = true;
} else if (droneType === 2) {
// Racing/Fast drone - escalate if close
if (rssi >= -55 && threatLevel !== 'critical') {
threatLevel = 'high';
description += ' - HIGH-SPEED DRONE DETECTED';
actionRequired = true;
}
}
return {
level: threatLevel,
estimatedDistance: Math.round(estimatedDistance),
rssi,
droneType: droneTypes[droneType] || 'Unknown',
description,
requiresImmediateAction: actionRequired,
priority: threatLevel === 'critical' ? 1 : threatLevel === 'high' ? 2 : threatLevel === 'medium' ? 3 : 4
};
}
initializeTwilio() {
// Check if Twilio credentials are provided
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const phoneNumber = process.env.TWILIO_PHONE_NUMBER;
// If any Twilio credential is missing, disable SMS functionality
if (!accountSid || !authToken || !phoneNumber ||
accountSid.trim() === '' || authToken.trim() === '' || phoneNumber.trim() === '') {
console.log('📱 Twilio credentials not configured - SMS alerts disabled');
console.log(' To enable SMS alerts, set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER');
this.twilioEnabled = false;
this.twilioClient = null;
return;
}
// Validate Twilio Account SID format
if (!accountSid.startsWith('AC')) {
console.log('⚠️ Invalid Twilio Account SID format - SMS alerts disabled');
console.log(' Account SID must start with "AC"');
this.twilioEnabled = false;
this.twilioClient = null;
return;
}
try {
this.twilioClient = twilio(accountSid, authToken);
this.twilioPhone = phoneNumber;
this.twilioEnabled = true;
console.log('📱 Twilio SMS service initialized successfully');
} catch (error) {
console.error('❌ Failed to initialize Twilio:', error.message);
console.log('📱 SMS alerts disabled due to Twilio initialization error');
this.twilioEnabled = false;
this.twilioClient = null;
}
}
async processAlert(detection) {
try {
console.log(`🔍 Processing alert for detection ${detection.id}`);
// Assess threat level based on RSSI and drone type
const threatAssessment = this.assessThreatLevel(detection.rssi, detection.drone_type);
console.log('⚠️ Threat assessment:', threatAssessment);
// Update detection with threat assessment
await detection.update({
processed: true,
threat_level: threatAssessment.level,
estimated_distance: threatAssessment.estimatedDistance
});
// Get all active alert rules
const alertRules = await AlertRule.findAll({
where: { is_active: true },
include: [{
model: User,
as: 'user',
where: { is_active: true }
}]
});
for (const rule of alertRules) {
if (await this.shouldTriggerAlert(rule, detection, threatAssessment)) {
await this.triggerAlert(rule, detection, threatAssessment);
// Track active alert for potential clear notification
const alertKey = `${rule.id}-${detection.device_id}`;
this.activeAlerts.set(alertKey, {
rule,
detection,
threatAssessment,
alertTime: new Date()
});
}
}
} catch (error) {
console.error('Error processing alert:', error);
throw error;
}
}
async shouldTriggerAlert(rule, detection, threatAssessment) {
try {
// SECURITY ENHANCEMENT: Check threat level requirements
if (rule.conditions.min_threat_level) {
const threatLevels = { 'monitoring': 0, 'low': 1, 'medium': 2, 'high': 3, 'critical': 4 };
const requiredLevel = threatLevels[rule.conditions.min_threat_level] || 0;
const currentLevel = threatLevels[threatAssessment.level] || 0;
if (currentLevel < requiredLevel) {
console.log(`Alert rule ${rule.name}: Threat level ${threatAssessment.level} below minimum ${rule.conditions.min_threat_level}`);
return false;
}
}
// SECURITY ENHANCEMENT: For government/sensitive sites, always alert on critical threats
if (threatAssessment.level === 'critical') {
console.log(`🚨 CRITICAL THREAT DETECTED - Force triggering alert for rule ${rule.name}`);
return true;
}
// Check device filter
if (rule.conditions.device_ids && rule.conditions.device_ids.length > 0 &&
!rule.conditions.device_ids.includes(detection.device_id)) {
return false;
}
// Check drone type filter
if (rule.conditions.drone_types && rule.conditions.drone_types.length > 0 &&
!rule.conditions.drone_types.includes(detection.drone_type)) {
return false;
}
// Check RSSI thresholds (enhanced for security)
if (rule.conditions.rssi_threshold && detection.rssi < rule.conditions.rssi_threshold) {
return false;
}
// SECURITY ENHANCEMENT: Check estimated distance
if (rule.conditions.max_distance && threatAssessment.estimatedDistance > rule.conditions.max_distance) {
console.log(`Alert rule ${rule.name}: Distance ${threatAssessment.estimatedDistance}m exceeds maximum ${rule.conditions.max_distance}m`);
return false;
}
// Check frequency ranges
if (rule.frequency_ranges && rule.frequency_ranges.length > 0) {
const inRange = rule.frequency_ranges.some(range =>
detection.freq >= range.min && detection.freq <= range.max
);
if (!inRange) {
return false;
}
}
// Check time window and minimum detections
if (rule.min_detections > 1) {
const timeWindowStart = new Date(Date.now() - rule.time_window * 1000);
const recentDetections = await DroneDetection.count({
where: {
device_id: detection.device_id,
drone_id: detection.drone_id,
server_timestamp: {
[Op.gte]: timeWindowStart
}
}
});
if (recentDetections < rule.min_detections) {
return false;
}
}
// Check cooldown period
if (rule.cooldown_period > 0) {
const cooldownStart = new Date(Date.now() - rule.cooldown_period * 1000);
const recentAlert = await AlertLog.findOne({
where: {
alert_rule_id: rule.id,
status: 'sent',
sent_at: {
[Op.gte]: cooldownStart
}
},
include: [{
model: DroneDetection,
as: 'detection',
where: {
device_id: detection.device_id,
drone_id: detection.drone_id
}
}]
});
if (recentAlert) {
return false;
}
}
// Check active hours and days
if (rule.active_hours || rule.active_days) {
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentTime = currentHour * 60 + currentMinute;
const currentDay = now.getDay() || 7; // Convert Sunday from 0 to 7
if (rule.active_days && !rule.active_days.includes(currentDay)) {
return false;
}
if (rule.active_hours) {
const startTime = this.parseTime(rule.active_hours.start);
const endTime = this.parseTime(rule.active_hours.end);
if (startTime !== null && endTime !== null) {
if (startTime <= endTime) {
// Same day range
if (currentTime < startTime || currentTime > endTime) {
return false;
}
} else {
// Overnight range
if (currentTime < startTime && currentTime > endTime) {
return false;
}
}
}
}
}
return true;
} catch (error) {
console.error('Error checking alert conditions:', error);
return false;
}
}
async triggerAlert(rule, detection, threatAssessment) {
try {
const user = rule.user;
const device = await Device.findByPk(detection.device_id);
// Generate enhanced alert message with threat assessment
const message = this.generateSecurityAlertMessage(detection, device, rule, threatAssessment);
// SECURITY ENHANCEMENT: For critical threats, send to all available channels
const channels = threatAssessment.level === 'critical'
? ['sms', 'email', 'webhook'] // Force all channels for critical threats
: rule.alert_channels || ['sms'];
// Send alerts through configured channels
for (const channel of channels) {
let alertLog = null;
try {
switch (channel) {
case 'sms':
if (rule.alert_channels.includes('sms') && rule.sms_phone_number) {
alertLog = await this.sendSMSAlert(rule.sms_phone_number, message, rule, detection, threatAssessment);
}
break;
case 'email':
if (rule.actions.email && user.email) {
alertLog = await this.sendEmailAlert(user.email, message, rule, detection, threatAssessment);
}
break;
case 'webhook':
if (rule.actions.webhook_url) {
alertLog = await this.sendWebhookAlert(rule.actions.webhook_url, detection, device, rule, threatAssessment);
}
break;
default:
console.warn(`Unknown alert channel: ${channel}`);
}
if (alertLog) {
console.log(`🚨 ${threatAssessment.level.toUpperCase()} THREAT: Alert sent via ${channel} for detection ${detection.id}`);
}
} catch (channelError) {
console.error(`Error sending ${channel} alert:`, channelError);
// Log failed alert
await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: channel,
recipient: channel === 'sms' ? user.phone_number :
channel === 'email' ? user.email : rule.webhook_url,
message: message,
status: 'failed',
error_message: channelError.message,
priority: rule.priority
});
}
}
} catch (error) {
console.error('Error triggering alert:', error);
throw error;
}
}
async sendSMSAlert(phoneNumber, message, rule, detection) {
// Check if Twilio is enabled
if (!this.twilioEnabled || !this.twilioClient) {
console.log('📱 SMS alert skipped - Twilio not configured');
console.log(`📱 Would have sent to ${phoneNumber}:`);
console.log(`📱 Message: ${message}`);
return await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: 'sms',
recipient: phoneNumber,
message: message,
status: 'failed',
sent_at: new Date(),
external_id: null,
priority: rule.priority,
error_message: 'SMS service not configured'
});
}
try {
console.log(`📱 Sending SMS alert to ${phoneNumber} for rule: ${rule.name}`);
console.log(`📱 Message: ${message}`);
const twilioMessage = await this.twilioClient.messages.create({
body: message,
from: this.twilioPhone,
to: phoneNumber
});
console.log(`✅ SMS sent successfully to ${phoneNumber}: ${twilioMessage.sid}`);
return await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: 'sms',
recipient: phoneNumber,
message: message,
status: 'sent',
sent_at: new Date(),
external_id: twilioMessage.sid,
priority: rule.priority
});
} catch (error) {
console.error(`❌ Failed to send SMS to ${phoneNumber}:`, error.message);
return await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: 'sms',
recipient: phoneNumber,
message: message,
status: 'failed',
sent_at: new Date(),
external_id: null,
priority: rule.priority,
error_message: error.message
});
}
}
async sendEmailAlert(email, message, rule, detection) {
// Email implementation would go here
// For now, just log the alert
console.log(`Email alert would be sent to ${email}: ${message}`);
return await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: 'email',
recipient: email,
message: message,
status: 'sent',
sent_at: new Date(),
priority: rule.priority
});
}
async sendWebhookAlert(webhookUrl, detection, device, rule) {
const payload = {
event: 'drone_detection',
timestamp: new Date().toISOString(),
detection: {
id: detection.id,
device_id: detection.device_id,
drone_id: detection.drone_id,
drone_type: detection.drone_type,
rssi: detection.rssi,
freq: detection.freq,
geo_lat: detection.geo_lat,
geo_lon: detection.geo_lon,
server_timestamp: detection.server_timestamp
},
device: {
id: device.id,
name: device.name,
geo_lat: device.geo_lat,
geo_lon: device.geo_lon,
location_description: device.location_description
},
alert_rule: {
id: rule.id,
name: rule.name,
priority: rule.priority
}
};
const response = await fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'DroneDetectionSystem/1.0'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`Webhook failed with status ${response.status}`);
}
return await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: 'webhook',
recipient: webhookUrl,
message: JSON.stringify(payload),
status: 'sent',
sent_at: new Date(),
priority: rule.priority
});
}
generateAlertMessage(detection, device, rule) {
const deviceName = device.name || `Device ${device.id}`;
const location = device.location_description ||
`${device.geo_lat}, ${device.geo_lon}` ||
'Unknown location';
return `🚨 DRONE ALERT: Drone detected by ${deviceName} at ${location}. ` +
`Drone ID: ${detection.drone_id}, Frequency: ${detection.freq}MHz, ` +
`RSSI: ${detection.rssi}dBm. Time: ${new Date().toLocaleString()}`;
}
// SECURITY ENHANCEMENT: Enhanced message generation with threat assessment
generateSecurityAlertMessage(detection, device, rule, threatAssessment) {
const timestamp = new Date().toLocaleString('sv-SE', { timeZone: 'Europe/Stockholm' });
const deviceName = device ? device.name : `Device ${detection.device_id}`;
const location = device ? device.location : 'Unknown location';
// Create security-focused alert message
let message = `🚨 SECURITY ALERT 🚨\n`;
message += `THREAT LEVEL: ${threatAssessment.level.toUpperCase()}\n`;
message += `${threatAssessment.description}\n\n`;
message += `📍 LOCATION: ${location}\n`;
message += `🔧 DEVICE: ${deviceName}\n`;
message += `📏 DISTANCE: ~${threatAssessment.estimatedDistance}m\n`;
message += `📶 SIGNAL: ${detection.rssi} dBm\n`;
message += `🚁 DRONE TYPE: ${threatAssessment.droneType}\n`;
message += `⏰ TIME: ${timestamp}\n`;
if (threatAssessment.requiresImmediateAction) {
message += `\n⚠️ IMMEDIATE ACTION REQUIRED\n`;
message += `Security personnel should respond immediately.`;
}
return message;
}
parseTime(timeString) {
if (!timeString) return null;
const match = timeString.match(/^(\d{1,2}):(\d{2})$/);
if (!match) return null;
const hours = parseInt(match[1]);
const minutes = parseInt(match[2]);
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
return null;
}
return hours * 60 + minutes;
}
// Check for cleared alerts (call this periodically)
async checkClearedAlerts() {
try {
const now = new Date();
const clearThreshold = 5 * 60 * 1000; // 5 minutes without detection = cleared
for (const [alertKey, alertData] of this.activeAlerts.entries()) {
const timeSinceAlert = now - alertData.alertTime;
if (timeSinceAlert > clearThreshold) {
// Check if there are any recent detections from this device
const recentDetections = await DroneDetection.count({
where: {
device_id: alertData.detection.device_id,
device_timestamp: {
[Op.gte]: new Date(now - clearThreshold)
}
}
});
if (recentDetections === 0) {
await this.sendClearAlert(alertData);
this.activeAlerts.delete(alertKey);
}
}
}
} catch (error) {
console.error('Error checking cleared alerts:', error);
}
}
async sendClearAlert(alertData) {
const { rule, detection } = alertData;
if (rule.alert_channels.includes('sms') && rule.sms_phone_number) {
const device = await Device.findByPk(detection.device_id);
const clearMessage = this.generateClearMessage(device, rule);
console.log(`🟢 ALERT CLEARED - Sending clear notification for ${device.name} to ${rule.sms_phone_number}`);
await this.sendSMSAlert(rule.sms_phone_number, clearMessage, rule, detection, null);
}
}
generateClearMessage(device, rule) {
return `🟢 ALL CLEAR 🟢\n\n` +
`📍 LOCATION: ${device.location_description || device.name}\n` +
`🔧 DEVICE: ${device.name}\n` +
`⏰ TIME: ${new Date().toLocaleString('sv-SE')}\n\n` +
`✅ No drone activity detected for 5+ minutes.\n` +
`🛡️ Area is secure.`;
}
}
// Export the class directly (not a singleton instance)
module.exports = AlertService;