- Create secubox-app-vhost-manager package for unified vhost orchestration - Single CLI tool (secubox-vhost) manages HAProxy, DNS, Tor, Mesh, mitmproxy - Unified UCI config (/etc/config/vhosts) as single source of truth - Backend adapters for each component (haproxy.sh, dns.sh, tor.sh, mesh.sh, mitmproxy.sh) - Centralized backend resolution function (backends.sh) - Import tool for existing HAProxy vhosts - Validation of backend reachability before creation Also includes: - FAQ-TROUBLESHOOTING.md with LXC cgroup v1/v2 fixes - Fix mitmproxyctl cgroup v1 -> v2 syntax for container compatibility - HAProxy backend resolution bugfixes CLI commands: secubox-vhost add <domain> <service> <port> [--ssl] [--tor] [--mesh] secubox-vhost remove/list/status/enable/disable/set/sync/validate/import Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
353 lines
12 KiB
JavaScript
353 lines
12 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require dom';
|
|
'require poll';
|
|
'require ui';
|
|
'require threat-analyst.api as api';
|
|
|
|
/**
|
|
* Threat Analyst Dashboard - v0.1.0
|
|
* Generative AI-powered threat filtering
|
|
*
|
|
* Following CrowdSec Dashboard KISS template pattern
|
|
*/
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
var link = document.createElement('link');
|
|
link.rel = 'stylesheet';
|
|
link.href = L.resource('threat-analyst/dashboard.css');
|
|
document.head.appendChild(link);
|
|
return api.getOverview().catch(function() { return {}; });
|
|
},
|
|
|
|
render: function(data) {
|
|
var self = this;
|
|
var s = data.status || {};
|
|
var threats = data.threats || [];
|
|
var pending = data.pending || [];
|
|
|
|
var view = E('div', { 'class': 'ta-view' }, [
|
|
// Header
|
|
E('div', { 'class': 'ta-header' }, [
|
|
E('div', { 'class': 'ta-title' }, 'Threat Analyst'),
|
|
E('div', { 'class': 'ta-status' }, [
|
|
E('span', { 'class': 'ta-dot ' + (s.daemon_running ? 'online' : 'offline') }),
|
|
s.daemon_running ? 'Running' : 'Stopped'
|
|
])
|
|
]),
|
|
|
|
// Stats
|
|
E('div', { 'class': 'ta-stats', 'id': 'ta-stats' }, this.renderStats(s, pending)),
|
|
|
|
// Two column layout
|
|
E('div', { 'class': 'ta-grid-2' }, [
|
|
// Health card
|
|
E('div', { 'class': 'ta-card' }, [
|
|
E('div', { 'class': 'ta-card-header' }, 'System Health'),
|
|
E('div', { 'class': 'ta-card-body' }, this.renderHealth(s))
|
|
]),
|
|
// Pending Rules card
|
|
E('div', { 'class': 'ta-card' }, [
|
|
E('div', { 'class': 'ta-card-header' }, 'Pending Rules (' + pending.length + ')'),
|
|
E('div', { 'class': 'ta-card-body', 'id': 'ta-pending' }, this.renderPending(pending))
|
|
])
|
|
]),
|
|
|
|
// Generate Rules card
|
|
E('div', { 'class': 'ta-card' }, [
|
|
E('div', { 'class': 'ta-card-header' }, 'Generate Filter Rules'),
|
|
E('div', { 'class': 'ta-card-body' }, this.renderTargets())
|
|
]),
|
|
|
|
// Threats card
|
|
E('div', { 'class': 'ta-card' }, [
|
|
E('div', { 'class': 'ta-card-header' }, 'Recent Threats from CrowdSec'),
|
|
E('div', { 'class': 'ta-card-body', 'id': 'ta-threats' }, this.renderThreats(threats))
|
|
]),
|
|
|
|
// AI Chat card
|
|
E('div', { 'class': 'ta-card' }, [
|
|
E('div', { 'class': 'ta-card-header' }, 'AI Security Assistant'),
|
|
E('div', { 'class': 'ta-card-body' }, this.renderChat())
|
|
])
|
|
]);
|
|
|
|
poll.add(L.bind(this.pollData, this), 30);
|
|
return view;
|
|
},
|
|
|
|
renderStats: function(s, pending) {
|
|
var stats = [
|
|
{ label: 'Daemon', value: s.daemon_running ? 'ON' : 'OFF', type: s.daemon_running ? 'success' : 'danger' },
|
|
{ label: 'LocalAI', value: s.localai_status === 'online' ? 'OK' : 'OFF', type: s.localai_status === 'online' ? 'success' : 'danger' },
|
|
{ label: 'Threats (1h)', value: s.recent_threats || 0, type: (s.recent_threats || 0) > 10 ? 'danger' : (s.recent_threats || 0) > 0 ? 'warning' : 'success' },
|
|
{ label: 'Pending', value: pending.length || 0, type: (pending.length || 0) > 0 ? 'warning' : '' }
|
|
];
|
|
return stats.map(function(st) {
|
|
return E('div', { 'class': 'ta-stat ' + st.type }, [
|
|
E('div', { 'class': 'ta-stat-value' }, String(st.value)),
|
|
E('div', { 'class': 'ta-stat-label' }, st.label)
|
|
]);
|
|
});
|
|
},
|
|
|
|
renderHealth: function(s) {
|
|
var cveCount = s.cve_alerts || 0;
|
|
var checks = [
|
|
{ label: 'Daemon', ok: s.daemon_running },
|
|
{ label: 'LocalAI', ok: s.localai_status === 'online' },
|
|
{ label: 'CrowdSec', ok: s.recent_threats !== undefined },
|
|
{ label: 'CVE Alerts', ok: cveCount === 0, value: cveCount > 0 ? cveCount + ' Active' : 'None', warn: cveCount > 0 },
|
|
{ label: 'Auto-Apply', ok: s.enabled, value: s.enabled ? 'Enabled' : 'Manual' }
|
|
];
|
|
return E('div', { 'class': 'ta-health' }, checks.map(function(c) {
|
|
var valueText = c.value ? c.value : (c.ok ? 'OK' : 'Unavailable');
|
|
var iconClass = c.warn ? 'warning' : (c.ok ? 'ok' : 'error');
|
|
var iconChar = c.warn ? '\u26A0' : (c.ok ? '\u2713' : '\u2717');
|
|
return E('div', { 'class': 'ta-health-item' }, [
|
|
E('div', { 'class': 'ta-health-icon ' + iconClass }, iconChar),
|
|
E('div', {}, [
|
|
E('div', { 'class': 'ta-health-label' }, c.label),
|
|
E('div', { 'class': 'ta-health-value' }, valueText)
|
|
])
|
|
]);
|
|
}));
|
|
},
|
|
|
|
renderPending: function(pending) {
|
|
var self = this;
|
|
if (!pending.length) {
|
|
return E('div', { 'class': 'ta-empty' }, 'No pending rules for approval');
|
|
}
|
|
return E('div', { 'class': 'ta-pending-list' }, pending.map(function(rule) {
|
|
return E('div', { 'class': 'ta-pending-item' }, [
|
|
E('div', { 'class': 'ta-pending-info' }, [
|
|
E('div', { 'class': 'ta-pending-type' }, [
|
|
E('span', { 'class': 'ta-badge ' + rule.type }, rule.type)
|
|
]),
|
|
E('div', { 'class': 'ta-pending-date' }, (rule.created || '').substring(0, 10))
|
|
]),
|
|
E('div', { 'class': 'ta-pending-actions' }, [
|
|
E('button', {
|
|
'class': 'ta-btn ta-btn-success ta-btn-sm',
|
|
'click': function() { self.approveRule(rule.id); }
|
|
}, 'Approve'),
|
|
E('button', {
|
|
'class': 'ta-btn ta-btn-danger ta-btn-sm',
|
|
'click': function() { self.rejectRule(rule.id); }
|
|
}, 'Reject')
|
|
])
|
|
]);
|
|
}));
|
|
},
|
|
|
|
renderTargets: function() {
|
|
var self = this;
|
|
var targets = [
|
|
{ id: 'crowdsec', name: 'CrowdSec', desc: 'Generate autoban scenarios', icon: '\uD83D\uDEE1' },
|
|
{ id: 'mitmproxy', name: 'mitmproxy', desc: 'Generate Python filters', icon: '\uD83D\uDD0D' },
|
|
{ id: 'waf', name: 'WAF', desc: 'Generate ModSecurity rules', icon: '\uD83D\uDEA7' }
|
|
];
|
|
return E('div', {}, [
|
|
E('div', { 'class': 'ta-targets' }, targets.map(function(t) {
|
|
return E('div', {
|
|
'class': 'ta-target',
|
|
'click': function() { self.generateRules(t.id); }
|
|
}, [
|
|
E('div', { 'class': 'ta-target-icon' }, t.icon),
|
|
E('div', { 'class': 'ta-target-name' }, t.name),
|
|
E('div', { 'class': 'ta-target-desc' }, t.desc)
|
|
]);
|
|
})),
|
|
E('div', { 'class': 'ta-actions' }, [
|
|
E('button', {
|
|
'class': 'ta-btn ta-btn-primary',
|
|
'click': function() { self.runAnalysis(); }
|
|
}, 'Run Analysis Cycle'),
|
|
E('button', {
|
|
'class': 'ta-btn ta-btn-success',
|
|
'click': function() { self.generateRules('all'); }
|
|
}, 'Generate All Rules')
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderThreats: function(threats) {
|
|
if (!threats.length) {
|
|
return E('div', { 'class': 'ta-empty' }, 'No recent threats detected');
|
|
}
|
|
return E('table', { 'class': 'ta-table' }, [
|
|
E('thead', {}, E('tr', {}, [
|
|
E('th', {}, 'Time'),
|
|
E('th', {}, 'Source IP'),
|
|
E('th', {}, 'Scenario'),
|
|
E('th', {}, 'CVE'),
|
|
E('th', {}, 'Severity')
|
|
])),
|
|
E('tbody', {}, threats.slice(0, 10).map(function(t) {
|
|
var src = t.source || {};
|
|
var severity = api.getSeverityClass(t.scenario);
|
|
var cveId = api.extractCVE(t.scenario);
|
|
var cveCell = cveId ?
|
|
E('a', {
|
|
'class': 'ta-cve-link',
|
|
'href': 'https://nvd.nist.gov/vuln/detail/' + cveId,
|
|
'target': '_blank',
|
|
'rel': 'noopener'
|
|
}, cveId) :
|
|
E('span', { 'class': 'ta-no-cve' }, '-');
|
|
return E('tr', { 'class': cveId ? 'ta-cve-row' : '' }, [
|
|
E('td', { 'class': 'ta-time' }, api.formatRelativeTime(t.created_at)),
|
|
E('td', {}, E('span', { 'class': 'ta-ip' }, src.ip || '-')),
|
|
E('td', {}, E('span', { 'class': 'ta-scenario' }, api.parseScenario(t.scenario))),
|
|
E('td', {}, cveCell),
|
|
E('td', {}, E('span', { 'class': 'ta-badge ' + severity }, severity))
|
|
]);
|
|
}))
|
|
]);
|
|
},
|
|
|
|
renderChat: function() {
|
|
var self = this;
|
|
return E('div', { 'class': 'ta-chat' }, [
|
|
E('div', { 'class': 'ta-chat-messages', 'id': 'ta-chat-messages' }, [
|
|
E('div', { 'class': 'ta-message ai' }, [
|
|
E('div', { 'class': 'ta-message-bubble' },
|
|
'Hello! I\'m your Threat Analyst AI. Ask me about security threats, ' +
|
|
'or request rules for CrowdSec, mitmproxy, or WAF.'),
|
|
E('div', { 'class': 'ta-message-time' }, 'System')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'ta-chat-input' }, [
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'ta-chat-input',
|
|
'placeholder': 'Ask about threats or request filter rules...',
|
|
'keypress': function(e) { if (e.key === 'Enter') self.sendChat(); }
|
|
}),
|
|
E('button', {
|
|
'class': 'ta-btn ta-btn-primary',
|
|
'click': function() { self.sendChat(); }
|
|
}, 'Send')
|
|
])
|
|
]);
|
|
},
|
|
|
|
sendChat: function() {
|
|
var input = document.getElementById('ta-chat-input');
|
|
var messages = document.getElementById('ta-chat-messages');
|
|
var message = input.value.trim();
|
|
|
|
if (!message) return;
|
|
|
|
// Add user message
|
|
messages.appendChild(E('div', { 'class': 'ta-message user' }, [
|
|
E('div', { 'class': 'ta-message-bubble' }, message),
|
|
E('div', { 'class': 'ta-message-time' }, new Date().toLocaleTimeString())
|
|
]));
|
|
|
|
input.value = '';
|
|
messages.scrollTop = messages.scrollHeight;
|
|
|
|
// Add loading
|
|
var loading = E('div', { 'class': 'ta-message ai', 'id': 'ta-chat-loading' }, [
|
|
E('div', { 'class': 'ta-message-bubble' }, 'Analyzing...')
|
|
]);
|
|
messages.appendChild(loading);
|
|
|
|
api.chat(message).then(function(result) {
|
|
var loadingEl = document.getElementById('ta-chat-loading');
|
|
if (loadingEl) loadingEl.remove();
|
|
|
|
var response = result.response || result.error || 'No response';
|
|
messages.appendChild(E('div', { 'class': 'ta-message ai' }, [
|
|
E('div', { 'class': 'ta-message-bubble' }, response),
|
|
E('div', { 'class': 'ta-message-time' }, new Date().toLocaleTimeString())
|
|
]));
|
|
messages.scrollTop = messages.scrollHeight;
|
|
}).catch(function(err) {
|
|
var loadingEl = document.getElementById('ta-chat-loading');
|
|
if (loadingEl) loadingEl.remove();
|
|
|
|
messages.appendChild(E('div', { 'class': 'ta-message ai' }, [
|
|
E('div', { 'class': 'ta-message-bubble' }, 'Error: ' + (err.message || 'Request failed'))
|
|
]));
|
|
});
|
|
},
|
|
|
|
runAnalysis: function() {
|
|
ui.showModal('Running Analysis', [
|
|
E('p', { 'class': 'spinning' }, 'Running threat analysis cycle...')
|
|
]);
|
|
|
|
api.runCycle().then(function() {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, 'Analysis cycle started'), 'success');
|
|
}).catch(function() {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, 'Failed to start analysis'), 'error');
|
|
});
|
|
},
|
|
|
|
generateRules: function(target) {
|
|
var targetName = target === 'all' ? 'All' : target;
|
|
ui.showModal('Generating Rules', [
|
|
E('p', { 'class': 'spinning' }, 'Generating ' + targetName + ' rules with AI...')
|
|
]);
|
|
|
|
api.generateRules(target).then(function(result) {
|
|
ui.hideModal();
|
|
if (result.rules) {
|
|
ui.addNotification(null, E('p', {}, 'Rules generated. Check pending queue for approval.'), 'success');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, 'No rules generated'), 'warning');
|
|
}
|
|
}).catch(function() {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, 'Failed to generate rules'), 'error');
|
|
});
|
|
},
|
|
|
|
approveRule: function(id) {
|
|
api.approveRule(id).then(function(result) {
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', {}, 'Rule approved and applied'), 'success');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, 'Failed to approve rule'), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
rejectRule: function(id) {
|
|
api.rejectRule(id).then(function() {
|
|
ui.addNotification(null, E('p', {}, 'Rule rejected'), 'info');
|
|
window.location.reload();
|
|
});
|
|
},
|
|
|
|
pollData: function() {
|
|
var self = this;
|
|
return api.getOverview().then(function(data) {
|
|
var s = data.status || {};
|
|
var pending = data.pending || [];
|
|
var threats = data.threats || [];
|
|
|
|
var el = document.getElementById('ta-stats');
|
|
if (el) dom.content(el, self.renderStats(s, pending));
|
|
|
|
el = document.getElementById('ta-pending');
|
|
if (el) dom.content(el, self.renderPending(pending));
|
|
|
|
el = document.getElementById('ta-threats');
|
|
if (el) dom.content(el, self.renderThreats(threats));
|
|
});
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|