482 lines
14 KiB
JavaScript
482 lines
14 KiB
JavaScript
|
|
/**
|
||
|
|
* Threshold Manager
|
||
|
|
* Configurable notification thresholds for security threat detection
|
||
|
|
* CWE-778 Compliance: Logs all threshold configurations and evaluations
|
||
|
|
*/
|
||
|
|
|
||
|
|
const logger = require('./logger');
|
||
|
|
const logAggregator = require('./logAggregator');
|
||
|
|
const { db } = require('../database/db');
|
||
|
|
|
||
|
|
class ThresholdManager {
|
||
|
|
constructor() {
|
||
|
|
this.thresholds = new Map();
|
||
|
|
this.initialize();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize threshold manager
|
||
|
|
*/
|
||
|
|
async initialize() {
|
||
|
|
await this.createThresholdsTable();
|
||
|
|
await this.loadThresholds();
|
||
|
|
|
||
|
|
logger.info('[ThresholdManager] Initialized with configurable thresholds');
|
||
|
|
|
||
|
|
// Log initialization (CWE-778)
|
||
|
|
logAggregator.aggregate('threshold_manager', 'info', 'security', 'Threshold manager initialized', {
|
||
|
|
totalThresholds: this.thresholds.size
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create thresholds table
|
||
|
|
*/
|
||
|
|
async createThresholdsTable() {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
db.run(`
|
||
|
|
CREATE TABLE IF NOT EXISTS security_thresholds (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
threshold_id TEXT UNIQUE NOT NULL,
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
description TEXT,
|
||
|
|
pattern_type TEXT NOT NULL,
|
||
|
|
metric_name TEXT NOT NULL,
|
||
|
|
operator TEXT NOT NULL,
|
||
|
|
threshold_value INTEGER NOT NULL,
|
||
|
|
time_window_minutes INTEGER DEFAULT 30,
|
||
|
|
severity TEXT NOT NULL,
|
||
|
|
enabled INTEGER DEFAULT 1,
|
||
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
|
|
)
|
||
|
|
`, async (err) => {
|
||
|
|
if (err) reject(err);
|
||
|
|
else {
|
||
|
|
db.run(`CREATE INDEX IF NOT EXISTS idx_thresholds_pattern ON security_thresholds(pattern_type, enabled)`);
|
||
|
|
db.run(`CREATE INDEX IF NOT EXISTS idx_thresholds_enabled ON security_thresholds(enabled)`);
|
||
|
|
await this.createDefaultThresholds();
|
||
|
|
resolve();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create default thresholds for common security patterns
|
||
|
|
*/
|
||
|
|
async createDefaultThresholds() {
|
||
|
|
const defaultThresholds = [
|
||
|
|
{
|
||
|
|
threshold_id: 'THRESHOLD-BRUTE-FORCE',
|
||
|
|
name: 'Brute Force Attack Threshold',
|
||
|
|
description: 'Alert when failed login attempts exceed threshold',
|
||
|
|
pattern_type: 'brute_force_attack',
|
||
|
|
metric_name: 'failed_login_count',
|
||
|
|
operator: '>=',
|
||
|
|
threshold_value: 5,
|
||
|
|
time_window_minutes: 10,
|
||
|
|
severity: 'critical'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
threshold_id: 'THRESHOLD-CREDENTIAL-STUFFING',
|
||
|
|
name: 'Credential Stuffing Threshold',
|
||
|
|
description: 'Alert on multiple username attempts from same IP',
|
||
|
|
pattern_type: 'credential_stuffing',
|
||
|
|
metric_name: 'unique_username_count',
|
||
|
|
operator: '>=',
|
||
|
|
threshold_value: 5,
|
||
|
|
time_window_minutes: 5,
|
||
|
|
severity: 'critical'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
threshold_id: 'THRESHOLD-PRIVILEGE-ESC',
|
||
|
|
name: 'Privilege Escalation Threshold',
|
||
|
|
description: 'Alert on repeated unauthorized access attempts',
|
||
|
|
pattern_type: 'privilege_escalation',
|
||
|
|
metric_name: 'escalation_attempt_count',
|
||
|
|
operator: '>=',
|
||
|
|
threshold_value: 3,
|
||
|
|
time_window_minutes: 30,
|
||
|
|
severity: 'critical'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
threshold_id: 'THRESHOLD-SUSPICIOUS-IP',
|
||
|
|
name: 'Suspicious IP Activity Threshold',
|
||
|
|
description: 'Alert on excessive requests from single IP',
|
||
|
|
pattern_type: 'suspicious_ip',
|
||
|
|
metric_name: 'request_count',
|
||
|
|
operator: '>=',
|
||
|
|
threshold_value: 100,
|
||
|
|
time_window_minutes: 15,
|
||
|
|
severity: 'high'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
threshold_id: 'THRESHOLD-DATA-EXFIL',
|
||
|
|
name: 'Data Exfiltration Threshold',
|
||
|
|
description: 'Alert on excessive data downloads',
|
||
|
|
pattern_type: 'data_exfiltration',
|
||
|
|
metric_name: 'download_count',
|
||
|
|
operator: '>=',
|
||
|
|
threshold_value: 10,
|
||
|
|
time_window_minutes: 60,
|
||
|
|
severity: 'high'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
threshold_id: 'THRESHOLD-SESSION-ANOMALY',
|
||
|
|
name: 'Session Anomaly Threshold',
|
||
|
|
description: 'Alert on unusual session patterns',
|
||
|
|
pattern_type: 'session_anomaly',
|
||
|
|
metric_name: 'anomaly_score',
|
||
|
|
operator: '>=',
|
||
|
|
threshold_value: 70,
|
||
|
|
time_window_minutes: 30,
|
||
|
|
severity: 'medium'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
threshold_id: 'THRESHOLD-IMPOSSIBLE-TRAVEL',
|
||
|
|
name: 'Impossible Travel Threshold',
|
||
|
|
description: 'Alert on geographically impossible travel speed',
|
||
|
|
pattern_type: 'impossible_travel',
|
||
|
|
metric_name: 'travel_speed_kmh',
|
||
|
|
operator: '>=',
|
||
|
|
threshold_value: 800,
|
||
|
|
time_window_minutes: 60,
|
||
|
|
severity: 'high'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
threshold_id: 'THRESHOLD-THREAT-SCORE',
|
||
|
|
name: 'Critical Threat Score Threshold',
|
||
|
|
description: 'Alert when overall threat score is critical',
|
||
|
|
pattern_type: 'threat_score',
|
||
|
|
metric_name: 'threat_score',
|
||
|
|
operator: '>=',
|
||
|
|
threshold_value: 80,
|
||
|
|
time_window_minutes: 60,
|
||
|
|
severity: 'critical'
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
for (const threshold of defaultThresholds) {
|
||
|
|
await new Promise((resolve, reject) => {
|
||
|
|
db.run(
|
||
|
|
`INSERT OR IGNORE INTO security_thresholds
|
||
|
|
(threshold_id, name, description, pattern_type, metric_name, operator, threshold_value, time_window_minutes, severity)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
|
|
[
|
||
|
|
threshold.threshold_id,
|
||
|
|
threshold.name,
|
||
|
|
threshold.description,
|
||
|
|
threshold.pattern_type,
|
||
|
|
threshold.metric_name,
|
||
|
|
threshold.operator,
|
||
|
|
threshold.threshold_value,
|
||
|
|
threshold.time_window_minutes,
|
||
|
|
threshold.severity
|
||
|
|
],
|
||
|
|
(err) => {
|
||
|
|
if (err) reject(err);
|
||
|
|
else resolve();
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info(`[ThresholdManager] Created ${defaultThresholds.length} default thresholds`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Load thresholds from database into memory
|
||
|
|
*/
|
||
|
|
async loadThresholds() {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
db.all(
|
||
|
|
`SELECT * FROM security_thresholds WHERE enabled = 1`,
|
||
|
|
[],
|
||
|
|
(err, rows) => {
|
||
|
|
if (err) {
|
||
|
|
reject(err);
|
||
|
|
} else {
|
||
|
|
this.thresholds.clear();
|
||
|
|
rows.forEach(row => {
|
||
|
|
this.thresholds.set(row.threshold_id, row);
|
||
|
|
});
|
||
|
|
logger.info(`[ThresholdManager] Loaded ${rows.length} active thresholds`);
|
||
|
|
resolve();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Evaluate if a metric value exceeds threshold
|
||
|
|
* CWE-778: Logs all threshold evaluations
|
||
|
|
*/
|
||
|
|
async evaluateThreshold(patternType, metricName, value, context = {}) {
|
||
|
|
const matchingThresholds = Array.from(this.thresholds.values()).filter(
|
||
|
|
t => t.pattern_type === patternType && t.metric_name === metricName
|
||
|
|
);
|
||
|
|
|
||
|
|
if (matchingThresholds.length === 0) {
|
||
|
|
return { exceeded: false, thresholds: [] };
|
||
|
|
}
|
||
|
|
|
||
|
|
const exceededThresholds = [];
|
||
|
|
|
||
|
|
for (const threshold of matchingThresholds) {
|
||
|
|
const exceeded = this.compareValue(value, threshold.operator, threshold.threshold_value);
|
||
|
|
|
||
|
|
// Log threshold evaluation (CWE-778)
|
||
|
|
logAggregator.aggregate('threshold_manager', 'info', 'security', 'Threshold evaluated', {
|
||
|
|
thresholdId: threshold.threshold_id,
|
||
|
|
patternType,
|
||
|
|
metricName,
|
||
|
|
value,
|
||
|
|
operator: threshold.operator,
|
||
|
|
thresholdValue: threshold.threshold_value,
|
||
|
|
exceeded,
|
||
|
|
severity: threshold.severity,
|
||
|
|
context
|
||
|
|
});
|
||
|
|
|
||
|
|
if (exceeded) {
|
||
|
|
exceededThresholds.push({
|
||
|
|
...threshold,
|
||
|
|
actualValue: value,
|
||
|
|
context
|
||
|
|
});
|
||
|
|
|
||
|
|
logger.warn(`[ThresholdManager] Threshold exceeded: ${threshold.name} (${value} ${threshold.operator} ${threshold.threshold_value})`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
exceeded: exceededThresholds.length > 0,
|
||
|
|
thresholds: exceededThresholds
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Compare value against threshold using operator
|
||
|
|
*/
|
||
|
|
compareValue(value, operator, threshold) {
|
||
|
|
switch (operator) {
|
||
|
|
case '>=': return value >= threshold;
|
||
|
|
case '>': return value > threshold;
|
||
|
|
case '<=': return value <= threshold;
|
||
|
|
case '<': return value < threshold;
|
||
|
|
case '==': return value == threshold;
|
||
|
|
case '!=': return value != threshold;
|
||
|
|
default: return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get all thresholds
|
||
|
|
*/
|
||
|
|
async getThresholds(filters = {}) {
|
||
|
|
const { patternType, enabled, limit = 100 } = filters;
|
||
|
|
|
||
|
|
let whereClause = [];
|
||
|
|
let params = [];
|
||
|
|
|
||
|
|
if (patternType) {
|
||
|
|
whereClause.push('pattern_type = ?');
|
||
|
|
params.push(patternType);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (enabled !== undefined) {
|
||
|
|
whereClause.push('enabled = ?');
|
||
|
|
params.push(enabled ? 1 : 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
const where = whereClause.length > 0 ? `WHERE ${whereClause.join(' AND ')}` : '';
|
||
|
|
params.push(limit);
|
||
|
|
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
db.all(
|
||
|
|
`SELECT * FROM security_thresholds ${where}
|
||
|
|
ORDER BY pattern_type, threshold_value DESC
|
||
|
|
LIMIT ?`,
|
||
|
|
params,
|
||
|
|
(err, rows) => {
|
||
|
|
if (err) reject(err);
|
||
|
|
else resolve(rows);
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get threshold by ID
|
||
|
|
*/
|
||
|
|
async getThresholdById(thresholdId) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
db.get(
|
||
|
|
`SELECT * FROM security_thresholds WHERE threshold_id = ?`,
|
||
|
|
[thresholdId],
|
||
|
|
(err, row) => {
|
||
|
|
if (err) reject(err);
|
||
|
|
else resolve(row);
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create new threshold
|
||
|
|
* CWE-778: Logs threshold creation
|
||
|
|
*/
|
||
|
|
async createThreshold(data, userId) {
|
||
|
|
const thresholdId = `THRESHOLD-${Date.now()}-${Math.random().toString(36).substr(2, 9).toUpperCase()}`;
|
||
|
|
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
db.run(
|
||
|
|
`INSERT INTO security_thresholds
|
||
|
|
(threshold_id, name, description, pattern_type, metric_name, operator, threshold_value, time_window_minutes, severity, enabled)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
|
|
[
|
||
|
|
thresholdId,
|
||
|
|
data.name,
|
||
|
|
data.description || '',
|
||
|
|
data.pattern_type,
|
||
|
|
data.metric_name,
|
||
|
|
data.operator,
|
||
|
|
data.threshold_value,
|
||
|
|
data.time_window_minutes || 30,
|
||
|
|
data.severity,
|
||
|
|
data.enabled !== undefined ? (data.enabled ? 1 : 0) : 1
|
||
|
|
],
|
||
|
|
async (err) => {
|
||
|
|
if (err) {
|
||
|
|
reject(err);
|
||
|
|
} else {
|
||
|
|
await this.loadThresholds();
|
||
|
|
|
||
|
|
// Log threshold creation (CWE-778)
|
||
|
|
logAggregator.aggregate('threshold_manager', 'info', 'security', 'Threshold created', {
|
||
|
|
thresholdId,
|
||
|
|
userId,
|
||
|
|
name: data.name,
|
||
|
|
patternType: data.pattern_type,
|
||
|
|
metricName: data.metric_name,
|
||
|
|
thresholdValue: data.threshold_value,
|
||
|
|
severity: data.severity
|
||
|
|
});
|
||
|
|
|
||
|
|
logger.info(`[ThresholdManager] Threshold created: ${thresholdId} by user ${userId}`);
|
||
|
|
resolve({ thresholdId });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update threshold
|
||
|
|
* CWE-778: Logs threshold modifications
|
||
|
|
*/
|
||
|
|
async updateThreshold(thresholdId, updates, userId) {
|
||
|
|
const allowedFields = ['name', 'description', 'operator', 'threshold_value', 'time_window_minutes', 'severity', 'enabled'];
|
||
|
|
const setClause = [];
|
||
|
|
const params = [];
|
||
|
|
|
||
|
|
for (const [key, value] of Object.entries(updates)) {
|
||
|
|
if (allowedFields.includes(key)) {
|
||
|
|
setClause.push(`${key} = ?`);
|
||
|
|
params.push(key === 'enabled' ? (value ? 1 : 0) : value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (setClause.length === 0) {
|
||
|
|
throw new Error('No valid fields to update');
|
||
|
|
}
|
||
|
|
|
||
|
|
setClause.push('updated_at = CURRENT_TIMESTAMP');
|
||
|
|
params.push(thresholdId);
|
||
|
|
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
db.run(
|
||
|
|
`UPDATE security_thresholds
|
||
|
|
SET ${setClause.join(', ')}
|
||
|
|
WHERE threshold_id = ?`,
|
||
|
|
params,
|
||
|
|
async (err) => {
|
||
|
|
if (err) {
|
||
|
|
reject(err);
|
||
|
|
} else {
|
||
|
|
await this.loadThresholds();
|
||
|
|
|
||
|
|
// Log threshold update (CWE-778)
|
||
|
|
logAggregator.aggregate('threshold_manager', 'info', 'security', 'Threshold updated', {
|
||
|
|
thresholdId,
|
||
|
|
userId,
|
||
|
|
updates
|
||
|
|
});
|
||
|
|
|
||
|
|
logger.info(`[ThresholdManager] Threshold updated: ${thresholdId} by user ${userId}`);
|
||
|
|
resolve({ success: true });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Delete threshold
|
||
|
|
* CWE-778: Logs threshold deletion
|
||
|
|
*/
|
||
|
|
async deleteThreshold(thresholdId, userId) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
db.run(
|
||
|
|
`DELETE FROM security_thresholds WHERE threshold_id = ?`,
|
||
|
|
[thresholdId],
|
||
|
|
async (err) => {
|
||
|
|
if (err) {
|
||
|
|
reject(err);
|
||
|
|
} else {
|
||
|
|
await this.loadThresholds();
|
||
|
|
|
||
|
|
// Log threshold deletion (CWE-778)
|
||
|
|
logAggregator.aggregate('threshold_manager', 'warn', 'security', 'Threshold deleted', {
|
||
|
|
thresholdId,
|
||
|
|
userId
|
||
|
|
});
|
||
|
|
|
||
|
|
logger.info(`[ThresholdManager] Threshold deleted: ${thresholdId} by user ${userId}`);
|
||
|
|
resolve({ success: true });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get threshold statistics
|
||
|
|
*/
|
||
|
|
async getStatistics() {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
db.get(
|
||
|
|
`SELECT
|
||
|
|
COUNT(*) as total,
|
||
|
|
SUM(CASE WHEN enabled = 1 THEN 1 ELSE 0 END) as enabled,
|
||
|
|
SUM(CASE WHEN enabled = 0 THEN 1 ELSE 0 END) as disabled,
|
||
|
|
COUNT(DISTINCT pattern_type) as unique_patterns,
|
||
|
|
COUNT(DISTINCT severity) as unique_severities
|
||
|
|
FROM security_thresholds`,
|
||
|
|
[],
|
||
|
|
(err, row) => {
|
||
|
|
if (err) reject(err);
|
||
|
|
else resolve(row);
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create singleton instance
|
||
|
|
const thresholdManager = new ThresholdManager();
|
||
|
|
|
||
|
|
module.exports = thresholdManager;
|