diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5a76f7d9..3ef5baf9 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -232,7 +232,30 @@ "Bash(# Find usign keys find ~/CyberMindStudio/secubox-openwrt -name \"\"*.key\"\")", "Bash(cd ~/CyberMindStudio/secubox-openwrt/package/secubox/secubox-app-bonus/root/www/secubox-feed ls -la Packages* luci-app-secubox-security-threats*.ipk echo \"\" grep -A3 \"Package: luci-app-secubox-security-threats\" Packages)", "Bash(docker pull:*)", - "Bash(do echo \"=== $f ===\")" + "Bash(do echo \"=== $f ===\")", + "WebFetch(domain:fabianlee.org)", + "Bash(do grep \"^PKG_NAME\" \"$f\")", + "WebFetch(domain:sysupgrade.openwrt.org)", + "Bash(./secubox-tools/secubox-image.sh:*)", + "Bash(source:*)", + "Bash(build_request_json:*)", + "Bash(bash -c '\nSCRIPT_DIR=\"\"$\\(cd secubox-tools && pwd\\)\"\"\n\n# Inline the function\ngenerate_defaults\\(\\) {\n cat <<'\"''\"'DEFAULTS'\"''\"'\n#!/bin/sh\ntest line\nDEFAULTS\n}\n\ndefaults_file=$\\(mktemp\\)\ngenerate_defaults > \"\"$defaults_file\"\"\ncat \"\"$defaults_file\"\"\necho \"\"---\"\"\npython3 -c \"\"\nimport json\nwith open\\('\"''\"'$defaults_file'\"''\"'\\) as f:\n print\\(json.dumps\\(f.read\\(\\)\\)\\)\n\"\"\nrm -f \"\"$defaults_file\"\"\n')", + "Bash(bash:*)", + "Bash(sudo ./secubox-tools/secubox-image.sh:*)", + "Bash(DISPLAY=:0 gnome-screenshot:*)", + "Bash(DISPLAY=:0 scrot:*)", + "Bash(DISPLAY=:1 gnome-screenshot:*)", + "Bash(DISPLAY=:1 import -window root /tmp/claude/-home-reepost-CyberMindStudio-secubox-openwrt/d82d04b5-7250-4f1b-99cf-0bb5c38ac8e8/scratchpad/master-link-landing.png)", + "Bash(DISPLAY=:1 xdpyinfo:*)", + "Bash(DISPLAY=:0 xdpyinfo:*)", + "Bash(DISPLAY=:1 xdotool search:*)", + "Bash(DISPLAY=:1 wmctrl:*)", + "Bash(DISPLAY=:1 xdotool:*)", + "Bash(while read wid)", + "Bash(do DISPLAY=:1 xdotool getwindowname \"$wid\")", + "Bash(dpkg:*)", + "Bash(cosmic-screenshot:*)", + "Bash(do bash -n \"$f\")" ] } } diff --git a/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/threat-hub.js b/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/threat-hub.js new file mode 100644 index 00000000..1b6672ba --- /dev/null +++ b/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/threat-hub.js @@ -0,0 +1,406 @@ +'use strict'; +'require view'; +'require ui'; +'require dom'; +'require poll'; +'require request'; + +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: '#e74c3c', + high: '#e67e22', + medium: '#f1c40f', + low: '#3498db' + }; + var color = colors[severity] || '#95a5a6'; + return E('span', { + 'style': 'display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:bold;color:#fff;background:' + color + }, severity || 'unknown'); +} + +function trustBadge(trust) { + var colors = { + direct: '#27ae60', + transitive: '#f39c12', + unknown: '#95a5a6', + self: '#3498db' + }; + var icons = { + direct: '\u2714', + transitive: '\u2194', + unknown: '?', + self: '\u2605' + }; + var color = colors[trust] || '#95a5a6'; + return E('span', { + 'style': 'display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:bold;color:#fff;background:' + color + }, (icons[trust] || '') + ' ' + (trust || 'unknown')); +} + +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; + + var view = E('div', { 'class': 'cbi-map', 'style': 'padding:20px;' }, [ + this.renderHeader(), + this.renderSummaryCards(), + this.renderActions(), + this.renderPeerTable(), + this.renderIOCTable() + ]); + + 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); + + return view; + }, + + renderHeader: function() { + var enabled = this.status.enabled !== false; + return E('div', { 'style': 'margin-bottom:24px;' }, [ + E('h2', { 'style': 'margin:0 0 8px;color:#ecf0f1;font-size:24px;' }, + 'Threat Intelligence Hub'), + E('p', { 'style': 'margin:0;color:#95a5a6;font-size:14px;' }, + 'Decentralized IOC sharing across mesh nodes via CrowdSec + mitmproxy'), + E('div', { 'style': 'margin-top:8px;' }, [ + E('span', { + 'style': 'display:inline-block;padding:4px 12px;border-radius:12px;font-size:12px;font-weight:bold;color:#fff;background:' + (enabled ? '#27ae60' : '#e74c3c') + }, enabled ? 'ACTIVE' : 'DISABLED'), + this.status.auto_apply ? + E('span', { 'style': 'display:inline-block;margin-left:8px;padding:4px 12px;border-radius:12px;font-size:12px;color:#fff;background:#2980b9;' }, 'Auto-Apply ON') : null + ]) + ]); + }, + + renderSummaryCards: function() { + var s = this.status; + var cards = [ + { id: 'card-local', label: 'Local IOCs Shared', value: s.local_iocs || 0, icon: '\uD83D\uDCE4', color: '#3498db' }, + { id: 'card-received', label: 'Received from Mesh', value: s.received_iocs || 0, icon: '\uD83D\uDCE5', color: '#e67e22' }, + { id: 'card-applied', label: 'Applied to Firewall', value: s.applied_iocs || 0, icon: '\uD83D\uDEE1', color: '#27ae60' }, + { id: 'card-peers', label: 'Peer Contributors', value: s.peer_contributors || 0, icon: '\uD83D\uDC65', color: '#9b59b6' }, + { id: 'card-chain', label: 'Chain Blocks', value: s.chain_threat_blocks || 0, icon: '\u26D3', color: '#1abc9c' } + ]; + + return E('div', { + 'id': 'summary-cards', + 'style': 'display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:16px;margin-bottom:24px;' + }, cards.map(function(c) { + return E('div', { + 'id': c.id, + 'style': 'background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:12px;padding:16px;text-align:center;border-left:4px solid ' + c.color + ';' + }, [ + E('div', { 'style': 'font-size:28px;margin-bottom:4px;' }, c.icon), + E('div', { + 'class': 'card-value', + 'style': 'font-size:32px;font-weight:bold;color:' + c.color + ';' + }, String(c.value)), + E('div', { 'style': 'font-size:12px;color:#95a5a6;margin-top:4px;' }, c.label) + ]); + })); + }, + + renderActions: function() { + var self = this; + + var publishBtn = E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'margin-right:12px;padding:8px 20px;', + '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'); + + var applyBtn = E('button', { + 'class': 'cbi-button cbi-button-apply', + 'style': 'padding:8px 20px;', + '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'); + + return E('div', { 'style': 'margin-bottom:24px;padding:16px;background:rgba(255,255,255,0.03);border-radius:8px;border:1px solid rgba(255,255,255,0.08);' }, [ + E('h3', { 'style': 'margin:0 0 12px;color:#ecf0f1;font-size:16px;' }, 'Actions'), + E('div', {}, [publishBtn, applyBtn]) + ]); + }, + + renderPeerTable: function() { + var peers = this.peers || []; + + var rows = peers.map(function(p) { + return E('tr', {}, [ + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);font-family:monospace;font-size:13px;color:#ecf0f1;' }, + (p.node || '').substring(0, 12) + '...'), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);' }, + trustBadge(p.trust)), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#ecf0f1;text-align:center;' }, + String(p.ioc_count || 0)), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#ecf0f1;text-align:center;' }, + String(p.applied_count || 0)), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#95a5a6;font-size:12px;' }, + timeAgo(p.last_seen)) + ]); + }); + + if (rows.length === 0) { + rows = [E('tr', {}, [ + E('td', { 'colspan': '5', 'style': 'padding:24px;text-align:center;color:#95a5a6;' }, + 'No peer contributions yet. IOCs from mesh nodes will appear here after sync.') + ])]; + } + + return E('div', { 'style': 'margin-bottom:24px;' }, [ + E('h3', { 'style': 'margin:0 0 12px;color:#ecf0f1;font-size:16px;' }, 'Peer Contributions'), + E('div', { 'style': 'overflow-x:auto;' }, [ + E('table', { + 'id': 'peer-table', + 'class': 'table', + 'style': 'width:100%;border-collapse:collapse;background:rgba(255,255,255,0.03);border-radius:8px;border:1px solid rgba(255,255,255,0.08);' + }, [ + E('thead', {}, [ + E('tr', { 'style': 'background:rgba(255,255,255,0.05);' }, [ + E('th', { 'style': 'padding:10px 12px;text-align:left;color:#95a5a6;font-size:12px;text-transform:uppercase;' }, 'Node'), + E('th', { 'style': 'padding:10px 12px;text-align:left;color:#95a5a6;font-size:12px;text-transform:uppercase;' }, 'Trust'), + E('th', { 'style': 'padding:10px 12px;text-align:center;color:#95a5a6;font-size:12px;text-transform:uppercase;' }, 'IOCs'), + E('th', { 'style': 'padding:10px 12px;text-align:center;color:#95a5a6;font-size:12px;text-transform:uppercase;' }, 'Applied'), + E('th', { 'style': 'padding:10px 12px;text-align:left;color:#95a5a6;font-size:12px;text-transform:uppercase;' }, 'Last Seen') + ]) + ]), + E('tbody', {}, rows) + ]) + ]) + ]); + }, + + renderIOCTable: function() { + var iocs = this.iocs || []; + + var rows = iocs.slice(0, 50).map(function(ioc) { + return E('tr', {}, [ + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);font-family:monospace;font-size:13px;color:#ecf0f1;' }, + ioc.ip || '-'), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);' }, + severityBadge(ioc.severity)), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#95a5a6;font-size:12px;' }, + ioc.source || '-'), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#95a5a6;font-size:12px;' }, + ioc.scenario || '-'), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);font-family:monospace;font-size:11px;color:#7f8c8d;' }, + (ioc.node || '').substring(0, 12)), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);' }, + trustBadge(ioc.trust)), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);text-align:center;' }, + ioc.applied ? + E('span', { 'style': 'color:#27ae60;font-weight:bold;' }, '\u2714') : + E('span', { 'style': 'color:#95a5a6;' }, '\u2013')), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#95a5a6;font-size:12px;' }, + timeAgo(ioc.ts)) + ]); + }); + + if (rows.length === 0) { + rows = [E('tr', {}, [ + E('td', { 'colspan': '8', 'style': 'padding:24px;text-align:center;color:#95a5a6;' }, + 'No IOCs received from mesh yet.') + ])]; + } + + return E('div', {}, [ + E('h3', { 'style': 'margin:0 0 4px;color:#ecf0f1;font-size:16px;' }, 'Received IOCs'), + E('p', { 'style': 'margin:0 0 12px;color:#7f8c8d;font-size:12px;' }, + 'Showing up to 50 most recent. Total: ' + (this.iocs || []).length), + E('div', { 'style': 'overflow-x:auto;' }, [ + E('table', { + 'id': 'ioc-table', + 'class': 'table', + 'style': 'width:100%;border-collapse:collapse;background:rgba(255,255,255,0.03);border-radius:8px;border:1px solid rgba(255,255,255,0.08);' + }, [ + E('thead', {}, [ + E('tr', { 'style': 'background:rgba(255,255,255,0.05);' }, [ + E('th', { 'style': 'padding:10px 12px;text-align:left;color:#95a5a6;font-size:12px;text-transform:uppercase;' }, 'IP'), + E('th', { 'style': 'padding:10px 12px;text-align:left;color:#95a5a6;font-size:12px;text-transform:uppercase;' }, 'Severity'), + E('th', { 'style': 'padding:10px 12px;text-align:left;color:#95a5a6;font-size:12px;text-transform:uppercase;' }, 'Source'), + E('th', { 'style': 'padding:10px 12px;text-align:left;color:#95a5a6;font-size:12px;text-transform:uppercase;' }, 'Scenario'), + E('th', { 'style': 'padding:10px 12px;text-align:left;color:#95a5a6;font-size:12px;text-transform:uppercase;' }, 'Origin'), + E('th', { 'style': 'padding:10px 12px;text-align:left;color:#95a5a6;font-size:12px;text-transform:uppercase;' }, 'Trust'), + E('th', { 'style': 'padding:10px 12px;text-align:center;color:#95a5a6;font-size:12px;text-transform:uppercase;' }, 'Applied'), + E('th', { 'style': 'padding:10px 12px;text-align:left;color:#95a5a6;font-size:12px;text-transform:uppercase;' }, 'Age') + ]) + ]), + E('tbody', {}, rows) + ]) + ]) + ]); + }, + + 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:#95a5a6;' }, + 'No peer contributions yet.') + ]) : + peers.map(function(p) { + return E('tr', {}, [ + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);font-family:monospace;font-size:13px;color:#ecf0f1;' }, + (p.node || '').substring(0, 12) + '...'), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);' }, + trustBadge(p.trust)), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#ecf0f1;text-align:center;' }, + String(p.ioc_count || 0)), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#ecf0f1;text-align:center;' }, + String(p.applied_count || 0)), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#95a5a6;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 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:#95a5a6;' }, + 'No IOCs received from mesh yet.') + ]) : + iocs.map(function(ioc) { + return E('tr', {}, [ + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);font-family:monospace;font-size:13px;color:#ecf0f1;' }, + ioc.ip || '-'), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);' }, + severityBadge(ioc.severity)), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#95a5a6;font-size:12px;' }, + ioc.source || '-'), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#95a5a6;font-size:12px;' }, + ioc.scenario || '-'), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);font-family:monospace;font-size:11px;color:#7f8c8d;' }, + (ioc.node || '').substring(0, 12)), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);' }, + trustBadge(ioc.trust)), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);text-align:center;' }, + ioc.applied ? + E('span', { 'style': 'color:#27ae60;font-weight:bold;' }, '\u2714') : + E('span', { 'style': 'color:#95a5a6;' }, '\u2013')), + E('td', { 'style': 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#95a5a6;font-size:12px;' }, + timeAgo(ioc.ts)) + ]); + }) + ); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-secubox-p2p/root/usr/share/luci/menu.d/luci-app-secubox-p2p.json b/package/secubox/luci-app-secubox-p2p/root/usr/share/luci/menu.d/luci-app-secubox-p2p.json index 2ed26d1d..c276d652 100644 --- a/package/secubox/luci-app-secubox-p2p/root/usr/share/luci/menu.d/luci-app-secubox-p2p.json +++ b/package/secubox/luci-app-secubox-p2p/root/usr/share/luci/menu.d/luci-app-secubox-p2p.json @@ -66,6 +66,14 @@ "path": "secubox-p2p/factory" } }, + "admin/secubox/mirrorbox/p2p-hub": { + "title": "Threat Intel", + "order": 25, + "action": { + "type": "view", + "path": "secubox-p2p/threat-hub" + } + }, "admin/secubox/mirrorbox/settings": { "title": "Settings", "order": 90, diff --git a/package/secubox/luci-app-secubox-p2p/root/usr/share/rpcd/acl.d/luci-app-secubox-p2p.json b/package/secubox/luci-app-secubox-p2p/root/usr/share/rpcd/acl.d/luci-app-secubox-p2p.json index 65c4c97d..fdecc9ba 100644 --- a/package/secubox/luci-app-secubox-p2p/root/usr/share/rpcd/acl.d/luci-app-secubox-p2p.json +++ b/package/secubox/luci-app-secubox-p2p/root/usr/share/rpcd/acl.d/luci-app-secubox-p2p.json @@ -23,7 +23,10 @@ ], "uci": ["get", "state"] }, - "uci": ["secubox-p2p"] + "uci": ["secubox-p2p"], + "file": { + "/var/lib/secubox/threat-intel/*": ["read"] + } }, "write": { "ubus": { diff --git a/package/secubox/secubox-p2p/root/etc/config/secubox-p2p b/package/secubox/secubox-p2p/root/etc/config/secubox-p2p index 015a9c9a..b28ff0df 100644 --- a/package/secubox/secubox-p2p/root/etc/config/secubox-p2p +++ b/package/secubox/secubox-p2p/root/etc/config/secubox-p2p @@ -57,3 +57,12 @@ config backup 'backup' option backup_dir '/etc/secubox/backups' option max_backups '10' option auto_cleanup '1' + +config threat_intel 'threat_intel' + option enabled '1' + option auto_apply '1' + option apply_transitive '1' + option min_severity 'high' + option collect_interval '900' + option max_iocs_per_batch '20' + option ioc_ttl '86400' diff --git a/package/secubox/secubox-p2p/root/etc/uci-defaults/99-secubox-p2p-api b/package/secubox/secubox-p2p/root/etc/uci-defaults/99-secubox-p2p-api index 66cac4c1..642cc05a 100644 --- a/package/secubox/secubox-p2p/root/etc/uci-defaults/99-secubox-p2p-api +++ b/package/secubox/secubox-p2p/root/etc/uci-defaults/99-secubox-p2p-api @@ -48,4 +48,19 @@ if ! uci show firewall 2>/dev/null | grep -q "mDNS"; then uci commit firewall fi +# Add threat-intel cron jobs if not already present +CRONTAB="/etc/crontabs/root" +[ -f "$CRONTAB" ] || touch "$CRONTAB" + +if ! grep -q "threat-intel.sh collect-and-publish" "$CRONTAB" 2>/dev/null; then + echo "*/15 * * * * /usr/lib/secubox/threat-intel.sh collect-and-publish" >> "$CRONTAB" +fi + +if ! grep -q "threat-intel.sh apply-pending" "$CRONTAB" 2>/dev/null; then + echo "*/30 * * * * /usr/lib/secubox/threat-intel.sh apply-pending" >> "$CRONTAB" +fi + +# Restart cron if running +/etc/init.d/cron restart 2>/dev/null || true + exit 0 diff --git a/package/secubox/secubox-p2p/root/usr/lib/secubox/threat-intel.sh b/package/secubox/secubox-p2p/root/usr/lib/secubox/threat-intel.sh new file mode 100755 index 00000000..a8b7e0aa --- /dev/null +++ b/package/secubox/secubox-p2p/root/usr/lib/secubox/threat-intel.sh @@ -0,0 +1,869 @@ +#!/bin/sh +# SecuBox Threat Intelligence - Decentralized IOC Sharing via P2P Mesh +# Shares CrowdSec bans and mitmproxy detections between mesh nodes +# IOCs propagate via the existing blockchain chain + gossip sync +# Copyright 2026 CyberMind - Licensed under MIT + +# Source mesh and factory libraries (suppress case-statement output) +. /usr/lib/secubox/p2p-mesh.sh >/dev/null 2>/dev/null +. /usr/lib/secubox/factory.sh >/dev/null 2>/dev/null + +# ============================================================================ +# Chain helper (fixes single-line JSON append in p2p-mesh.sh) +# ============================================================================ +_ti_chain_add_block() { + local block_type="$1" + local block_data="$2" + local block_hash="$3" + + local prev_hash=$(chain_get_hash) + local index=$(jsonfilter -i "$CHAIN_FILE" -e '@.blocks[*]' | wc -l) + local timestamp=$(date +%s) + local node_id=$(cat "$NODE_ID_FILE") + + local block_record="{\"index\":$index,\"timestamp\":$timestamp,\"type\":\"$block_type\",\"hash\":\"$block_hash\",\"prev_hash\":\"$prev_hash\",\"node\":\"$node_id\",\"data\":$block_data}" + + # Strip trailing ] } from LAST LINE only (avoids corrupting block data) + local tmp_chain="$MESH_DIR/tmp/chain_$$.json" + sed '$ s/ *\] *} *$//' "$CHAIN_FILE" > "$tmp_chain" + echo ", $block_record ] }" >> "$tmp_chain" + mv "$tmp_chain" "$CHAIN_FILE" + + echo "$block_hash" +} + +# ============================================================================ +# Configuration +# ============================================================================ +TI_DIR="/var/lib/secubox/threat-intel" +IOC_LOCAL="$TI_DIR/iocs-local.json" +IOC_RECEIVED="$TI_DIR/iocs-received.json" +IOC_APPLIED="$TI_DIR/iocs-applied.json" +TI_PROCESSED="$TI_DIR/processed-blocks.list" +TI_WHITELIST="$TI_DIR/whitelist.list" +TI_LOCK="/tmp/secubox-threat-intel.lock" + +# UCI defaults +TI_ENABLED="1" +TI_AUTO_APPLY="1" +TI_APPLY_TRANSITIVE="1" +TI_MIN_SEVERITY="high" +TI_COLLECT_INTERVAL="900" +TI_MAX_BATCH="20" +TI_IOC_TTL="86400" + +# ============================================================================ +# Initialization +# ============================================================================ +ti_init() { + mkdir -p "$TI_DIR" + + # Load UCI config + TI_ENABLED=$(uci -q get secubox-p2p.threat_intel.enabled || echo "1") + TI_AUTO_APPLY=$(uci -q get secubox-p2p.threat_intel.auto_apply || echo "1") + TI_APPLY_TRANSITIVE=$(uci -q get secubox-p2p.threat_intel.apply_transitive || echo "1") + TI_MIN_SEVERITY=$(uci -q get secubox-p2p.threat_intel.min_severity || echo "high") + TI_COLLECT_INTERVAL=$(uci -q get secubox-p2p.threat_intel.collect_interval || echo "900") + TI_MAX_BATCH=$(uci -q get secubox-p2p.threat_intel.max_iocs_per_batch || echo "20") + TI_IOC_TTL=$(uci -q get secubox-p2p.threat_intel.ioc_ttl || echo "86400") + + # Initialize JSON files if missing + [ -f "$IOC_LOCAL" ] || echo '[]' > "$IOC_LOCAL" + [ -f "$IOC_RECEIVED" ] || echo '[]' > "$IOC_RECEIVED" + [ -f "$IOC_APPLIED" ] || echo '[]' > "$IOC_APPLIED" + [ -f "$TI_PROCESSED" ] || touch "$TI_PROCESSED" + [ -f "$TI_WHITELIST" ] || touch "$TI_WHITELIST" +} + +# ============================================================================ +# Severity Helpers +# ============================================================================ +_severity_rank() { + case "$1" in + critical) echo 4 ;; + high) echo 3 ;; + medium) echo 2 ;; + low) echo 1 ;; + *) echo 0 ;; + esac +} + +_severity_meets_min() { + local sev="$1" + local min="$2" + local sev_rank=$(_severity_rank "$sev") + local min_rank=$(_severity_rank "$min") + [ "$sev_rank" -ge "$min_rank" ] +} + +# ============================================================================ +# Collection - CrowdSec +# ============================================================================ +ti_collect_crowdsec() { + command -v cscli >/dev/null 2>&1 || return 0 + + local now=$(date +%s) + local node_id=$(cat "$NODE_ID_FILE" 2>/dev/null || echo "unknown") + local decisions + + decisions=$(cscli decisions list -o json 2>/dev/null) + [ -z "$decisions" ] && return 0 + + # Parse decisions - each is an IP ban with scenario and duration + local count=0 + local iocs="[" + local first=1 + + echo "$decisions" | jsonfilter -e '@[*]' 2>/dev/null | while read -r decision; do + local ip=$(echo "$decision" | jsonfilter -e '@.value' 2>/dev/null) + local scenario=$(echo "$decision" | jsonfilter -e '@.scenario' 2>/dev/null) + local duration=$(echo "$decision" | jsonfilter -e '@.duration' 2>/dev/null) + + [ -z "$ip" ] && continue + + # Skip whitelisted IPs + grep -q "^${ip}$" "$TI_WHITELIST" 2>/dev/null && continue + + # Determine severity from scenario + local severity="high" + case "$scenario" in + *brute*|*exploit*|*scan*) severity="critical" ;; + *crawl*|*probe*) severity="high" ;; + *bad-user-agent*) severity="medium" ;; + esac + + # Normalize duration to seconds + local dur_secs="$TI_IOC_TTL" + case "$duration" in + *h) dur_secs=$(echo "$duration" | sed 's/h//' | awk '{print $1 * 3600}') ;; + *m) dur_secs=$(echo "$duration" | sed 's/m//' | awk '{print $1 * 60}') ;; + *s) dur_secs=$(echo "$duration" | sed 's/s//') ;; + esac + [ -z "$dur_secs" ] && dur_secs="$TI_IOC_TTL" + + echo "{\"ip\":\"$ip\",\"type\":\"ban\",\"severity\":\"$severity\",\"source\":\"crowdsec\",\"scenario\":\"$scenario\",\"duration\":\"${dur_secs}s\",\"ts\":$now,\"node\":\"$node_id\",\"ttl\":$dur_secs}" + done +} + +# ============================================================================ +# Collection - mitmproxy threats log +# ============================================================================ +ti_collect_mitmproxy() { + local threats_log="/srv/mitmproxy/threats.log" + [ -f "$threats_log" ] || return 0 + + local now=$(date +%s) + local node_id=$(cat "$NODE_ID_FILE" 2>/dev/null || echo "unknown") + local last_collect_pos="$TI_DIR/.mitmproxy-last-pos" + local last_pos=0 + [ -f "$last_collect_pos" ] && last_pos=$(cat "$last_collect_pos") + + # Get current file size; only process new lines since last run + local current_size=$(wc -c < "$threats_log" 2>/dev/null) + [ -z "$current_size" ] && return 0 + + # If file was truncated/rotated, reset position + [ "$last_pos" -gt "$current_size" ] 2>/dev/null && last_pos=0 + + # Read recent entries (tail last 5000 lines for coverage) + tail -n 5000 "$threats_log" | while read -r line; do + [ -z "$line" ] && continue + + # Parse JSON fields using jsonfilter + local ip=$(echo "$line" | jsonfilter -e '@.source_ip' 2>/dev/null) + [ -z "$ip" ] && continue + + # Skip private/local IPs + case "$ip" in + 192.168.*|10.*|172.1[6-9].*|172.2[0-9].*|172.3[01].*|127.*) continue ;; + esac + + local severity=$(echo "$line" | jsonfilter -e '@.severity' 2>/dev/null) + + # Only high and critical severity + case "$severity" in + critical|high) ;; + *) continue ;; + esac + + # Skip whitelisted IPs + grep -q "^${ip}$" "$TI_WHITELIST" 2>/dev/null && continue + + local scenario=$(echo "$line" | jsonfilter -e '@.type' 2>/dev/null || echo "unknown") + local pattern=$(echo "$line" | jsonfilter -e '@.pattern' 2>/dev/null) + [ -n "$pattern" ] && scenario="${scenario}:${pattern}" + + echo "{\"ip\":\"$ip\",\"type\":\"ban\",\"severity\":\"$severity\",\"source\":\"mitmproxy\",\"scenario\":\"$scenario\",\"duration\":\"${TI_IOC_TTL}s\",\"ts\":$now,\"node\":\"$node_id\",\"ttl\":$TI_IOC_TTL}" + done + + echo "$current_size" > "$last_collect_pos" +} + +# ============================================================================ +# Collection - Aggregate and deduplicate +# ============================================================================ +ti_collect_all() { + ti_init + + local tmp_file="$TI_DIR/tmp-collect-$$.json" + local existing_ips="" + + # Gather existing local IOC IPs for dedup + if [ -f "$IOC_LOCAL" ] && [ "$(cat "$IOC_LOCAL")" != "[]" ]; then + existing_ips=$(jsonfilter -i "$IOC_LOCAL" -e '@[*].ip' 2>/dev/null | sort -u) + fi + + # Collect from all sources + { + ti_collect_crowdsec + ti_collect_mitmproxy + } > "$tmp_file" + + # Deduplicate by IP against existing and within new results + local new_iocs="[" + local first=1 + local seen_ips="" + + while read -r ioc_line; do + [ -z "$ioc_line" ] && continue + + local ip=$(echo "$ioc_line" | jsonfilter -e '@.ip' 2>/dev/null) + [ -z "$ip" ] && continue + + # Skip if already in local IOCs + echo "$existing_ips" | grep -q "^${ip}$" && continue + + # Skip if already seen in this batch + echo "$seen_ips" | grep -q "^${ip}$" && continue + seen_ips="$seen_ips +$ip" + + [ $first -eq 0 ] && new_iocs="$new_iocs," + first=0 + new_iocs="$new_iocs$ioc_line" + done < "$tmp_file" + + new_iocs="$new_iocs]" + rm -f "$tmp_file" + + # Merge with existing local IOCs, pruning expired + local now=$(date +%s) + local merged="[" + local mfirst=1 + + # Keep non-expired existing IOCs + if [ -f "$IOC_LOCAL" ] && [ "$(cat "$IOC_LOCAL")" != "[]" ]; then + jsonfilter -i "$IOC_LOCAL" -e '@[*]' 2>/dev/null | while read -r existing; do + local ets=$(echo "$existing" | jsonfilter -e '@.ts' 2>/dev/null || echo "0") + local ettl=$(echo "$existing" | jsonfilter -e '@.ttl' 2>/dev/null || echo "$TI_IOC_TTL") + local expires=$((ets + ettl)) + [ "$now" -ge "$expires" ] && continue + echo "$existing" + done + fi > "$TI_DIR/tmp-existing-$$.json" + + # Rebuild local IOC list + local final="[" + local ffirst=1 + + while read -r line; do + [ -z "$line" ] && continue + [ $ffirst -eq 0 ] && final="$final," + ffirst=0 + final="$final$line" + done < "$TI_DIR/tmp-existing-$$.json" + + # Append new IOCs + echo "$new_iocs" | jsonfilter -e '@[*]' 2>/dev/null | while read -r nioc; do + echo "$nioc" + done > "$TI_DIR/tmp-new-$$.json" + + while read -r line; do + [ -z "$line" ] && continue + [ $ffirst -eq 0 ] && final="$final," + ffirst=0 + final="$final$line" + done < "$TI_DIR/tmp-new-$$.json" + + final="$final]" + echo "$final" > "$IOC_LOCAL" + + rm -f "$TI_DIR/tmp-existing-$$.json" "$TI_DIR/tmp-new-$$.json" + + local total=$(echo "$final" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) + logger -t threat-intel "Collected IOCs: $total local" +} + +# ============================================================================ +# Publishing - Push IOCs to chain as blocks +# ============================================================================ +ti_publish_iocs() { + ti_init + [ "$TI_ENABLED" != "1" ] && return 0 + + [ -f "$IOC_LOCAL" ] || return 0 + local count=$(jsonfilter -i "$IOC_LOCAL" -e '@[*]' 2>/dev/null | wc -l) + [ "$count" -eq 0 ] && return 0 + + local published_file="$TI_DIR/.published-hashes" + [ -f "$published_file" ] || touch "$published_file" + + # Dump IOCs to temp file to avoid pipe-subshell variable loss + local tmp_iocs="$TI_DIR/tmp-pub-$$.list" + jsonfilter -i "$IOC_LOCAL" -e '@[*]' > "$tmp_iocs" 2>/dev/null + + # Build batches of max TI_MAX_BATCH IOCs + local batch="[" + local batch_count=0 + local bfirst=1 + local total_published=0 + + while read -r ioc; do + [ -z "$ioc" ] && continue + local ioc_hash=$(echo "$ioc" | sha256sum | cut -c1-16) + + # Skip already published + grep -q "$ioc_hash" "$published_file" 2>/dev/null && continue + + [ $bfirst -eq 0 ] && batch="$batch," + bfirst=0 + batch="$batch$ioc" + batch_count=$((batch_count + 1)) + + echo "$ioc_hash" >> "$published_file" + + # Publish batch when full + if [ "$batch_count" -ge "$TI_MAX_BATCH" ]; then + batch="$batch]" + local block_data="{\"version\":1,\"count\":$batch_count,\"iocs\":$batch}" + local block_hash=$(echo "$block_data" | sha256sum | cut -d' ' -f1) + _ti_chain_add_block "threat_ioc" "$block_data" "$block_hash" + total_published=$((total_published + batch_count)) + + # Reset batch + batch="[" + batch_count=0 + bfirst=1 + fi + done < "$tmp_iocs" + + # Publish remaining IOCs + if [ "$batch_count" -gt 0 ]; then + batch="$batch]" + local block_data="{\"version\":1,\"count\":$batch_count,\"iocs\":$batch}" + local block_hash=$(echo "$block_data" | sha256sum | cut -d' ' -f1) + _ti_chain_add_block "threat_ioc" "$block_data" "$block_hash" + total_published=$((total_published + batch_count)) + fi + + rm -f "$tmp_iocs" + logger -t threat-intel "Published $total_published IOCs to chain" + echo "{\"published\":$total_published}" +} + +# ============================================================================ +# Receiving - Scan chain for threat_ioc blocks +# ============================================================================ +ti_process_pending() { + ti_init + + [ -f "$CHAIN_FILE" ] || return 0 + local node_id=$(cat "$NODE_ID_FILE" 2>/dev/null || echo "unknown") + local now=$(date +%s) + local new_count=0 + + # Scan chain for unprocessed threat_ioc blocks + jsonfilter -i "$CHAIN_FILE" -e '@.blocks[*]' 2>/dev/null | while read -r block; do + local btype=$(echo "$block" | jsonfilter -e '@.type' 2>/dev/null) + [ "$btype" = "threat_ioc" ] || continue + + local bhash=$(echo "$block" | jsonfilter -e '@.hash' 2>/dev/null) + local bnode=$(echo "$block" | jsonfilter -e '@.node' 2>/dev/null) + + # Skip our own blocks + [ "$bnode" = "$node_id" ] && continue + + # Skip already processed + grep -q "$bhash" "$TI_PROCESSED" 2>/dev/null && continue + + # Extract IOCs from block data + local block_data=$(echo "$block" | jsonfilter -e '@.data' 2>/dev/null) + echo "$block_data" | jsonfilter -e '@.iocs[*]' 2>/dev/null | while read -r ioc; do + local ip=$(echo "$ioc" | jsonfilter -e '@.ip' 2>/dev/null) + [ -z "$ip" ] && continue + + local ioc_ts=$(echo "$ioc" | jsonfilter -e '@.ts' 2>/dev/null || echo "0") + local ioc_ttl=$(echo "$ioc" | jsonfilter -e '@.ttl' 2>/dev/null || echo "$TI_IOC_TTL") + local expires=$((ioc_ts + ioc_ttl)) + + # Skip expired IOCs + [ "$now" -ge "$expires" ] && continue + + echo "$ioc" + done + + # Mark block as processed + echo "$bhash" >> "$TI_PROCESSED" + done > "$TI_DIR/tmp-received-$$.json" + + # Append to received IOCs (dedup by IP) + local existing_recv_ips="" + if [ -f "$IOC_RECEIVED" ] && [ "$(cat "$IOC_RECEIVED")" != "[]" ]; then + existing_recv_ips=$(jsonfilter -i "$IOC_RECEIVED" -e '@[*].ip' 2>/dev/null | sort -u) + fi + + # Load existing received IOCs (prune expired) + local recv="[" + local rfirst=1 + + if [ -f "$IOC_RECEIVED" ] && [ "$(cat "$IOC_RECEIVED")" != "[]" ]; then + jsonfilter -i "$IOC_RECEIVED" -e '@[*]' 2>/dev/null | while read -r existing; do + local ets=$(echo "$existing" | jsonfilter -e '@.ts' 2>/dev/null || echo "0") + local ettl=$(echo "$existing" | jsonfilter -e '@.ttl' 2>/dev/null || echo "$TI_IOC_TTL") + local expires=$((ets + ettl)) + [ "$now" -ge "$expires" ] && continue + [ $rfirst -eq 0 ] && recv="$recv," + rfirst=0 + recv="$recv$existing" + done + fi + + # Add new received IOCs + while read -r ioc_line; do + [ -z "$ioc_line" ] && continue + local ip=$(echo "$ioc_line" | jsonfilter -e '@.ip' 2>/dev/null) + [ -z "$ip" ] && continue + echo "$existing_recv_ips" | grep -q "^${ip}$" && continue + + [ $rfirst -eq 0 ] && recv="$recv," + rfirst=0 + recv="$recv$ioc_line" + new_count=$((new_count + 1)) + done < "$TI_DIR/tmp-received-$$.json" + + recv="$recv]" + echo "$recv" > "$IOC_RECEIVED" + rm -f "$TI_DIR/tmp-received-$$.json" + + logger -t threat-intel "Processed pending: $new_count new IOCs received" + echo "{\"new_received\":$new_count}" +} + +# ============================================================================ +# Trust Model +# ============================================================================ +ti_trust_score() { + local target_node="$1" + [ -z "$target_node" ] && { echo "unknown"; return; } + + local node_id=$(cat "$NODE_ID_FILE" 2>/dev/null || echo "unknown") + [ "$target_node" = "$node_id" ] && { echo "self"; return; } + + # Check if node is a direct approved peer + local peers_file="/tmp/secubox-p2p-peers.json" + if [ -f "$peers_file" ]; then + local peer_count=$(jsonfilter -i "$peers_file" -e '@.peers[*]' 2>/dev/null | wc -l) + local p=0 + while [ $p -lt $peer_count ]; do + local pid=$(jsonfilter -i "$peers_file" -e "@.peers[$p].id" 2>/dev/null) + local pname=$(jsonfilter -i "$peers_file" -e "@.peers[$p].name" 2>/dev/null) + if [ "$pid" = "$target_node" ] || [ "$pname" = "$target_node" ]; then + echo "direct" + return + fi + p=$((p + 1)) + done + fi + + # Check if node is known in the chain (transitive trust) + if [ -f "$CHAIN_FILE" ]; then + local in_chain=$(jsonfilter -i "$CHAIN_FILE" -e '@.blocks[*].node' 2>/dev/null | grep -c "^${target_node}$") + [ "$in_chain" -gt 0 ] && { echo "transitive"; return; } + fi + + # Check trusted peers directory + if [ -d "$TRUSTED_PEERS_DIR" ]; then + for pub in "$TRUSTED_PEERS_DIR"/*.pub; do + [ -f "$pub" ] || continue + local fp=$(basename "$pub" .pub) + if echo "$target_node" | grep -q "$fp"; then + echo "direct" + return + fi + done + fi + + echo "unknown" +} + +# ============================================================================ +# Apply IOCs - Add CrowdSec decisions +# ============================================================================ +ti_apply_ioc() { + local ioc_json="$1" + local ip=$(echo "$ioc_json" | jsonfilter -e '@.ip' 2>/dev/null) + local severity=$(echo "$ioc_json" | jsonfilter -e '@.severity' 2>/dev/null) + local source_node=$(echo "$ioc_json" | jsonfilter -e '@.node' 2>/dev/null) + local scenario=$(echo "$ioc_json" | jsonfilter -e '@.scenario' 2>/dev/null || echo "mesh-shared") + local duration=$(echo "$ioc_json" | jsonfilter -e '@.duration' 2>/dev/null || echo "${TI_IOC_TTL}s") + local ttl=$(echo "$ioc_json" | jsonfilter -e '@.ttl' 2>/dev/null || echo "$TI_IOC_TTL") + local ts=$(echo "$ioc_json" | jsonfilter -e '@.ts' 2>/dev/null || echo "0") + + [ -z "$ip" ] && return 1 + + # Check whitelist + grep -q "^${ip}$" "$TI_WHITELIST" 2>/dev/null && { + logger -t threat-intel "Skipping whitelisted IP: $ip" + return 1 + } + + # Check TTL (skip expired) + local now=$(date +%s) + local expires=$((ts + ttl)) + [ "$now" -ge "$expires" ] && return 1 + + # Check trust level + local trust=$(ti_trust_score "$source_node") + + case "$trust" in + direct) + # Apply as-is for direct peers + ;; + transitive) + # Only apply if policy allows and severity is high enough + [ "$TI_APPLY_TRANSITIVE" != "1" ] && return 1 + _severity_meets_min "$severity" "high" || return 1 + # Halve the remaining TTL for transitive trust + local remaining=$((expires - now)) + ttl=$((remaining / 2)) + duration="${ttl}s" + ;; + unknown|*) + # Never auto-apply unknown sources + logger -t threat-intel "Skipping IOC from unknown node: $source_node ($ip)" + return 1 + ;; + esac + + # Check minimum severity + _severity_meets_min "$severity" "$TI_MIN_SEVERITY" || return 1 + + # Apply via CrowdSec + if command -v cscli >/dev/null 2>&1; then + cscli decisions add --ip "$ip" --duration "$duration" \ + --reason "mesh-p2p:$scenario" --type ban 2>/dev/null + if [ $? -eq 0 ]; then + logger -t threat-intel "Applied IOC: $ip (trust=$trust, severity=$severity, source=$source_node)" + return 0 + fi + fi + + return 1 +} + +ti_apply_pending() { + ti_init + [ "$TI_ENABLED" != "1" ] && return 0 + [ "$TI_AUTO_APPLY" != "1" ] && return 0 + + # First process any new blocks from chain + ti_process_pending >/dev/null 2>&1 + + [ -f "$IOC_RECEIVED" ] || return 0 + local recv_count=$(jsonfilter -i "$IOC_RECEIVED" -e '@[*]' 2>/dev/null | wc -l) + [ "$recv_count" -eq 0 ] && return 0 + + local applied_count=0 + local skipped_count=0 + local now=$(date +%s) + + # Get already-applied IPs + local applied_ips="" + if [ -f "$IOC_APPLIED" ] && [ "$(cat "$IOC_APPLIED")" != "[]" ]; then + applied_ips=$(jsonfilter -i "$IOC_APPLIED" -e '@[*].ip' 2>/dev/null | sort -u) + fi + + local new_applied="[" + local afirst=1 + + # Load existing applied (prune expired) via temp file to avoid subshell + local tmp_applied="$TI_DIR/tmp-applied-$$.list" + if [ -f "$IOC_APPLIED" ] && [ "$(cat "$IOC_APPLIED")" != "[]" ]; then + jsonfilter -i "$IOC_APPLIED" -e '@[*]' > "$tmp_applied" 2>/dev/null + while read -r existing; do + [ -z "$existing" ] && continue + local ets=$(echo "$existing" | jsonfilter -e '@.ts' 2>/dev/null || echo "0") + local ettl=$(echo "$existing" | jsonfilter -e '@.ttl' 2>/dev/null || echo "$TI_IOC_TTL") + local expires=$((ets + ettl)) + [ "$now" -ge "$expires" ] && continue + [ $afirst -eq 0 ] && new_applied="$new_applied," + afirst=0 + new_applied="$new_applied$existing" + done < "$tmp_applied" + fi + + # Dump received IOCs to temp file to avoid subshell + local tmp_recv="$TI_DIR/tmp-recv-$$.list" + jsonfilter -i "$IOC_RECEIVED" -e '@[*]' > "$tmp_recv" 2>/dev/null + + # Apply pending received IOCs + while read -r ioc; do + [ -z "$ioc" ] && continue + local ip=$(echo "$ioc" | jsonfilter -e '@.ip' 2>/dev/null) + [ -z "$ip" ] && continue + + # Skip already applied + echo "$applied_ips" | grep -q "^${ip}$" && continue + + if ti_apply_ioc "$ioc"; then + [ $afirst -eq 0 ] && new_applied="$new_applied," + afirst=0 + # Add applied_at timestamp + local ioc_with_meta=$(echo "$ioc" | sed "s/}$/,\"applied_at\":$now}/") + new_applied="$new_applied$ioc_with_meta" + applied_count=$((applied_count + 1)) + else + skipped_count=$((skipped_count + 1)) + fi + done < "$tmp_recv" + + new_applied="$new_applied]" + echo "$new_applied" > "$IOC_APPLIED" + + rm -f "$tmp_applied" "$tmp_recv" + logger -t threat-intel "Apply pending: applied=$applied_count skipped=$skipped_count" + echo "{\"applied\":$applied_count,\"skipped\":$skipped_count}" +} + +# ============================================================================ +# Status and Listings +# ============================================================================ +ti_status() { + ti_init + + local now=$(date +%s) + local node_id=$(cat "$NODE_ID_FILE" 2>/dev/null || echo "unknown") + + # Count IOCs + local local_count=0 + local received_count=0 + local applied_count=0 + + [ -f "$IOC_LOCAL" ] && local_count=$(jsonfilter -i "$IOC_LOCAL" -e '@[*]' 2>/dev/null | wc -l) + [ -f "$IOC_RECEIVED" ] && received_count=$(jsonfilter -i "$IOC_RECEIVED" -e '@[*]' 2>/dev/null | wc -l) + [ -f "$IOC_APPLIED" ] && applied_count=$(jsonfilter -i "$IOC_APPLIED" -e '@[*]' 2>/dev/null | wc -l) + + # Count contributing peers + local peer_nodes="" + local peer_count=0 + if [ -f "$IOC_RECEIVED" ] && [ "$received_count" -gt 0 ]; then + peer_nodes=$(jsonfilter -i "$IOC_RECEIVED" -e '@[*].node' 2>/dev/null | sort -u) + peer_count=$(echo "$peer_nodes" | grep -c '.' 2>/dev/null) + [ -z "$peer_count" ] && peer_count=0 + fi + + # Chain threat_ioc block count + local chain_blocks=0 + if [ -f "$CHAIN_FILE" ]; then + chain_blocks=$(jsonfilter -i "$CHAIN_FILE" -e '@.blocks[*].type' 2>/dev/null | grep -c "^threat_ioc$" 2>/dev/null) + [ -z "$chain_blocks" ] && chain_blocks=0 + fi + + cat << EOF +{ + "enabled": $( [ "$TI_ENABLED" = "1" ] && echo "true" || echo "false"), + "auto_apply": $( [ "$TI_AUTO_APPLY" = "1" ] && echo "true" || echo "false"), + "node_id": "$node_id", + "timestamp": $now, + "local_iocs": $local_count, + "received_iocs": $received_count, + "applied_iocs": $applied_count, + "peer_contributors": $peer_count, + "chain_threat_blocks": $chain_blocks, + "min_severity": "$TI_MIN_SEVERITY", + "ioc_ttl": $TI_IOC_TTL, + "apply_transitive": $( [ "$TI_APPLY_TRANSITIVE" = "1" ] && echo "true" || echo "false") +} +EOF +} + +ti_list_local() { + ti_init + [ -f "$IOC_LOCAL" ] && cat "$IOC_LOCAL" || echo '[]' +} + +ti_list_received() { + ti_init + + if [ ! -f "$IOC_RECEIVED" ] || [ "$(cat "$IOC_RECEIVED")" = "[]" ]; then + echo '[]' + return + fi + + # Enrich with trust scores + local enriched="[" + local efirst=1 + + jsonfilter -i "$IOC_RECEIVED" -e '@[*]' 2>/dev/null | while read -r ioc; do + local source_node=$(echo "$ioc" | jsonfilter -e '@.node' 2>/dev/null) + local trust=$(ti_trust_score "$source_node") + + # Check if already applied + local ip=$(echo "$ioc" | jsonfilter -e '@.ip' 2>/dev/null) + local is_applied="false" + if [ -f "$IOC_APPLIED" ]; then + jsonfilter -i "$IOC_APPLIED" -e '@[*].ip' 2>/dev/null | grep -q "^${ip}$" && is_applied="true" + fi + + # Add trust and applied fields + local enriched_ioc=$(echo "$ioc" | sed "s/}$/,\"trust\":\"$trust\",\"applied\":$is_applied}/") + [ $efirst -eq 0 ] && enriched="$enriched," + efirst=0 + enriched="$enriched$enriched_ioc" + done + + enriched="$enriched]" + echo "$enriched" +} + +ti_list_applied() { + ti_init + [ -f "$IOC_APPLIED" ] && cat "$IOC_APPLIED" || echo '[]' +} + +# ============================================================================ +# Peer Statistics +# ============================================================================ +ti_peer_stats() { + ti_init + + [ -f "$IOC_RECEIVED" ] || { echo '[]'; return; } + + local now=$(date +%s) + local stats="[" + local sfirst=1 + local seen_nodes="" + + jsonfilter -i "$IOC_RECEIVED" -e '@[*]' 2>/dev/null | while read -r ioc; do + local node=$(echo "$ioc" | jsonfilter -e '@.node' 2>/dev/null) + [ -z "$node" ] && continue + + # Skip already counted nodes + echo "$seen_nodes" | grep -q "^${node}$" && continue + seen_nodes="$seen_nodes +$node" + + # Count IOCs from this node + local ioc_count=$(jsonfilter -i "$IOC_RECEIVED" -e '@[*].node' 2>/dev/null | grep -c "^${node}$" 2>/dev/null) + [ -z "$ioc_count" ] && ioc_count=0 + + # Get last seen timestamp + local last_ts=0 + jsonfilter -i "$IOC_RECEIVED" -e '@[*]' 2>/dev/null | while read -r n_ioc; do + local n_node=$(echo "$n_ioc" | jsonfilter -e '@.node' 2>/dev/null) + [ "$n_node" = "$node" ] || continue + local n_ts=$(echo "$n_ioc" | jsonfilter -e '@.ts' 2>/dev/null || echo "0") + [ "$n_ts" -gt "$last_ts" ] && last_ts="$n_ts" + done + + local trust=$(ti_trust_score "$node") + + # Count applied from this node + local applied_count=0 + if [ -f "$IOC_APPLIED" ]; then + applied_count=$(jsonfilter -i "$IOC_APPLIED" -e '@[*].node' 2>/dev/null | grep -c "^${node}$" 2>/dev/null) + [ -z "$applied_count" ] && applied_count=0 + fi + + [ $sfirst -eq 0 ] && stats="$stats," + sfirst=0 + stats="$stats{\"node\":\"$node\",\"trust\":\"$trust\",\"ioc_count\":$ioc_count,\"applied_count\":$applied_count,\"last_seen\":$last_ts}" + done + + stats="$stats]" + echo "$stats" +} + +# ============================================================================ +# Collect and Publish (cron entry point) +# ============================================================================ +ti_collect_and_publish() { + # Acquire lock + if [ -f "$TI_LOCK" ]; then + local lock_age=$(( $(date +%s) - $(stat -c %Y "$TI_LOCK" 2>/dev/null || echo "0") )) + [ "$lock_age" -lt 300 ] && { + logger -t threat-intel "Skipping collect-and-publish: locked" + return 0 + } + fi + touch "$TI_LOCK" + + ti_init + + if [ "$TI_ENABLED" != "1" ]; then + rm -f "$TI_LOCK" + return 0 + fi + + ti_collect_all + ti_publish_iocs + + rm -f "$TI_LOCK" +} + +# ============================================================================ +# CLI Interface +# ============================================================================ +case "${1:-}" in + collect-and-publish) + ti_collect_and_publish + ;; + apply-pending) + ti_init + ti_apply_pending + ;; + status) + ti_status + ;; + list) + case "${2:-}" in + local) ti_list_local ;; + received) ti_list_received ;; + applied) ti_list_applied ;; + *) ti_list_received ;; + esac + ;; + peers) + ti_peer_stats + ;; + collect) + ti_init + ti_collect_all + ;; + publish) + ti_init + ti_publish_iocs + ;; + process) + ti_init + ti_process_pending + ;; + *) + # When sourced as library, do nothing + [ -n "${1:-}" ] && { + echo "SecuBox Threat Intelligence - P2P IOC Sharing" + echo "" + echo "Usage: $0 " + echo "" + echo "Commands:" + echo " collect-and-publish Collect IOCs and publish to chain (cron)" + echo " apply-pending Process and apply received IOCs (cron)" + echo " status Show threat intel status (JSON)" + echo " list [local|received|applied] List IOCs" + echo " peers Show peer contribution stats" + echo " collect Collect IOCs from local sources" + echo " publish Publish local IOCs to chain" + echo " process Process pending chain blocks" + } + ;; +esac diff --git a/package/secubox/secubox-p2p/root/www/api/threat-intel/apply b/package/secubox/secubox-p2p/root/www/api/threat-intel/apply new file mode 100755 index 00000000..cf72b3a0 --- /dev/null +++ b/package/secubox/secubox-p2p/root/www/api/threat-intel/apply @@ -0,0 +1,33 @@ +#!/bin/sh +# Threat Intel API - Apply pending IOCs +# POST: Triggers processing and applying of received IOCs + +echo "Content-Type: application/json" +echo "Access-Control-Allow-Origin: *" +echo "Access-Control-Allow-Methods: POST, OPTIONS" +echo "Access-Control-Allow-Headers: Content-Type" +echo "" + +# Handle CORS preflight +if [ "$REQUEST_METHOD" = "OPTIONS" ]; then + exit 0 +fi + +if [ "$REQUEST_METHOD" != "POST" ]; then + echo '{"success":false,"error":"method_not_allowed","message":"Use POST to trigger apply"}' + exit 0 +fi + +. /usr/lib/secubox/threat-intel.sh 2>/dev/null + +ti_init + +result=$(ti_apply_pending 2>/dev/null) + +if [ -n "$result" ]; then + applied=$(echo "$result" | jsonfilter -e '@.applied' 2>/dev/null || echo "0") + skipped=$(echo "$result" | jsonfilter -e '@.skipped' 2>/dev/null || echo "0") + echo "{\"success\":true,\"applied\":$applied,\"skipped\":$skipped}" +else + echo '{"success":true,"applied":0,"skipped":0}' +fi diff --git a/package/secubox/secubox-p2p/root/www/api/threat-intel/iocs b/package/secubox/secubox-p2p/root/www/api/threat-intel/iocs new file mode 100755 index 00000000..b12b39c1 --- /dev/null +++ b/package/secubox/secubox-p2p/root/www/api/threat-intel/iocs @@ -0,0 +1,23 @@ +#!/bin/sh +# Threat Intel API - IOC listing endpoint +# GET: Returns IOCs (query: type=local|received|applied, default=received) + +echo "Content-Type: application/json" +echo "Access-Control-Allow-Origin: *" +echo "" + +. /usr/lib/secubox/threat-intel.sh 2>/dev/null + +# Parse query string for type parameter +ioc_type="received" +case "$QUERY_STRING" in + *type=local*) ioc_type="local" ;; + *type=received*) ioc_type="received" ;; + *type=applied*) ioc_type="applied" ;; +esac + +case "$ioc_type" in + local) ti_list_local 2>/dev/null ;; + received) ti_list_received 2>/dev/null ;; + applied) ti_list_applied 2>/dev/null ;; +esac || echo '[]' diff --git a/package/secubox/secubox-p2p/root/www/api/threat-intel/peers b/package/secubox/secubox-p2p/root/www/api/threat-intel/peers new file mode 100755 index 00000000..cedd0aba --- /dev/null +++ b/package/secubox/secubox-p2p/root/www/api/threat-intel/peers @@ -0,0 +1,11 @@ +#!/bin/sh +# Threat Intel API - Peer contribution stats +# GET: Returns per-peer IOC statistics and trust levels + +echo "Content-Type: application/json" +echo "Access-Control-Allow-Origin: *" +echo "" + +. /usr/lib/secubox/threat-intel.sh 2>/dev/null + +ti_peer_stats 2>/dev/null || echo '[]' diff --git a/package/secubox/secubox-p2p/root/www/api/threat-intel/publish b/package/secubox/secubox-p2p/root/www/api/threat-intel/publish new file mode 100755 index 00000000..24fc4d09 --- /dev/null +++ b/package/secubox/secubox-p2p/root/www/api/threat-intel/publish @@ -0,0 +1,34 @@ +#!/bin/sh +# Threat Intel API - Publish IOCs to chain +# POST: Triggers collection and publishing of local IOCs + +echo "Content-Type: application/json" +echo "Access-Control-Allow-Origin: *" +echo "Access-Control-Allow-Methods: POST, OPTIONS" +echo "Access-Control-Allow-Headers: Content-Type" +echo "" + +# Handle CORS preflight +if [ "$REQUEST_METHOD" = "OPTIONS" ]; then + exit 0 +fi + +if [ "$REQUEST_METHOD" != "POST" ]; then + echo '{"success":false,"error":"method_not_allowed","message":"Use POST to trigger publish"}' + exit 0 +fi + +. /usr/lib/secubox/threat-intel.sh 2>/dev/null + +ti_init + +# Collect fresh IOCs then publish +ti_collect_all >/dev/null 2>&1 +result=$(ti_publish_iocs 2>/dev/null) + +if [ -n "$result" ]; then + published=$(echo "$result" | jsonfilter -e '@.published' 2>/dev/null || echo "0") + echo "{\"success\":true,\"published\":$published}" +else + echo '{"success":true,"published":0}' +fi diff --git a/package/secubox/secubox-p2p/root/www/api/threat-intel/status b/package/secubox/secubox-p2p/root/www/api/threat-intel/status new file mode 100755 index 00000000..05fab4cb --- /dev/null +++ b/package/secubox/secubox-p2p/root/www/api/threat-intel/status @@ -0,0 +1,11 @@ +#!/bin/sh +# Threat Intel API - Status endpoint +# GET: Returns threat intelligence status summary + +echo "Content-Type: application/json" +echo "Access-Control-Allow-Origin: *" +echo "" + +. /usr/lib/secubox/threat-intel.sh 2>/dev/null + +ti_status 2>/dev/null || echo '{"error":"threat_intel_unavailable"}'