Convert 90+ LuCI view files from legacy cbi-button-* classes to KissTheme kiss-btn-* classes for consistent dark theme styling. Pattern conversions applied: - cbi-button-positive → kiss-btn-green - cbi-button-negative/remove → kiss-btn-red - cbi-button-apply → kiss-btn-cyan - cbi-button-action → kiss-btn-blue - cbi-button (plain) → kiss-btn Also replaced hardcoded colors (#080, #c00, #888, etc.) with CSS variables (--kiss-green, --kiss-red, --kiss-muted, etc.) for proper dark theme compatibility. Apps updated include: ai-gateway, auth-guardian, bandwidth-manager, cloner, config-advisor, crowdsec-dashboard, dns-provider, exposure, glances, haproxy, hexojs, iot-guard, jellyfin, ksm-manager, mac-guardian, magicmirror2, master-link, meshname-dns, metablogizer, metabolizer, mqtt-bridge, netdata-dashboard, picobrew, routes-status, secubox-admin, secubox-mirror, secubox-p2p, secubox-security-threats, service-registry, simplex, streamlit, system-hub, tor-shield, traffic-shaper, vhost-manager, vortex-dns, vortex-firewall, webradio, wireguard-dashboard, zigbee2mqtt, zkp, and more. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
407 lines
13 KiB
JavaScript
407 lines
13 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require ui';
|
|
'require dom';
|
|
'require poll';
|
|
'require request';
|
|
'require secubox/kiss-theme';
|
|
|
|
var API_BASE = window.location.protocol + '//' + window.location.hostname + ':7331/api/threat-intel';
|
|
|
|
function fetchJSON(endpoint) {
|
|
return request.get(API_BASE + '/' + endpoint, { timeout: 10000 })
|
|
.then(function(res) {
|
|
try { return res.json(); }
|
|
catch(e) { return null; }
|
|
})
|
|
.catch(function() { return null; });
|
|
}
|
|
|
|
function postJSON(endpoint) {
|
|
return request.post(API_BASE + '/' + endpoint, null, { timeout: 15000 })
|
|
.then(function(res) {
|
|
try { return res.json(); }
|
|
catch(e) { return null; }
|
|
})
|
|
.catch(function() { return null; });
|
|
}
|
|
|
|
function timeAgo(ts) {
|
|
if (!ts || ts === 0) return 'Never';
|
|
var now = Math.floor(Date.now() / 1000);
|
|
var diff = now - ts;
|
|
if (diff < 60) return diff + 's ago';
|
|
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
|
return Math.floor(diff / 86400) + 'd ago';
|
|
}
|
|
|
|
function severityBadge(severity) {
|
|
var colors = {
|
|
critical: 'red',
|
|
high: 'orange',
|
|
medium: 'purple',
|
|
low: 'blue'
|
|
};
|
|
var colorType = colors[severity] || 'muted';
|
|
return KissTheme.badge(severity || 'unknown', colorType);
|
|
}
|
|
|
|
function trustBadge(trust) {
|
|
var colors = {
|
|
direct: 'green',
|
|
transitive: 'orange',
|
|
unknown: 'muted',
|
|
self: 'blue'
|
|
};
|
|
var icons = {
|
|
direct: '\u2714',
|
|
transitive: '\u2194',
|
|
unknown: '?',
|
|
self: '\u2605'
|
|
};
|
|
var colorType = colors[trust] || 'muted';
|
|
return KissTheme.badge((icons[trust] || '') + ' ' + (trust || 'unknown'), colorType);
|
|
}
|
|
|
|
return view.extend({
|
|
status: null,
|
|
iocs: [],
|
|
peers: [],
|
|
|
|
load: function() {
|
|
return Promise.all([
|
|
fetchJSON('status'),
|
|
fetchJSON('iocs'),
|
|
fetchJSON('peers')
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
this.status = data[0] || {};
|
|
this.iocs = data[1] || [];
|
|
this.peers = data[2] || [];
|
|
|
|
var self = this;
|
|
|
|
poll.add(function() {
|
|
return Promise.all([
|
|
fetchJSON('status'),
|
|
fetchJSON('iocs'),
|
|
fetchJSON('peers')
|
|
]).then(function(fresh) {
|
|
self.status = fresh[0] || self.status;
|
|
self.iocs = fresh[1] || self.iocs;
|
|
self.peers = fresh[2] || self.peers;
|
|
self.updateCards();
|
|
self.updatePeerTable();
|
|
self.updateIOCTable();
|
|
});
|
|
}, 30);
|
|
|
|
var content = [
|
|
this.renderHeader(),
|
|
this.renderSummaryCards(),
|
|
this.renderActions(),
|
|
this.renderPeerTable(),
|
|
this.renderIOCTable()
|
|
];
|
|
|
|
return KissTheme.wrap(content, 'admin/secubox/p2p/threat-hub');
|
|
},
|
|
|
|
renderHeader: function() {
|
|
var enabled = this.status.enabled !== false;
|
|
return E('div', { 'style': 'margin-bottom: 24px;' }, [
|
|
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [
|
|
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'Threat Intelligence Hub'),
|
|
KissTheme.badge(enabled ? 'Active' : 'Disabled', enabled ? 'green' : 'red'),
|
|
this.status.auto_apply ? KissTheme.badge('Auto-Apply ON', 'blue') : null
|
|
]),
|
|
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' },
|
|
'Decentralized IOC sharing across mesh nodes via CrowdSec + mitmproxy')
|
|
]);
|
|
},
|
|
|
|
renderStats: function() {
|
|
var s = this.status;
|
|
var c = KissTheme.colors;
|
|
return [
|
|
KissTheme.stat(s.local_iocs || 0, 'Local IOCs Shared', c.blue),
|
|
KissTheme.stat(s.received_iocs || 0, 'Received from Mesh', c.orange),
|
|
KissTheme.stat(s.applied_iocs || 0, 'Applied to Firewall', c.green),
|
|
KissTheme.stat(s.peer_contributors || 0, 'Peer Contributors', c.purple)
|
|
];
|
|
},
|
|
|
|
renderSummaryCards: function() {
|
|
var s = this.status;
|
|
var c = KissTheme.colors;
|
|
var cards = [
|
|
{ id: 'card-local', label: 'Local IOCs Shared', value: s.local_iocs || 0, color: c.blue },
|
|
{ id: 'card-received', label: 'Received from Mesh', value: s.received_iocs || 0, color: c.orange },
|
|
{ id: 'card-applied', label: 'Applied to Firewall', value: s.applied_iocs || 0, color: c.green },
|
|
{ id: 'card-peers', label: 'Peer Contributors', value: s.peer_contributors || 0, color: c.purple },
|
|
{ id: 'card-chain', label: 'Chain Blocks', value: s.chain_threat_blocks || 0, color: c.cyan }
|
|
];
|
|
|
|
return E('div', {
|
|
'id': 'summary-cards',
|
|
'class': 'kiss-grid kiss-grid-4',
|
|
'style': 'margin-bottom: 24px;'
|
|
}, cards.map(function(card) {
|
|
return E('div', {
|
|
'id': card.id,
|
|
'style': 'background: var(--kiss-bg2); border: 1px solid var(--kiss-line); border-radius: 12px; ' +
|
|
'padding: 16px; text-align: center; border-left: 4px solid ' + card.color + ';'
|
|
}, [
|
|
E('div', {
|
|
'class': 'card-value',
|
|
'style': 'font-size: 32px; font-weight: bold; color: ' + card.color + ';'
|
|
}, String(card.value)),
|
|
E('div', { 'style': 'font-size: 12px; color: var(--kiss-muted); margin-top: 4px;' }, card.label)
|
|
]);
|
|
}));
|
|
},
|
|
|
|
renderActions: function() {
|
|
var self = this;
|
|
|
|
return KissTheme.card('Actions',
|
|
E('div', { 'style': 'display: flex; gap: 12px;' }, [
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-blue',
|
|
'click': function() {
|
|
this.disabled = true;
|
|
this.textContent = 'Publishing...';
|
|
var btn = this;
|
|
postJSON('publish').then(function(res) {
|
|
btn.disabled = false;
|
|
btn.textContent = 'Publish Now';
|
|
if (res && res.success)
|
|
ui.addNotification(null, E('p', 'Published ' + (res.published || 0) + ' IOCs to chain'), 'info');
|
|
else
|
|
ui.addNotification(null, E('p', 'Publish failed'), 'error');
|
|
});
|
|
}
|
|
}, 'Publish Now'),
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-green',
|
|
'click': function() {
|
|
this.disabled = true;
|
|
this.textContent = 'Applying...';
|
|
var btn = this;
|
|
postJSON('apply').then(function(res) {
|
|
btn.disabled = false;
|
|
btn.textContent = 'Apply Pending';
|
|
if (res && res.success)
|
|
ui.addNotification(null, E('p', 'Applied ' + (res.applied || 0) + ' IOCs, skipped ' + (res.skipped || 0)), 'info');
|
|
else
|
|
ui.addNotification(null, E('p', 'Apply failed'), 'error');
|
|
});
|
|
}
|
|
}, 'Apply Pending')
|
|
])
|
|
);
|
|
},
|
|
|
|
renderPeerTable: function() {
|
|
var peers = this.peers || [];
|
|
|
|
var rows = peers.map(function(p) {
|
|
return E('tr', {}, [
|
|
E('td', { 'style': 'padding: 10px 12px; font-family: monospace; font-size: 13px;' },
|
|
(p.node || '').substring(0, 12) + '...'),
|
|
E('td', { 'style': 'padding: 10px 12px;' },
|
|
trustBadge(p.trust)),
|
|
E('td', { 'style': 'padding: 10px 12px; text-align: center;' },
|
|
String(p.ioc_count || 0)),
|
|
E('td', { 'style': 'padding: 10px 12px; text-align: center;' },
|
|
String(p.applied_count || 0)),
|
|
E('td', { 'style': 'padding: 10px 12px; color: var(--kiss-muted); font-size: 12px;' },
|
|
timeAgo(p.last_seen))
|
|
]);
|
|
});
|
|
|
|
var tableContent;
|
|
if (rows.length === 0) {
|
|
tableContent = E('p', { 'style': 'color: var(--kiss-muted); text-align: center; padding: 24px;' },
|
|
'No peer contributions yet. IOCs from mesh nodes will appear here after sync.');
|
|
} else {
|
|
tableContent = E('table', { 'id': 'peer-table', 'class': 'kiss-table' }, [
|
|
E('thead', {}, E('tr', {}, [
|
|
E('th', { 'style': 'padding: 10px 12px;' }, 'Node'),
|
|
E('th', { 'style': 'padding: 10px 12px;' }, 'Trust'),
|
|
E('th', { 'style': 'padding: 10px 12px; text-align: center;' }, 'IOCs'),
|
|
E('th', { 'style': 'padding: 10px 12px; text-align: center;' }, 'Applied'),
|
|
E('th', { 'style': 'padding: 10px 12px;' }, 'Last Seen')
|
|
])),
|
|
E('tbody', {}, rows)
|
|
]);
|
|
}
|
|
|
|
return KissTheme.card(
|
|
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
|
|
E('span', {}, 'Peer Contributions'),
|
|
KissTheme.badge(peers.length + ' peers', 'purple')
|
|
]),
|
|
tableContent
|
|
);
|
|
},
|
|
|
|
renderIOCTable: function() {
|
|
var iocs = this.iocs || [];
|
|
var c = KissTheme.colors;
|
|
|
|
var rows = iocs.slice(0, 50).map(function(ioc) {
|
|
return E('tr', {}, [
|
|
E('td', { 'style': 'padding: 10px 12px; font-family: monospace; font-size: 13px;' },
|
|
ioc.ip || '-'),
|
|
E('td', { 'style': 'padding: 10px 12px;' },
|
|
severityBadge(ioc.severity)),
|
|
E('td', { 'style': 'padding: 10px 12px; color: var(--kiss-muted); font-size: 12px;' },
|
|
ioc.source || '-'),
|
|
E('td', { 'style': 'padding: 10px 12px; color: var(--kiss-muted); font-size: 12px;' },
|
|
ioc.scenario || '-'),
|
|
E('td', { 'style': 'padding: 10px 12px; font-family: monospace; font-size: 11px; color: var(--kiss-muted);' },
|
|
(ioc.node || '').substring(0, 12)),
|
|
E('td', { 'style': 'padding: 10px 12px;' },
|
|
trustBadge(ioc.trust)),
|
|
E('td', { 'style': 'padding: 10px 12px; text-align: center;' },
|
|
ioc.applied ?
|
|
E('span', { 'style': 'color: ' + c.green + '; font-weight: bold;' }, '\u2714') :
|
|
E('span', { 'style': 'color: var(--kiss-muted);' }, '\u2013')),
|
|
E('td', { 'style': 'padding: 10px 12px; color: var(--kiss-muted); font-size: 12px;' },
|
|
timeAgo(ioc.ts))
|
|
]);
|
|
});
|
|
|
|
var tableContent;
|
|
if (rows.length === 0) {
|
|
tableContent = E('p', { 'style': 'color: var(--kiss-muted); text-align: center; padding: 24px;' },
|
|
'No IOCs received from mesh yet.');
|
|
} else {
|
|
tableContent = E('div', {}, [
|
|
E('p', { 'style': 'color: var(--kiss-muted); font-size: 12px; margin: 0 0 12px 0;' },
|
|
'Showing up to 50 most recent. Total: ' + (this.iocs || []).length),
|
|
E('div', { 'style': 'overflow-x: auto;' }, [
|
|
E('table', { 'id': 'ioc-table', 'class': 'kiss-table' }, [
|
|
E('thead', {}, E('tr', {}, [
|
|
E('th', { 'style': 'padding: 10px 12px;' }, 'IP'),
|
|
E('th', { 'style': 'padding: 10px 12px;' }, 'Severity'),
|
|
E('th', { 'style': 'padding: 10px 12px;' }, 'Source'),
|
|
E('th', { 'style': 'padding: 10px 12px;' }, 'Scenario'),
|
|
E('th', { 'style': 'padding: 10px 12px;' }, 'Origin'),
|
|
E('th', { 'style': 'padding: 10px 12px;' }, 'Trust'),
|
|
E('th', { 'style': 'padding: 10px 12px; text-align: center;' }, 'Applied'),
|
|
E('th', { 'style': 'padding: 10px 12px;' }, 'Age')
|
|
])),
|
|
E('tbody', {}, rows)
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
|
|
return KissTheme.card(
|
|
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
|
|
E('span', {}, 'Received IOCs'),
|
|
KissTheme.badge(iocs.length + ' total', 'orange')
|
|
]),
|
|
tableContent
|
|
);
|
|
},
|
|
|
|
updateCards: function() {
|
|
var s = this.status || {};
|
|
var mapping = {
|
|
'card-local': s.local_iocs || 0,
|
|
'card-received': s.received_iocs || 0,
|
|
'card-applied': s.applied_iocs || 0,
|
|
'card-peers': s.peer_contributors || 0,
|
|
'card-chain': s.chain_threat_blocks || 0
|
|
};
|
|
|
|
Object.keys(mapping).forEach(function(id) {
|
|
var card = document.getElementById(id);
|
|
if (card) {
|
|
var valEl = card.querySelector('.card-value');
|
|
if (valEl) valEl.textContent = String(mapping[id]);
|
|
}
|
|
});
|
|
},
|
|
|
|
updatePeerTable: function() {
|
|
var table = document.getElementById('peer-table');
|
|
if (!table) return;
|
|
var tbody = table.querySelector('tbody');
|
|
if (!tbody) return;
|
|
|
|
var peers = this.peers || [];
|
|
dom.content(tbody, peers.length === 0 ?
|
|
E('tr', {}, [
|
|
E('td', { 'colspan': '5', 'style': 'padding: 24px; text-align: center; color: var(--kiss-muted);' },
|
|
'No peer contributions yet.')
|
|
]) :
|
|
peers.map(function(p) {
|
|
return E('tr', {}, [
|
|
E('td', { 'style': 'padding: 10px 12px; font-family: monospace; font-size: 13px;' },
|
|
(p.node || '').substring(0, 12) + '...'),
|
|
E('td', { 'style': 'padding: 10px 12px;' },
|
|
trustBadge(p.trust)),
|
|
E('td', { 'style': 'padding: 10px 12px; text-align: center;' },
|
|
String(p.ioc_count || 0)),
|
|
E('td', { 'style': 'padding: 10px 12px; text-align: center;' },
|
|
String(p.applied_count || 0)),
|
|
E('td', { 'style': 'padding: 10px 12px; color: var(--kiss-muted); font-size: 12px;' },
|
|
timeAgo(p.last_seen))
|
|
]);
|
|
})
|
|
);
|
|
},
|
|
|
|
updateIOCTable: function() {
|
|
var table = document.getElementById('ioc-table');
|
|
if (!table) return;
|
|
var tbody = table.querySelector('tbody');
|
|
if (!tbody) return;
|
|
|
|
var iocs = (this.iocs || []).slice(0, 50);
|
|
var c = KissTheme.colors;
|
|
var countEl = table.parentNode.parentNode.querySelector('p');
|
|
if (countEl) countEl.textContent = 'Showing up to 50 most recent. Total: ' + (this.iocs || []).length;
|
|
|
|
dom.content(tbody, iocs.length === 0 ?
|
|
E('tr', {}, [
|
|
E('td', { 'colspan': '8', 'style': 'padding: 24px; text-align: center; color: var(--kiss-muted);' },
|
|
'No IOCs received from mesh yet.')
|
|
]) :
|
|
iocs.map(function(ioc) {
|
|
return E('tr', {}, [
|
|
E('td', { 'style': 'padding: 10px 12px; font-family: monospace; font-size: 13px;' },
|
|
ioc.ip || '-'),
|
|
E('td', { 'style': 'padding: 10px 12px;' },
|
|
severityBadge(ioc.severity)),
|
|
E('td', { 'style': 'padding: 10px 12px; color: var(--kiss-muted); font-size: 12px;' },
|
|
ioc.source || '-'),
|
|
E('td', { 'style': 'padding: 10px 12px; color: var(--kiss-muted); font-size: 12px;' },
|
|
ioc.scenario || '-'),
|
|
E('td', { 'style': 'padding: 10px 12px; font-family: monospace; font-size: 11px; color: var(--kiss-muted);' },
|
|
(ioc.node || '').substring(0, 12)),
|
|
E('td', { 'style': 'padding: 10px 12px;' },
|
|
trustBadge(ioc.trust)),
|
|
E('td', { 'style': 'padding: 10px 12px; text-align: center;' },
|
|
ioc.applied ?
|
|
E('span', { 'style': 'color: ' + c.green + '; font-weight: bold;' }, '\u2714') :
|
|
E('span', { 'style': 'color: var(--kiss-muted);' }, '\u2013')),
|
|
E('td', { 'style': 'padding: 10px 12px; color: var(--kiss-muted); font-size: 12px;' },
|
|
timeAgo(ioc.ts))
|
|
]);
|
|
})
|
|
);
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|