diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 7764be02..516d9036 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -594,3 +594,48 @@ _Last updated: 2026-02-07_ - **UCI configuration**: sources enable/disable, signing, validation settings, application method, auto-apply - **Daemon**: Configurable collect_interval (default 300s), auto_collect, auto_share, auto_apply - Part of v0.19 MirrorNetworking roadmap (Couche 3). + +42. **Config Advisor - ANSSI CSPN Compliance (2026-02-07)** + - Created `secubox-config-advisor` — security configuration analysis and hardening tool. + - **ANSSI CSPN compliance framework**: + - 7 check categories: network, firewall, authentication, encryption, services, logging, updates + - 25+ security check rules with severity levels (critical, high, medium, low, info) + - JSON rules database in `/usr/share/config-advisor/anssi-rules.json` + - **Security check modules** (`checks.sh`): + - Network: IPv6, management access restriction, SYN flood protection + - Firewall: default deny policy, drop invalid packets, WAN port exposure + - Authentication: root password, SSH key auth, SSH password auth + - Encryption: HTTPS enabled, WireGuard configured, DNS encryption + - Services: CrowdSec running, services bound to localhost + - Logging: syslog enabled, log rotation configured + - **Risk scoring module** (`scoring.sh`): + - 0-100 score with severity weights (critical=40, high=25, medium=20, low=10, info=5) + - Grade calculation (A-F) based on thresholds (90/80/70/60) + - Risk level classification: critical, high, medium, low, minimal + - Score history tracking and trend analysis + - **ANSSI compliance module** (`anssi.sh`): + - Compliance rate calculation (percentage of passing rules) + - Report generation in text, JSON, and Markdown formats + - Category filtering and strict mode + - **Remediation module** (`remediate.sh`): + - Auto-remediation for 7 checks: NET-002, NET-004, FW-001, FW-002, AUTH-003, CRYPT-001, LOG-002 + - Safe vs manual remediation separation + - Dry-run mode for preview + - LocalAI integration for AI-powered suggestions + - Pending approvals queue + - **CLI** (`config-advisorctl`): + - Check commands: `check`, `check-category`, `results` + - Compliance commands: `compliance`, `compliance-status`, `compliance-report`, `is-compliant` + - Scoring commands: `score`, `score-history`, `score-trend`, `risk-summary` + - Remediation commands: `remediate`, `remediate-dry`, `remediate-safe`, `remediate-pending`, `suggest` + - Daemon mode with configurable check interval + - Created `luci-app-config-advisor` — LuCI dashboard. + - Dashboard: score circle, grade, risk level, compliance rate, last check time + - Check results table with status icons + - Score history table + - Compliance view: summary cards, progress bar, results by category + - Remediation view: quick actions, failed checks with apply buttons, pending approvals + - Settings: framework selection, scoring weights, category toggles, LocalAI config + - **RPCD methods**: status, results, score, compliance, check, pending, history, suggest, remediate, remediate_safe, set_config + - **UCI configuration**: main (enabled, check_interval, auto_remediate), compliance (framework, strict_mode), scoring (passing_score, weights), categories (enable/disable), localai (url, model) + - Part of v1.0.0 certification roadmap (ANSSI CSPN compliance tooling). diff --git a/.claude/WIP.md b/.claude/WIP.md index f8974712..f00ba853 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -283,9 +283,35 @@ Required components: | LocalAI 3.9 | DONE | | LocalAI Emancipation | DONE (Tor + DNS + mDNS) | +### v1.0.0 Progress + +| Item | Status | +|------|--------| +| Config Advisor | DONE | +| ANSSI CSPN Compliance | DONE | +| Remediation Engine | DONE | +| LuCI Dashboard | DONE | + +### Just Completed (2026-02-07) + +- **Config Advisor Package** — DONE + - Created `secubox-config-advisor` - ANSSI CSPN compliance checking daemon + - 7 check categories, 25+ security rules + - Risk scoring (0-100) with grade (A-F) and risk level + - Auto-remediation for 7 checks with dry-run mode + - LocalAI integration for AI-powered suggestions + - `config-advisorctl` CLI with 20+ commands + +- **Config Advisor Dashboard** — DONE + - Created `luci-app-config-advisor` - LuCI dashboard + - Score display with grade circle and risk level + - Compliance view by category with pass/fail/warn badges + - Remediation view with apply/preview buttons + - Settings for framework, weights, categories, LocalAI + ### Certifications -- ANSSI CSPN: Data Classifier + Mistral EU + offline mode +- ANSSI CSPN: Config Advisor compliance tool DONE - GDPR: Currently compliant - ISO 27001, NIS2, SOC2: Planned for v1.1+ diff --git a/package/secubox/luci-app-config-advisor/Makefile b/package/secubox/luci-app-config-advisor/Makefile new file mode 100644 index 00000000..a5b00031 --- /dev/null +++ b/package/secubox/luci-app-config-advisor/Makefile @@ -0,0 +1,31 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-config-advisor +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox +PKG_LICENSE:=MIT + +LUCI_TITLE:=LuCI Config Advisor Dashboard +LUCI_DESCRIPTION:=ANSSI CSPN compliance checking and security configuration advisor +LUCI_DEPENDS:=+luci-base +secubox-config-advisor +LUCI_PKGARCH:=all + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/$(PKG_NAME)/install + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-config-advisor.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-config-advisor.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.config-advisor $(1)/usr/libexec/rpcd/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/config-advisor + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/config-advisor/*.js $(1)/www/luci-static/resources/view/config-advisor/ +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/package/secubox/luci-app-config-advisor/htdocs/luci-static/resources/view/config-advisor/compliance.js b/package/secubox/luci-app-config-advisor/htdocs/luci-static/resources/view/config-advisor/compliance.js new file mode 100644 index 00000000..1688f598 --- /dev/null +++ b/package/secubox/luci-app-config-advisor/htdocs/luci-static/resources/view/config-advisor/compliance.js @@ -0,0 +1,183 @@ +'use strict'; +'require view'; +'require rpc'; +'require ui'; + +var callCompliance = rpc.declare({ + object: 'luci.config-advisor', + method: 'compliance', + expect: {} +}); + +var callCheck = rpc.declare({ + object: 'luci.config-advisor', + method: 'check', + expect: {} +}); + +function formatTimestamp(ts) { + if (!ts || ts === 0) return 'Never'; + var d = new Date(ts * 1000); + return d.toLocaleString(); +} + +function getSeverityColor(severity) { + switch(severity) { + case 'critical': return '#ef4444'; + case 'high': return '#f97316'; + case 'medium': return '#eab308'; + case 'low': return '#22c55e'; + case 'info': return '#3b82f6'; + default: return '#6b7280'; + } +} + +function getStatusBadge(status) { + var colors = { + 'pass': { bg: '#166534', text: '#22c55e' }, + 'fail': { bg: '#7f1d1d', text: '#ef4444' }, + 'warn': { bg: '#713f12', text: '#eab308' }, + 'info': { bg: '#1e3a5f', text: '#3b82f6' }, + 'skip': { bg: '#374151', text: '#9ca3af' } + }; + var c = colors[status] || colors['skip']; + return E('span', { + 'style': 'background:' + c.bg + '; color:' + c.text + '; padding:2px 8px; border-radius:4px; font-size:12px; text-transform:uppercase;' + }, status); +} + +return view.extend({ + load: function() { + return callCompliance(); + }, + + render: function(data) { + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, 'ANSSI CSPN Compliance'), + E('p', { 'class': 'cbi-map-descr' }, + 'Compliance status against ANSSI CSPN security requirements.') + ]); + + if (data.error) { + view.appendChild(E('div', { + 'style': 'background:#1e293b; padding:2rem; border-radius:8px; text-align:center;' + }, [ + E('p', { 'style': 'color:#94a3b8; margin-bottom:1rem' }, data.error), + E('button', { + 'class': 'cbi-button cbi-button-apply', + 'click': ui.createHandlerFn(this, function() { + return callCheck().then(function() { + ui.addNotification(null, E('p', 'Compliance check completed. Refreshing...')); + window.location.reload(); + }); + }) + }, 'Run Compliance Check') + ])); + return view; + } + + var summary = data.summary || {}; + var results = data.results || []; + + // Summary Cards + var summaryGrid = E('div', { + 'style': 'display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:1rem; margin-bottom:2rem;' + }); + + var metrics = [ + { label: 'Total Checks', value: summary.total || 0, color: '#f1f5f9' }, + { label: 'Passed', value: summary.passed || 0, color: '#22c55e' }, + { label: 'Failed', value: summary.failed || 0, color: '#ef4444' }, + { label: 'Warnings', value: summary.warnings || 0, color: '#eab308' }, + { label: 'Info', value: summary.info || 0, color: '#3b82f6' } + ]; + + metrics.forEach(function(m) { + summaryGrid.appendChild(E('div', { + 'style': 'background:#1e293b; border-radius:8px; padding:1rem; text-align:center;' + }, [ + E('div', { 'style': 'font-size:32px; font-weight:bold; color:' + m.color }, m.value), + E('div', { 'style': 'font-size:12px; color:#94a3b8; margin-top:0.5rem' }, m.label) + ])); + }); + + view.appendChild(summaryGrid); + + // Compliance Rate Progress Bar + var rate = data.compliance_rate || 0; + var rateColor = rate >= 80 ? '#22c55e' : rate >= 60 ? '#eab308' : '#ef4444'; + + view.appendChild(E('div', { + 'style': 'background:#1e293b; border-radius:8px; padding:1.5rem; margin-bottom:2rem;' + }, [ + E('div', { 'style': 'display:flex; justify-content:space-between; margin-bottom:0.5rem;' }, [ + E('span', { 'style': 'color:#f1f5f9; font-weight:bold;' }, 'Compliance Rate'), + E('span', { 'style': 'color:' + rateColor + '; font-weight:bold;' }, rate + '%') + ]), + E('div', { + 'style': 'background:#334155; border-radius:4px; height:12px; overflow:hidden;' + }, [ + E('div', { + 'style': 'background:' + rateColor + '; height:100%; width:' + rate + '%; transition:width 0.3s;' + }) + ]), + E('div', { 'style': 'font-size:12px; color:#94a3b8; margin-top:0.5rem;' }, + 'Framework: ' + (data.framework || 'ANSSI CSPN') + ' | Generated: ' + formatTimestamp(data.timestamp)) + ])); + + // Results by Category + var categories = {}; + results.forEach(function(r) { + var cat = r.category || 'other'; + if (!categories[cat]) categories[cat] = []; + categories[cat].push(r); + }); + + Object.keys(categories).sort().forEach(function(cat) { + var catResults = categories[cat]; + + view.appendChild(E('h3', { + 'style': 'margin-top:1.5rem; text-transform:capitalize; border-bottom:1px solid #334155; padding-bottom:0.5rem;' + }, cat.replace(/_/g, ' '))); + + var table = E('table', { 'class': 'table', 'style': 'width:100%' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th', 'style': 'width:100px' }, 'Rule ID'), + E('th', { 'class': 'th', 'style': 'width:80px' }, 'Severity'), + E('th', { 'class': 'th', 'style': 'width:80px' }, 'Status') + ]) + ]); + + catResults.forEach(function(r) { + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, E('code', {}, r.rule_id || '-')), + E('td', { 'class': 'td' }, E('span', { + 'style': 'color:' + getSeverityColor(r.severity) + '; text-transform:capitalize;' + }, r.severity || 'medium')), + E('td', { 'class': 'td' }, getStatusBadge(r.status)) + ])); + }); + + view.appendChild(table); + }); + + // Action Buttons + view.appendChild(E('div', { 'style': 'margin-top:2rem; display:flex; gap:1rem;' }, [ + E('button', { + 'class': 'cbi-button cbi-button-apply', + 'click': ui.createHandlerFn(this, function() { + return callCheck().then(function() { + ui.addNotification(null, E('p', 'Compliance check completed. Refreshing...')); + window.location.reload(); + }); + }) + }, 'Re-run Compliance Check') + ])); + + return view; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-config-advisor/htdocs/luci-static/resources/view/config-advisor/dashboard.js b/package/secubox/luci-app-config-advisor/htdocs/luci-static/resources/view/config-advisor/dashboard.js new file mode 100644 index 00000000..e503e951 --- /dev/null +++ b/package/secubox/luci-app-config-advisor/htdocs/luci-static/resources/view/config-advisor/dashboard.js @@ -0,0 +1,231 @@ +'use strict'; +'require view'; +'require rpc'; +'require poll'; +'require ui'; + +var callStatus = rpc.declare({ + object: 'luci.config-advisor', + method: 'status', + expect: {} +}); + +var callResults = rpc.declare({ + object: 'luci.config-advisor', + method: 'results', + expect: {} +}); + +var callCheck = rpc.declare({ + object: 'luci.config-advisor', + method: 'check', + expect: {} +}); + +var callHistory = rpc.declare({ + object: 'luci.config-advisor', + method: 'history', + params: ['count'], + expect: {} +}); + +function formatTimestamp(ts) { + if (!ts || ts === 0) return 'Never'; + var d = new Date(ts * 1000); + return d.toLocaleString(); +} + +function getGradeColor(grade) { + switch(grade) { + case 'A': return '#22c55e'; + case 'B': return '#84cc16'; + case 'C': return '#eab308'; + case 'D': return '#f97316'; + case 'F': return '#ef4444'; + default: return '#6b7280'; + } +} + +function getRiskColor(level) { + switch(level) { + case 'minimal': return '#22c55e'; + case 'low': return '#84cc16'; + case 'medium': return '#eab308'; + case 'high': return '#f97316'; + case 'critical': return '#ef4444'; + default: return '#6b7280'; + } +} + +function getStatusIcon(status) { + switch(status) { + case 'pass': return ''; + case 'fail': return ''; + case 'warn': return ''; + case 'info': return ''; + default: return ''; + } +} + +return view.extend({ + load: function() { + return Promise.all([ + callStatus(), + callResults(), + callHistory(10) + ]); + }, + + render: function(data) { + var status = data[0] || {}; + var resultsData = data[1] || {}; + var historyData = data[2] || {}; + + var results = resultsData.results || []; + var history = historyData.history || []; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, 'Config Advisor Dashboard'), + E('p', { 'class': 'cbi-map-descr' }, + 'ANSSI CSPN compliance checking and security configuration analysis.') + ]); + + // Score Card + var scoreCard = E('div', { + 'style': 'display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:1rem; margin-bottom:1.5rem;' + }); + + // Grade circle + var gradeColor = getGradeColor(status.grade || '?'); + scoreCard.appendChild(E('div', { + 'style': 'background:#1e293b; border-radius:12px; padding:1.5rem; text-align:center;' + }, [ + E('div', { + 'style': 'width:100px; height:100px; border-radius:50%; border:8px solid ' + gradeColor + '; margin:0 auto 1rem; display:flex; align-items:center; justify-content:center;' + }, [ + E('span', { 'style': 'font-size:48px; font-weight:bold; color:' + gradeColor }, status.grade || '?') + ]), + E('div', { 'style': 'font-size:14px; color:#94a3b8' }, 'Security Grade'), + E('div', { 'style': 'font-size:24px; font-weight:bold; color:#f1f5f9; margin-top:0.5rem' }, + (status.score || 0) + '/100') + ])); + + // Risk Level + var riskColor = getRiskColor(status.risk_level || 'unknown'); + scoreCard.appendChild(E('div', { + 'style': 'background:#1e293b; border-radius:12px; padding:1.5rem; text-align:center;' + }, [ + E('div', { + 'style': 'font-size:48px; margin-bottom:1rem; color:' + riskColor + }, status.risk_level === 'critical' ? '⚠' : + status.risk_level === 'high' ? '⚠' : + status.risk_level === 'minimal' ? '✔' : 'ℹ'), + E('div', { 'style': 'font-size:14px; color:#94a3b8' }, 'Risk Level'), + E('div', { + 'style': 'font-size:20px; font-weight:bold; color:' + riskColor + '; margin-top:0.5rem; text-transform:capitalize' + }, status.risk_level || 'Unknown') + ])); + + // Compliance Rate + scoreCard.appendChild(E('div', { + 'style': 'background:#1e293b; border-radius:12px; padding:1.5rem; text-align:center;' + }, [ + E('div', { + 'style': 'font-size:48px; margin-bottom:1rem; color:#3b82f6' + }, '☑'), + E('div', { 'style': 'font-size:14px; color:#94a3b8' }, 'ANSSI Compliance'), + E('div', { 'style': 'font-size:24px; font-weight:bold; color:#f1f5f9; margin-top:0.5rem' }, + (status.compliance_rate || 0) + '%') + ])); + + // Last Check + scoreCard.appendChild(E('div', { + 'style': 'background:#1e293b; border-radius:12px; padding:1.5rem; text-align:center;' + }, [ + E('div', { + 'style': 'font-size:48px; margin-bottom:1rem; color:#8b5cf6' + }, '🕐'), + E('div', { 'style': 'font-size:14px; color:#94a3b8' }, 'Last Check'), + E('div', { 'style': 'font-size:14px; color:#f1f5f9; margin-top:0.5rem' }, + formatTimestamp(status.last_check)) + ])); + + view.appendChild(scoreCard); + + // Run Check Button + var runBtn = E('button', { + 'class': 'cbi-button cbi-button-apply', + 'click': ui.createHandlerFn(this, function() { + return callCheck().then(function() { + ui.addNotification(null, E('p', 'Security check completed. Refreshing...')); + window.location.reload(); + }); + }) + }, 'Run Security Check'); + + view.appendChild(E('div', { 'style': 'margin-bottom:1.5rem' }, runBtn)); + + // Results Table + view.appendChild(E('h3', { 'style': 'margin-top:2rem' }, 'Check Results')); + + if (results.length > 0) { + var table = E('table', { 'class': 'table', 'style': 'width:100%' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Status'), + E('th', { 'class': 'th' }, 'Check ID'), + E('th', { 'class': 'th' }, 'Message'), + E('th', { 'class': 'th' }, 'Details') + ]) + ]); + + results.forEach(function(r) { + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'style': 'text-align:center' }, getStatusIcon(r.status)), + E('td', { 'class': 'td' }, E('code', {}, r.id || '-')), + E('td', { 'class': 'td' }, r.message || '-'), + E('td', { 'class': 'td', 'style': 'color:#94a3b8' }, r.details || '-') + ])); + }); + + view.appendChild(table); + } else { + view.appendChild(E('p', { 'style': 'color:#94a3b8' }, + 'No check results available. Run a security check first.')); + } + + // Score History + if (history.length > 0) { + view.appendChild(E('h3', { 'style': 'margin-top:2rem' }, 'Score History')); + + var historyTable = E('table', { 'class': 'table', 'style': 'width:100%' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Date'), + E('th', { 'class': 'th' }, 'Score'), + E('th', { 'class': 'th' }, 'Grade'), + E('th', { 'class': 'th' }, 'Risk Level') + ]) + ]); + + history.slice().reverse().forEach(function(h) { + historyTable.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, formatTimestamp(h.timestamp)), + E('td', { 'class': 'td' }, h.score + '/100'), + E('td', { 'class': 'td' }, E('span', { + 'style': 'color:' + getGradeColor(h.grade) + '; font-weight:bold' + }, h.grade)), + E('td', { 'class': 'td' }, E('span', { + 'style': 'color:' + getRiskColor(h.risk_level) + '; text-transform:capitalize' + }, h.risk_level)) + ])); + }); + + view.appendChild(historyTable); + } + + return view; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-config-advisor/htdocs/luci-static/resources/view/config-advisor/remediation.js b/package/secubox/luci-app-config-advisor/htdocs/luci-static/resources/view/config-advisor/remediation.js new file mode 100644 index 00000000..ff003846 --- /dev/null +++ b/package/secubox/luci-app-config-advisor/htdocs/luci-static/resources/view/config-advisor/remediation.js @@ -0,0 +1,257 @@ +'use strict'; +'require view'; +'require rpc'; +'require ui'; + +var callResults = rpc.declare({ + object: 'luci.config-advisor', + method: 'results', + expect: {} +}); + +var callPending = rpc.declare({ + object: 'luci.config-advisor', + method: 'pending', + expect: {} +}); + +var callSuggest = rpc.declare({ + object: 'luci.config-advisor', + method: 'suggest', + params: ['check_id'], + expect: {} +}); + +var callRemediate = rpc.declare({ + object: 'luci.config-advisor', + method: 'remediate', + params: ['check_id', 'dry_run'], + expect: {} +}); + +var callRemediateSafe = rpc.declare({ + object: 'luci.config-advisor', + method: 'remediate_safe', + params: ['dry_run'], + expect: {} +}); + +// Checks with available remediations +var remediableChecks = ['NET-002', 'NET-004', 'FW-001', 'FW-002', 'AUTH-003', 'CRYPT-001', 'LOG-002']; +var safeChecks = ['NET-004', 'FW-002', 'CRYPT-001', 'LOG-002']; + +function getStatusColor(status) { + switch(status) { + case 'pass': return '#22c55e'; + case 'fail': return '#ef4444'; + case 'warn': return '#eab308'; + default: return '#6b7280'; + } +} + +return view.extend({ + load: function() { + return Promise.all([ + callResults(), + callPending() + ]); + }, + + render: function(data) { + var resultsData = data[0] || {}; + var pendingData = data[1] || {}; + + var results = resultsData.results || []; + var pending = pendingData.pending || []; + + // Filter to failed/warn checks + var failedChecks = results.filter(function(r) { + return r.status === 'fail' || r.status === 'warn'; + }); + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, 'Security Remediation'), + E('p', { 'class': 'cbi-map-descr' }, + 'Apply automated fixes for failed security checks.') + ]); + + // Quick Actions + view.appendChild(E('div', { + 'style': 'background:#1e293b; border-radius:8px; padding:1.5rem; margin-bottom:2rem;' + }, [ + E('h3', { 'style': 'margin-top:0' }, 'Quick Actions'), + E('p', { 'style': 'color:#94a3b8; margin-bottom:1rem' }, + 'Safe remediations are non-destructive changes that can be applied without risk.'), + E('div', { 'style': 'display:flex; gap:1rem; flex-wrap:wrap;' }, [ + E('button', { + 'class': 'cbi-button', + 'click': ui.createHandlerFn(this, function() { + return callRemediateSafe(true).then(function(res) { + var msg = 'Dry run: Would apply ' + (res.applied || 0) + ' safe fixes.'; + ui.addNotification(null, E('p', msg)); + }); + }) + }, 'Preview Safe Fixes'), + E('button', { + 'class': 'cbi-button cbi-button-apply', + 'click': ui.createHandlerFn(this, function() { + if (!confirm('Apply all safe remediations?')) return; + return callRemediateSafe(false).then(function(res) { + var msg = 'Applied ' + (res.applied || 0) + ' safe fixes.'; + ui.addNotification(null, E('p', msg)); + window.location.reload(); + }); + }) + }, 'Apply Safe Fixes') + ]) + ])); + + // Failed Checks + view.appendChild(E('h3', {}, 'Failed Checks (' + failedChecks.length + ')')); + + if (failedChecks.length === 0) { + view.appendChild(E('div', { + 'style': 'background:#166534; border-radius:8px; padding:1.5rem; text-align:center;' + }, [ + E('span', { 'style': 'font-size:48px;' }, '✔'), + E('p', { 'style': 'margin:1rem 0 0; color:#f1f5f9;' }, 'All security checks passed!') + ])); + } else { + var table = E('table', { 'class': 'table', 'style': 'width:100%' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Check ID'), + E('th', { 'class': 'th' }, 'Status'), + E('th', { 'class': 'th' }, 'Message'), + E('th', { 'class': 'th' }, 'Actions') + ]) + ]); + + var self = this; + failedChecks.forEach(function(check) { + var hasRemediation = remediableChecks.indexOf(check.id) !== -1; + var isSafe = safeChecks.indexOf(check.id) !== -1; + + var actions = []; + + if (hasRemediation) { + actions.push(E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'margin-right:0.5rem;', + 'click': ui.createHandlerFn(self, function() { + return callRemediate(check.id, true).then(function(res) { + if (res.error) { + ui.addNotification(null, E('p', { 'style': 'color:#ef4444' }, res.error)); + } else { + ui.addNotification(null, E('p', 'Preview: ' + (res.action || res))); + } + }); + }) + }, 'Preview')); + + actions.push(E('button', { + 'class': 'cbi-button cbi-button-apply', + 'click': ui.createHandlerFn(self, function() { + if (!confirm('Apply remediation for ' + check.id + '?')) return; + return callRemediate(check.id, false).then(function(res) { + if (res.error) { + ui.addNotification(null, E('p', { 'style': 'color:#ef4444' }, res.error)); + } else { + ui.addNotification(null, E('p', 'Applied: ' + (res.action || 'Remediation applied'))); + window.location.reload(); + } + }); + }) + }, isSafe ? 'Apply (Safe)' : 'Apply')); + } + + actions.push(E('button', { + 'class': 'cbi-button', + 'style': 'margin-left:0.5rem;', + 'click': ui.createHandlerFn(self, function() { + return callSuggest(check.id).then(function(res) { + var suggestion = res.suggestion || 'No suggestion available'; + var source = res.source || 'unknown'; + ui.showModal('Remediation Suggestion', [ + E('p', {}, [ + E('strong', {}, 'Check: '), check.id + ]), + E('p', {}, [ + E('strong', {}, 'Source: '), source + ]), + E('div', { + 'style': 'background:#1e293b; padding:1rem; border-radius:4px; margin-top:1rem;' + }, suggestion), + E('div', { 'class': 'right', 'style': 'margin-top:1rem;' }, [ + E('button', { + 'class': 'cbi-button', + 'click': ui.hideModal + }, 'Close') + ]) + ]); + }); + }) + }, 'Suggest')); + + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, E('code', {}, check.id)), + E('td', { 'class': 'td' }, E('span', { + 'style': 'color:' + getStatusColor(check.status) + '; text-transform:uppercase;' + }, check.status)), + E('td', { 'class': 'td' }, check.message || '-'), + E('td', { 'class': 'td' }, actions) + ])); + }); + + view.appendChild(table); + } + + // Pending Remediations + if (pending.length > 0) { + view.appendChild(E('h3', { 'style': 'margin-top:2rem' }, 'Pending Approvals')); + + var pendingTable = E('table', { 'class': 'table', 'style': 'width:100%' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Check ID'), + E('th', { 'class': 'th' }, 'Action'), + E('th', { 'class': 'th' }, 'Queued'), + E('th', { 'class': 'th' }, 'Status') + ]) + ]); + + pending.forEach(function(p) { + pendingTable.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, E('code', {}, p.check_id)), + E('td', { 'class': 'td' }, p.action || '-'), + E('td', { 'class': 'td' }, new Date(p.queued_at * 1000).toLocaleString()), + E('td', { 'class': 'td' }, E('span', { + 'style': 'color:#eab308; text-transform:uppercase;' + }, p.status || 'pending')) + ])); + }); + + view.appendChild(pendingTable); + } + + // Legend + view.appendChild(E('div', { + 'style': 'background:#1e293b; border-radius:8px; padding:1rem; margin-top:2rem;' + }, [ + E('h4', { 'style': 'margin-top:0' }, 'Available Remediations'), + E('ul', { 'style': 'color:#94a3b8; margin:0; padding-left:1.5rem;' }, [ + E('li', {}, E('strong', {}, 'NET-002: '), 'Restrict management access to LAN'), + E('li', {}, E('strong', {}, 'NET-004: '), 'Enable SYN flood protection'), + E('li', {}, E('strong', {}, 'FW-001: '), 'Set default deny policy on WAN'), + E('li', {}, E('strong', {}, 'FW-002: '), 'Enable drop invalid packets'), + E('li', {}, E('strong', {}, 'AUTH-003: '), 'Disable SSH password auth (requires SSH keys)'), + E('li', {}, E('strong', {}, 'CRYPT-001: '), 'Enable HTTPS redirect'), + E('li', {}, E('strong', {}, 'LOG-002: '), 'Configure log rotation') + ]) + ])); + + return view; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-config-advisor/htdocs/luci-static/resources/view/config-advisor/settings.js b/package/secubox/luci-app-config-advisor/htdocs/luci-static/resources/view/config-advisor/settings.js new file mode 100644 index 00000000..58e2d980 --- /dev/null +++ b/package/secubox/luci-app-config-advisor/htdocs/luci-static/resources/view/config-advisor/settings.js @@ -0,0 +1,139 @@ +'use strict'; +'require view'; +'require form'; +'require uci'; + +return view.extend({ + load: function() { + return uci.load('config-advisor'); + }, + + render: function() { + var m, s, o; + + m = new form.Map('config-advisor', 'Config Advisor Settings', + 'Configure security advisor behavior, compliance framework, and LocalAI integration.'); + + // Main settings + s = m.section(form.TypedSection, 'main', 'General Settings'); + s.anonymous = true; + + o = s.option(form.Flag, 'enabled', 'Enable Advisor', + 'Enable background security monitoring'); + o.default = '1'; + o.rmempty = false; + + o = s.option(form.Value, 'check_interval', 'Check Interval (seconds)', + 'How often to run security checks'); + o.datatype = 'uinteger'; + o.default = '3600'; + o.placeholder = '3600'; + + o = s.option(form.Flag, 'auto_remediate', 'Auto-Remediate Safe Issues', + 'Automatically apply safe remediations'); + o.default = '0'; + + o = s.option(form.Flag, 'notification_enabled', 'Enable Notifications', + 'Log warnings when security score drops'); + o.default = '1'; + + // Compliance settings + s = m.section(form.TypedSection, 'compliance', 'Compliance Settings'); + s.anonymous = true; + + o = s.option(form.ListValue, 'framework', 'Compliance Framework', + 'Select compliance standard to check against'); + o.value('anssi_cspn', 'ANSSI CSPN (French)'); + o.value('cis_benchmark', 'CIS Benchmark'); + o.value('custom', 'Custom'); + o.default = 'anssi_cspn'; + + o = s.option(form.Flag, 'strict_mode', 'Strict Mode', + 'Treat warnings as failures for compliance'); + o.default = '0'; + + // Scoring settings + s = m.section(form.TypedSection, 'scoring', 'Scoring Configuration'); + s.anonymous = true; + + o = s.option(form.Value, 'passing_score', 'Passing Score Threshold', + 'Minimum score to be considered secure (0-100)'); + o.datatype = 'range(0,100)'; + o.default = '70'; + o.placeholder = '70'; + + o = s.option(form.Value, 'weight_critical', 'Critical Weight', + 'Weight for critical severity checks'); + o.datatype = 'uinteger'; + o.default = '40'; + o.placeholder = '40'; + + o = s.option(form.Value, 'weight_high', 'High Weight', + 'Weight for high severity checks'); + o.datatype = 'uinteger'; + o.default = '25'; + o.placeholder = '25'; + + o = s.option(form.Value, 'weight_medium', 'Medium Weight', + 'Weight for medium severity checks'); + o.datatype = 'uinteger'; + o.default = '20'; + o.placeholder = '20'; + + o = s.option(form.Value, 'weight_low', 'Low Weight', + 'Weight for low severity checks'); + o.datatype = 'uinteger'; + o.default = '10'; + o.placeholder = '10'; + + // Category toggles + s = m.section(form.TypedSection, 'categories', 'Check Categories', + 'Enable or disable specific security check categories'); + s.anonymous = true; + + o = s.option(form.Flag, 'network', 'Network Checks'); + o.default = '1'; + + o = s.option(form.Flag, 'firewall', 'Firewall Checks'); + o.default = '1'; + + o = s.option(form.Flag, 'authentication', 'Authentication Checks'); + o.default = '1'; + + o = s.option(form.Flag, 'encryption', 'Encryption Checks'); + o.default = '1'; + + o = s.option(form.Flag, 'services', 'Services Checks'); + o.default = '1'; + + o = s.option(form.Flag, 'logging', 'Logging Checks'); + o.default = '1'; + + o = s.option(form.Flag, 'updates', 'Update Checks'); + o.default = '0'; + o.description = 'Can be slow as it queries opkg'; + + // LocalAI integration + s = m.section(form.TypedSection, 'localai', 'LocalAI Integration', + 'Configure AI-powered remediation suggestions'); + s.anonymous = true; + + o = s.option(form.Flag, 'enabled', 'Enable LocalAI', + 'Use LocalAI for intelligent remediation suggestions'); + o.default = '0'; + + o = s.option(form.Value, 'url', 'LocalAI URL', + 'URL of the LocalAI API endpoint'); + o.default = 'http://127.0.0.1:8091'; + o.placeholder = 'http://127.0.0.1:8091'; + o.depends('enabled', '1'); + + o = s.option(form.Value, 'model', 'Model Name', + 'LocalAI model to use for suggestions'); + o.default = 'mistral'; + o.placeholder = 'mistral'; + o.depends('enabled', '1'); + + return m.render(); + } +}); diff --git a/package/secubox/luci-app-config-advisor/root/usr/libexec/rpcd/luci.config-advisor b/package/secubox/luci-app-config-advisor/root/usr/libexec/rpcd/luci.config-advisor new file mode 100755 index 00000000..6403d205 --- /dev/null +++ b/package/secubox/luci-app-config-advisor/root/usr/libexec/rpcd/luci.config-advisor @@ -0,0 +1,184 @@ +#!/bin/sh +# RPCD handler for Config Advisor + +. /usr/share/libubox/jshn.sh + +# Load advisor libraries +[ -f /usr/lib/config-advisor/checks.sh ] && . /usr/lib/config-advisor/checks.sh +[ -f /usr/lib/config-advisor/anssi.sh ] && . /usr/lib/config-advisor/anssi.sh +[ -f /usr/lib/config-advisor/scoring.sh ] && . /usr/lib/config-advisor/scoring.sh +[ -f /usr/lib/config-advisor/remediate.sh ] && . /usr/lib/config-advisor/remediate.sh + +case "$1" in + list) + echo '{"status":{},"results":{},"score":{},"compliance":{},"check":{},"pending":{},"history":{"count":30},"suggest":{"check_id":"string"},"remediate":{"check_id":"string","dry_run":false},"remediate_safe":{"dry_run":false},"set_config":{"key":"string","value":"string"}}' + ;; + call) + case "$2" in + status) + # Get advisor status + json_init + json_add_string "version" "$(config-advisorctl version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo '0.1.0')" + json_add_boolean "enabled" "$(uci -q get config-advisor.main.enabled || echo 0)" + json_add_string "framework" "$(uci -q get config-advisor.compliance.framework || echo 'anssi_cspn')" + + # Last check timestamp + local last_check=0 + if [ -f /var/lib/config-advisor/results.json ]; then + last_check=$(stat -c %Y /var/lib/config-advisor/results.json 2>/dev/null || echo 0) + fi + json_add_int "last_check" "$last_check" + + # Score info + local score grade risk_level + if [ -f /var/lib/config-advisor/score.json ]; then + score=$(jsonfilter -i /var/lib/config-advisor/score.json -e '@.score' 2>/dev/null || echo 0) + grade=$(jsonfilter -i /var/lib/config-advisor/score.json -e '@.grade' 2>/dev/null || echo '?') + risk_level=$(jsonfilter -i /var/lib/config-advisor/score.json -e '@.risk_level' 2>/dev/null || echo 'unknown') + else + score=0 + grade="?" + risk_level="unknown" + fi + json_add_int "score" "$score" + json_add_string "grade" "$grade" + json_add_string "risk_level" "$risk_level" + + # Compliance rate + local compliance_rate=0 + if [ -f /var/lib/config-advisor/compliance.json ]; then + compliance_rate=$(jsonfilter -i /var/lib/config-advisor/compliance.json -e '@.compliance_rate' 2>/dev/null || echo 0) + fi + json_add_int "compliance_rate" "${compliance_rate%.*}" + + # LocalAI status + json_add_object "localai" + json_add_boolean "enabled" "$(uci -q get config-advisor.localai.enabled || echo 0)" + json_add_string "url" "$(uci -q get config-advisor.localai.url || echo 'http://127.0.0.1:8091')" + json_close_object + + json_dump + ;; + + results) + # Get check results + if [ -f /var/lib/config-advisor/results.json ]; then + echo "{\"results\":$(cat /var/lib/config-advisor/results.json)}" + else + echo '{"results":[]}' + fi + ;; + + score) + # Get score details + if [ -f /var/lib/config-advisor/score.json ]; then + cat /var/lib/config-advisor/score.json + else + echo '{"error":"No score available"}' + fi + ;; + + compliance) + # Get compliance report + if [ -f /var/lib/config-advisor/compliance.json ]; then + cat /var/lib/config-advisor/compliance.json + else + echo '{"error":"No compliance report available"}' + fi + ;; + + check) + # Run full check + run_all_checks >/dev/null 2>&1 + anssi_run_compliance >/dev/null 2>&1 + scoring_calculate >/dev/null 2>&1 + + json_init + json_add_boolean "success" 1 + json_add_string "message" "Check completed" + json_dump + ;; + + pending) + # Get pending remediations + if [ -f /var/lib/config-advisor/pending_remediations.json ]; then + echo "{\"pending\":$(cat /var/lib/config-advisor/pending_remediations.json)}" + else + echo '{"pending":[]}' + fi + ;; + + history) + read -r input + json_load "$input" + json_get_var count count + [ -z "$count" ] && count=30 + + if [ -f /var/lib/config-advisor/score_history.json ]; then + local history + history=$(jsonfilter -i /var/lib/config-advisor/score_history.json -e "@[-$count:]" 2>/dev/null || echo "[]") + echo "{\"history\":$history}" + else + echo '{"history":[]}' + fi + ;; + + suggest) + read -r input + json_load "$input" + json_get_var check_id check_id + + if [ -z "$check_id" ]; then + echo '{"error":"check_id required"}' + else + remediate_suggest "$check_id" + fi + ;; + + remediate) + read -r input + json_load "$input" + json_get_var check_id check_id + json_get_var dry_run dry_run + + if [ -z "$check_id" ]; then + echo '{"error":"check_id required"}' + else + [ "$dry_run" = "1" ] || [ "$dry_run" = "true" ] && dry_run=1 || dry_run=0 + remediate_apply "$check_id" "$dry_run" + fi + ;; + + remediate_safe) + read -r input + json_load "$input" + json_get_var dry_run dry_run + + [ "$dry_run" = "1" ] || [ "$dry_run" = "true" ] && dry_run=1 || dry_run=0 + remediate_apply_safe "$dry_run" + ;; + + set_config) + read -r input + json_load "$input" + json_get_var key key + json_get_var value value + + if [ -z "$key" ]; then + echo '{"error":"key required"}' + else + uci set "config-advisor.$key=$value" + uci commit config-advisor + + json_init + json_add_boolean "success" 1 + json_dump + fi + ;; + + *) + echo '{"error":"Unknown method"}' + ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-config-advisor/root/usr/share/luci/menu.d/luci-app-config-advisor.json b/package/secubox/luci-app-config-advisor/root/usr/share/luci/menu.d/luci-app-config-advisor.json new file mode 100644 index 00000000..f45cb8c5 --- /dev/null +++ b/package/secubox/luci-app-config-advisor/root/usr/share/luci/menu.d/luci-app-config-advisor.json @@ -0,0 +1,38 @@ +{ + "admin/services/config-advisor": { + "title": "Config Advisor", + "order": 85, + "action": { + "type": "view", + "path": "config-advisor/dashboard" + }, + "depends": { + "acl": ["luci-app-config-advisor"], + "uci": {"config-advisor": true} + } + }, + "admin/services/config-advisor/compliance": { + "title": "Compliance", + "order": 10, + "action": { + "type": "view", + "path": "config-advisor/compliance" + } + }, + "admin/services/config-advisor/remediation": { + "title": "Remediation", + "order": 20, + "action": { + "type": "view", + "path": "config-advisor/remediation" + } + }, + "admin/services/config-advisor/settings": { + "title": "Settings", + "order": 30, + "action": { + "type": "view", + "path": "config-advisor/settings" + } + } +} diff --git a/package/secubox/luci-app-config-advisor/root/usr/share/rpcd/acl.d/luci-app-config-advisor.json b/package/secubox/luci-app-config-advisor/root/usr/share/rpcd/acl.d/luci-app-config-advisor.json new file mode 100644 index 00000000..04e922c9 --- /dev/null +++ b/package/secubox/luci-app-config-advisor/root/usr/share/rpcd/acl.d/luci-app-config-advisor.json @@ -0,0 +1,17 @@ +{ + "luci-app-config-advisor": { + "description": "Grant access to Config Advisor", + "read": { + "ubus": { + "luci.config-advisor": ["status", "results", "score", "compliance", "pending", "history", "suggest"] + }, + "uci": ["config-advisor"] + }, + "write": { + "ubus": { + "luci.config-advisor": ["check", "remediate", "remediate_safe", "set_config"] + }, + "uci": ["config-advisor"] + } + } +} diff --git a/package/secubox/secubox-config-advisor/Makefile b/package/secubox/secubox-config-advisor/Makefile new file mode 100644 index 00000000..6aa4fc0b --- /dev/null +++ b/package/secubox/secubox-config-advisor/Makefile @@ -0,0 +1,60 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-config-advisor +PKG_VERSION:=0.1.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox Team +PKG_LICENSE:=GPL-3.0 + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-config-advisor + SECTION:=secubox + CATEGORY:=SecuBox + TITLE:=Configuration Security Advisor + DEPENDS:=+jsonfilter +curl +openssl-util + PKGARCH:=all +endef + +define Package/secubox-config-advisor/description + AI-powered configuration security advisor for SecuBox. + Features: + - ANSSI CSPN compliance checking + - Security hardening recommendations + - Configuration drift detection + - Risk scoring and prioritization + - LocalAI integration for intelligent analysis + - Automated remediation suggestions +endef + +define Package/secubox-config-advisor/conffiles +/etc/config/config-advisor +endef + +define Build/Compile +endef + +define Package/secubox-config-advisor/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/config-advisor $(1)/etc/config/config-advisor + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/etc/init.d/config-advisor $(1)/etc/init.d/config-advisor + + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./files/usr/sbin/config-advisorctl $(1)/usr/sbin/config-advisorctl + + $(INSTALL_DIR) $(1)/usr/lib/config-advisor + $(INSTALL_DATA) ./files/usr/lib/config-advisor/checks.sh $(1)/usr/lib/config-advisor/checks.sh + $(INSTALL_DATA) ./files/usr/lib/config-advisor/anssi.sh $(1)/usr/lib/config-advisor/anssi.sh + $(INSTALL_DATA) ./files/usr/lib/config-advisor/scoring.sh $(1)/usr/lib/config-advisor/scoring.sh + $(INSTALL_DATA) ./files/usr/lib/config-advisor/remediate.sh $(1)/usr/lib/config-advisor/remediate.sh + + $(INSTALL_DIR) $(1)/usr/share/config-advisor + $(INSTALL_DATA) ./files/usr/share/config-advisor/anssi-rules.json $(1)/usr/share/config-advisor/anssi-rules.json + + $(INSTALL_DIR) $(1)/var/lib/config-advisor +endef + +$(eval $(call BuildPackage,secubox-config-advisor)) diff --git a/package/secubox/secubox-config-advisor/files/etc/config/config-advisor b/package/secubox/secubox-config-advisor/files/etc/config/config-advisor new file mode 100644 index 00000000..bc656d5c --- /dev/null +++ b/package/secubox/secubox-config-advisor/files/etc/config/config-advisor @@ -0,0 +1,37 @@ +config advisor 'main' + option enabled '1' + option check_interval '3600' + # Check every hour + option auto_remediate '0' + # Manual remediation by default + option notification_enabled '1' + +config localai 'localai' + option enabled '1' + option url 'http://127.0.0.1:8091' + option model 'mistral' + option min_confidence '75' + +config compliance 'compliance' + option framework 'anssi_cspn' + # Frameworks: anssi_cspn, cis, nist, custom + option strict_mode '0' + # Strict mode fails on warnings + option report_format 'json' + +config scoring 'scoring' + option weight_critical '40' + option weight_high '25' + option weight_medium '20' + option weight_low '10' + option weight_info '5' + option passing_score '70' + +config categories 'categories' + option network '1' + option firewall '1' + option authentication '1' + option encryption '1' + option services '1' + option logging '1' + option updates '1' diff --git a/package/secubox/secubox-config-advisor/files/etc/init.d/config-advisor b/package/secubox/secubox-config-advisor/files/etc/init.d/config-advisor new file mode 100644 index 00000000..92429d3f --- /dev/null +++ b/package/secubox/secubox-config-advisor/files/etc/init.d/config-advisor @@ -0,0 +1,38 @@ +#!/bin/sh /etc/rc.common + +START=99 +STOP=10 +USE_PROCD=1 + +PROG=/usr/sbin/config-advisorctl + +start_service() { + local enabled + config_load config-advisor + config_get enabled main enabled '0' + + [ "$enabled" = "1" ] || return 0 + + procd_open_instance + procd_set_param command "$PROG" daemon + procd_set_param respawn 3600 5 5 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param pidfile /var/run/config-advisor.pid + procd_close_instance + + logger -t config-advisor "Config Advisor daemon started" +} + +stop_service() { + logger -t config-advisor "Config Advisor daemon stopped" +} + +reload_service() { + stop + start +} + +service_triggers() { + procd_add_reload_trigger "config-advisor" +} diff --git a/package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/anssi.sh b/package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/anssi.sh new file mode 100755 index 00000000..a5c381e5 --- /dev/null +++ b/package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/anssi.sh @@ -0,0 +1,272 @@ +#!/bin/sh +# Config Advisor - ANSSI CSPN Compliance Module + +. /lib/functions.sh + +RULES_FILE="/usr/share/config-advisor/anssi-rules.json" +COMPLIANCE_REPORT="/var/lib/config-advisor/compliance.json" + +# Load ANSSI rules +anssi_load_rules() { + if [ -f "$RULES_FILE" ]; then + cat "$RULES_FILE" + else + echo '{"error": "Rules file not found"}' + return 1 + fi +} + +# Get rules for a category +anssi_get_category_rules() { + local category="$1" + jsonfilter -i "$RULES_FILE" -e "@.categories.$category.rules[*]" 2>/dev/null +} + +# Get all categories +anssi_get_categories() { + jsonfilter -i "$RULES_FILE" -e '@.categories' 2>/dev/null | \ + grep -oE '"[a-z]+":' | tr -d '":' | sort -u +} + +# Check if category is enabled +_is_category_enabled() { + local category="$1" + local enabled + enabled=$(uci -q get config-advisor.categories."$category") + [ "$enabled" != "0" ] +} + +# Run ANSSI compliance check +anssi_run_compliance() { + local timestamp + timestamp=$(date +%s) + + local total=0 + local passed=0 + local failed=0 + local warnings=0 + local info=0 + + local results="[" + local first=1 + + # Load check functions + . /usr/lib/config-advisor/checks.sh + + # Iterate through categories + for category in $(anssi_get_categories); do + _is_category_enabled "$category" || continue + + local rules + rules=$(anssi_get_category_rules "$category") + + echo "$rules" | while read -r rule; do + [ -z "$rule" ] && continue + + local rule_id check_func severity + rule_id=$(echo "$rule" | jsonfilter -e '@.id' 2>/dev/null) + check_func=$(echo "$rule" | jsonfilter -e '@.check' 2>/dev/null) + severity=$(echo "$rule" | jsonfilter -e '@.severity' 2>/dev/null) + + [ -z "$rule_id" ] && continue + + total=$((total + 1)) + + # Run the check function + local status="skip" + if type "check_$check_func" >/dev/null 2>&1; then + if "check_$check_func" 2>/dev/null; then + status="pass" + passed=$((passed + 1)) + else + case "$severity" in + critical|high) + status="fail" + failed=$((failed + 1)) + ;; + medium) + status="warn" + warnings=$((warnings + 1)) + ;; + *) + status="info" + info=$((info + 1)) + ;; + esac + fi + else + status="skip" + fi + + [ "$first" = "1" ] || results="$results," + results="$results{\"rule_id\":\"$rule_id\",\"category\":\"$category\",\"severity\":\"$severity\",\"status\":\"$status\"}" + first=0 + done + done + + results="$results]" + + # Generate compliance report + cat > "$COMPLIANCE_REPORT" </dev/null || echo "0"), + "results": $results +} +EOF + + cat "$COMPLIANCE_REPORT" +} + +# Get compliance status +anssi_get_status() { + if [ -f "$COMPLIANCE_REPORT" ]; then + cat "$COMPLIANCE_REPORT" + else + echo '{"error": "No compliance report available. Run check first."}' + fi +} + +# Get failing rules +anssi_get_failures() { + if [ -f "$COMPLIANCE_REPORT" ]; then + jsonfilter -i "$COMPLIANCE_REPORT" -e '@.results[*]' 2>/dev/null | \ + grep '"status":"fail"' + else + echo "[]" + fi +} + +# Get warnings +anssi_get_warnings() { + if [ -f "$COMPLIANCE_REPORT" ]; then + jsonfilter -i "$COMPLIANCE_REPORT" -e '@.results[*]' 2>/dev/null | \ + grep '"status":"warn"' + else + echo "[]" + fi +} + +# Generate human-readable report +anssi_generate_report() { + local format="${1:-text}" + + if [ ! -f "$COMPLIANCE_REPORT" ]; then + echo "No compliance report available." + return 1 + fi + + local summary + summary=$(jsonfilter -i "$COMPLIANCE_REPORT" -e '@.summary') + + local total passed failed warnings compliance_rate + total=$(echo "$summary" | jsonfilter -e '@.total' 2>/dev/null) + passed=$(echo "$summary" | jsonfilter -e '@.passed' 2>/dev/null) + failed=$(echo "$summary" | jsonfilter -e '@.failed' 2>/dev/null) + warnings=$(echo "$summary" | jsonfilter -e '@.warnings' 2>/dev/null) + compliance_rate=$(jsonfilter -i "$COMPLIANCE_REPORT" -e '@.compliance_rate' 2>/dev/null) + + case "$format" in + text) + cat </dev/null) + category=$(echo "$result" | jsonfilter -e '@.category' 2>/dev/null) + echo " [$rule_id] $category" + done + echo "" + fi + + if [ "$warnings" -gt 0 ]; then + echo "Warnings:" + echo "---------" + anssi_get_warnings | while read -r result; do + local rule_id category + rule_id=$(echo "$result" | jsonfilter -e '@.rule_id' 2>/dev/null) + category=$(echo "$result" | jsonfilter -e '@.category' 2>/dev/null) + echo " [$rule_id] $category" + done + fi + ;; + + json) + cat "$COMPLIANCE_REPORT" + ;; + + markdown) + cat </dev/null) + category=$(echo "$result" | jsonfilter -e '@.category' 2>/dev/null) + severity=$(echo "$result" | jsonfilter -e '@.severity' 2>/dev/null) + echo "- **$rule_id** ($category) - Severity: $severity" + done + echo "" + fi + ;; + esac +} + +# Check if system is ANSSI compliant +anssi_is_compliant() { + local strict_mode + strict_mode=$(uci -q get config-advisor.compliance.strict_mode || echo "0") + + local failed warnings + failed=$(jsonfilter -i "$COMPLIANCE_REPORT" -e '@.summary.failed' 2>/dev/null || echo "999") + warnings=$(jsonfilter -i "$COMPLIANCE_REPORT" -e '@.summary.warnings' 2>/dev/null || echo "0") + + if [ "$failed" -eq 0 ]; then + if [ "$strict_mode" = "1" ] && [ "$warnings" -gt 0 ]; then + return 1 + fi + return 0 + fi + + return 1 +} diff --git a/package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/checks.sh b/package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/checks.sh new file mode 100755 index 00000000..37d80053 --- /dev/null +++ b/package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/checks.sh @@ -0,0 +1,319 @@ +#!/bin/sh +# Config Advisor - Security Check Functions + +. /lib/functions.sh + +RESULTS_FILE="/var/lib/config-advisor/results.json" + +# Initialize results storage +checks_init() { + mkdir -p /var/lib/config-advisor + echo '[]' > "$RESULTS_FILE" +} + +# Record check result +_record_result() { + local check_id="$1" + local status="$2" + local message="$3" + local details="$4" + + local timestamp + timestamp=$(date +%s) + + local result="{\"id\":\"$check_id\",\"status\":\"$status\",\"message\":\"$message\",\"details\":\"$details\",\"timestamp\":$timestamp}" + + local tmp_file="/tmp/check_results_$$.json" + if [ -s "$RESULTS_FILE" ] && [ "$(cat "$RESULTS_FILE")" != "[]" ]; then + sed 's/]$/,'"$result"']/' "$RESULTS_FILE" > "$tmp_file" + else + echo "[$result]" > "$tmp_file" + fi + mv "$tmp_file" "$RESULTS_FILE" +} + +# Network checks +check_ipv6_disabled() { + local ula_prefix + ula_prefix=$(uci -q get network.globals.ula_prefix) + + if [ -z "$ula_prefix" ]; then + _record_result "NET-001" "pass" "IPv6 ULA prefix not configured" "" + return 0 + else + _record_result "NET-001" "info" "IPv6 enabled with ULA prefix" "$ula_prefix" + return 1 + fi +} + +check_mgmt_restricted() { + local wan_ssh wan_https + wan_ssh=$(uci show firewall 2>/dev/null | grep -c "src='wan'.*dest_port='22'.*target='ACCEPT'") + wan_https=$(uci show firewall 2>/dev/null | grep -c "src='wan'.*dest_port='443'.*target='ACCEPT'") + + if [ "$wan_ssh" -eq 0 ] && [ "$wan_https" -eq 0 ]; then + _record_result "NET-002" "pass" "Management access restricted to LAN" "" + return 0 + else + _record_result "NET-002" "fail" "Management ports open on WAN" "SSH:$wan_ssh HTTPS:$wan_https" + return 1 + fi +} + +check_syn_flood_protection() { + local syn_protect + syn_protect=$(uci -q get firewall.@defaults[0].synflood_protect) + + if [ "$syn_protect" = "1" ]; then + _record_result "NET-004" "pass" "SYN flood protection enabled" "" + return 0 + else + _record_result "NET-004" "fail" "SYN flood protection not enabled" "" + return 1 + fi +} + +# Firewall checks +check_default_deny() { + local wan_input wan_forward + wan_input=$(uci -q get firewall.wan.input || echo "ACCEPT") + wan_forward=$(uci -q get firewall.wan.forward || echo "ACCEPT") + + if [ "$wan_input" = "REJECT" ] || [ "$wan_input" = "DROP" ]; then + if [ "$wan_forward" = "REJECT" ] || [ "$wan_forward" = "DROP" ]; then + _record_result "FW-001" "pass" "Default deny policy on WAN" "input=$wan_input forward=$wan_forward" + return 0 + fi + fi + + _record_result "FW-001" "fail" "WAN zone not properly restricted" "input=$wan_input forward=$wan_forward" + return 1 +} + +check_drop_invalid() { + local drop_invalid + drop_invalid=$(uci -q get firewall.@defaults[0].drop_invalid) + + if [ "$drop_invalid" = "1" ]; then + _record_result "FW-002" "pass" "Invalid packets dropped" "" + return 0 + else + _record_result "FW-002" "fail" "Invalid packets not dropped" "" + return 1 + fi +} + +check_wan_ports_closed() { + local open_ports + open_ports=$(uci show firewall 2>/dev/null | grep -c "src='wan'.*target='ACCEPT'") + + if [ "$open_ports" -le 2 ]; then + _record_result "FW-003" "pass" "Minimal ports open on WAN" "count=$open_ports" + return 0 + else + _record_result "FW-003" "warn" "Multiple ports open on WAN" "count=$open_ports" + return 1 + fi +} + +# Authentication checks +check_root_password_set() { + local root_hash + root_hash=$(grep "^root:" /etc/shadow 2>/dev/null | cut -d: -f2) + + if [ -n "$root_hash" ] && [ "$root_hash" != "!" ] && [ "$root_hash" != "*" ] && [ "$root_hash" != "" ]; then + _record_result "AUTH-001" "pass" "Root password is set" "" + return 0 + else + _record_result "AUTH-001" "fail" "Root password not set" "" + return 1 + fi +} + +check_ssh_key_auth() { + local authorized_keys="/etc/dropbear/authorized_keys" + + if [ -s "$authorized_keys" ]; then + local key_count + key_count=$(wc -l < "$authorized_keys") + _record_result "AUTH-002" "pass" "SSH keys configured" "keys=$key_count" + return 0 + else + _record_result "AUTH-002" "warn" "No SSH keys configured" "Password auth only" + return 1 + fi +} + +check_ssh_no_root_password() { + local password_auth + password_auth=$(uci -q get dropbear.@dropbear[0].PasswordAuth) + + if [ "$password_auth" = "off" ] || [ "$password_auth" = "0" ]; then + _record_result "AUTH-003" "pass" "SSH password auth disabled" "" + return 0 + else + _record_result "AUTH-003" "warn" "SSH password auth enabled" "" + return 1 + fi +} + +# Encryption checks +check_https_enabled() { + local https_enabled redirect_https + https_enabled=$(uci -q get uhttpd.main.listen_https) + redirect_https=$(uci -q get uhttpd.main.redirect_https) + + if [ -n "$https_enabled" ]; then + if [ "$redirect_https" = "1" ]; then + _record_result "CRYPT-001" "pass" "HTTPS enabled with redirect" "" + return 0 + else + _record_result "CRYPT-001" "warn" "HTTPS enabled but no redirect" "" + return 0 + fi + else + _record_result "CRYPT-001" "fail" "HTTPS not configured" "" + return 1 + fi +} + +check_wireguard_configured() { + local wg_interfaces + wg_interfaces=$(uci show network 2>/dev/null | grep -c "proto='wireguard'") + + if [ "$wg_interfaces" -gt 0 ]; then + _record_result "CRYPT-003" "pass" "WireGuard configured" "interfaces=$wg_interfaces" + return 0 + else + _record_result "CRYPT-003" "info" "WireGuard not configured" "" + return 1 + fi +} + +check_dns_encrypted() { + # Check for AdGuard Home or stubby + if pgrep -x AdGuardHome >/dev/null 2>&1; then + _record_result "CRYPT-004" "pass" "AdGuard Home running (encrypted DNS)" "" + return 0 + elif pgrep -x stubby >/dev/null 2>&1; then + _record_result "CRYPT-004" "pass" "Stubby running (DoT)" "" + return 0 + else + _record_result "CRYPT-004" "warn" "No encrypted DNS resolver detected" "" + return 1 + fi +} + +# Service checks +check_crowdsec_enabled() { + if pgrep crowdsec >/dev/null 2>&1; then + _record_result "SVC-003" "pass" "CrowdSec is running" "" + return 0 + else + _record_result "SVC-003" "fail" "CrowdSec not running" "" + return 1 + fi +} + +check_services_localhost() { + local exposed_services=0 + + # Check common services + if netstat -tln 2>/dev/null | grep -q "0.0.0.0:8091"; then + exposed_services=$((exposed_services + 1)) + fi + + if [ "$exposed_services" -eq 0 ]; then + _record_result "SVC-002" "pass" "Services properly bound" "" + return 0 + else + _record_result "SVC-002" "warn" "Some services bound to 0.0.0.0" "count=$exposed_services" + return 1 + fi +} + +# Logging checks +check_syslog_enabled() { + if pgrep logd >/dev/null 2>&1 || pgrep syslog >/dev/null 2>&1; then + _record_result "LOG-001" "pass" "System logging enabled" "" + return 0 + else + _record_result "LOG-001" "fail" "System logging not running" "" + return 1 + fi +} + +check_log_rotation() { + local log_size + log_size=$(uci -q get system.@system[0].log_size) + + if [ -n "$log_size" ] && [ "$log_size" -gt 0 ]; then + _record_result "LOG-002" "pass" "Log rotation configured" "size=${log_size}KB" + return 0 + else + _record_result "LOG-002" "warn" "Log rotation not configured" "" + return 1 + fi +} + +# Update checks +check_system_uptodate() { + opkg update >/dev/null 2>&1 + local upgradable + upgradable=$(opkg list-upgradable 2>/dev/null | wc -l) + + if [ "$upgradable" -eq 0 ]; then + _record_result "UPD-001" "pass" "System is up to date" "" + return 0 + else + _record_result "UPD-001" "warn" "Packages need updating" "count=$upgradable" + return 1 + fi +} + +# Run all checks +run_all_checks() { + checks_init + + # Network + check_ipv6_disabled + check_mgmt_restricted + check_syn_flood_protection + + # Firewall + check_default_deny + check_drop_invalid + check_wan_ports_closed + + # Authentication + check_root_password_set + check_ssh_key_auth + check_ssh_no_root_password + + # Encryption + check_https_enabled + check_wireguard_configured + check_dns_encrypted + + # Services + check_crowdsec_enabled + check_services_localhost + + # Logging + check_syslog_enabled + check_log_rotation + + # Updates (can be slow) + # check_system_uptodate + + cat "$RESULTS_FILE" +} + +# Get results +get_results() { + if [ -f "$RESULTS_FILE" ]; then + cat "$RESULTS_FILE" + else + echo "[]" + fi +} diff --git a/package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/remediate.sh b/package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/remediate.sh new file mode 100755 index 00000000..57ba9eae --- /dev/null +++ b/package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/remediate.sh @@ -0,0 +1,294 @@ +#!/bin/sh +# Config Advisor - Remediation Module + +. /lib/functions.sh + +REMEDIATION_LOG="/var/lib/config-advisor/remediation.log" +PENDING_FILE="/var/lib/config-advisor/pending_remediations.json" + +# Initialize remediation storage +remediate_init() { + mkdir -p /var/lib/config-advisor + [ -f "$PENDING_FILE" ] || echo '[]' > "$PENDING_FILE" +} + +# Log remediation action +_log_remediation() { + local check_id="$1" + local action="$2" + local status="$3" + local details="$4" + + local timestamp + timestamp=$(date '+%Y-%m-%d %H:%M:%S') + + echo "[$timestamp] $check_id: $action - $status - $details" >> "$REMEDIATION_LOG" +} + +# Remediation functions for specific checks + +# NET-002: Restrict management access +remediate_mgmt_restricted() { + local dry_run="${1:-0}" + + if [ "$dry_run" = "1" ]; then + echo "Would add firewall rules to restrict SSH and HTTPS to LAN only" + return 0 + fi + + # Remove any WAN SSH/HTTPS rules + local changed=0 + + # This is a destructive operation - be careful + # Only remove explicit WAN allow rules for management ports + uci show firewall 2>/dev/null | grep -E "src='wan'.*dest_port='(22|443)'.*target='ACCEPT'" | \ + while read -r rule; do + local rule_name + rule_name=$(echo "$rule" | cut -d. -f2 | cut -d= -f1) + if [ -n "$rule_name" ]; then + uci delete "firewall.$rule_name" + changed=1 + _log_remediation "NET-002" "Removed WAN rule" "success" "$rule_name" + fi + done + + if [ "$changed" = "1" ]; then + uci commit firewall + /etc/init.d/firewall reload + fi + + echo '{"success": true, "action": "Restricted management access to LAN"}' +} + +# NET-004: Enable SYN flood protection +remediate_syn_flood_protection() { + local dry_run="${1:-0}" + + if [ "$dry_run" = "1" ]; then + echo "Would enable synflood_protect in firewall defaults" + return 0 + fi + + uci set firewall.@defaults[0].synflood_protect='1' + uci commit firewall + /etc/init.d/firewall reload + + _log_remediation "NET-004" "Enabled SYN flood protection" "success" "" + echo '{"success": true, "action": "Enabled SYN flood protection"}' +} + +# FW-001: Default deny policy +remediate_default_deny() { + local dry_run="${1:-0}" + + if [ "$dry_run" = "1" ]; then + echo "Would set WAN zone to input=REJECT, forward=REJECT" + return 0 + fi + + uci set firewall.wan.input='REJECT' + uci set firewall.wan.forward='REJECT' + uci commit firewall + /etc/init.d/firewall reload + + _log_remediation "FW-001" "Set default deny on WAN" "success" "" + echo '{"success": true, "action": "Set default deny policy on WAN"}' +} + +# FW-002: Drop invalid packets +remediate_drop_invalid() { + local dry_run="${1:-0}" + + if [ "$dry_run" = "1" ]; then + echo "Would enable drop_invalid in firewall defaults" + return 0 + fi + + uci set firewall.@defaults[0].drop_invalid='1' + uci commit firewall + /etc/init.d/firewall reload + + _log_remediation "FW-002" "Enabled drop invalid" "success" "" + echo '{"success": true, "action": "Enabled drop invalid packets"}' +} + +# AUTH-003: Disable SSH password auth +remediate_ssh_no_root_password() { + local dry_run="${1:-0}" + + # Safety check: ensure SSH keys exist first + if [ ! -s /etc/dropbear/authorized_keys ]; then + echo '{"success": false, "error": "Cannot disable password auth without SSH keys configured"}' + return 1 + fi + + if [ "$dry_run" = "1" ]; then + echo "Would disable password authentication for SSH" + return 0 + fi + + uci set dropbear.@dropbear[0].PasswordAuth='off' + uci set dropbear.@dropbear[0].RootPasswordAuth='off' + uci commit dropbear + /etc/init.d/dropbear restart + + _log_remediation "AUTH-003" "Disabled SSH password auth" "success" "" + echo '{"success": true, "action": "Disabled SSH password authentication"}' +} + +# CRYPT-001: Enable HTTPS +remediate_https_enabled() { + local dry_run="${1:-0}" + + if [ "$dry_run" = "1" ]; then + echo "Would enable HTTPS redirect in uhttpd" + return 0 + fi + + uci set uhttpd.main.redirect_https='1' + uci commit uhttpd + /etc/init.d/uhttpd restart + + _log_remediation "CRYPT-001" "Enabled HTTPS redirect" "success" "" + echo '{"success": true, "action": "Enabled HTTPS redirect"}' +} + +# LOG-002: Configure log rotation +remediate_log_rotation() { + local dry_run="${1:-0}" + + if [ "$dry_run" = "1" ]; then + echo "Would set log size to 128KB" + return 0 + fi + + uci set system.@system[0].log_size='128' + uci commit system + /etc/init.d/system reload + + _log_remediation "LOG-002" "Configured log rotation" "success" "128KB" + echo '{"success": true, "action": "Configured log rotation to 128KB"}' +} + +# Queue remediation for approval +remediate_queue() { + local check_id="$1" + local action="$2" + local timestamp + timestamp=$(date +%s) + + local entry="{\"check_id\":\"$check_id\",\"action\":\"$action\",\"queued_at\":$timestamp,\"status\":\"pending\"}" + + local tmp_file="/tmp/pending_rem_$$.json" + if [ -s "$PENDING_FILE" ] && [ "$(cat "$PENDING_FILE")" != "[]" ]; then + sed 's/]$/,'"$entry"']/' "$PENDING_FILE" > "$tmp_file" + else + echo "[$entry]" > "$tmp_file" + fi + mv "$tmp_file" "$PENDING_FILE" + + logger -t config-advisor "Queued remediation: $check_id - $action" + echo '{"success": true, "queued": true}' +} + +# Get pending remediations +remediate_get_pending() { + cat "$PENDING_FILE" +} + +# Apply single remediation +remediate_apply() { + local check_id="$1" + local dry_run="${2:-0}" + + case "$check_id" in + NET-002) remediate_mgmt_restricted "$dry_run" ;; + NET-004) remediate_syn_flood_protection "$dry_run" ;; + FW-001) remediate_default_deny "$dry_run" ;; + FW-002) remediate_drop_invalid "$dry_run" ;; + AUTH-003) remediate_ssh_no_root_password "$dry_run" ;; + CRYPT-001) remediate_https_enabled "$dry_run" ;; + LOG-002) remediate_log_rotation "$dry_run" ;; + *) + echo "{\"success\": false, \"error\": \"No remediation available for $check_id\"}" + return 1 + ;; + esac +} + +# Apply all safe remediations +remediate_apply_safe() { + local dry_run="${1:-0}" + + local applied=0 + local failed=0 + + # Safe remediations (non-destructive) + for check_id in NET-004 FW-002 CRYPT-001 LOG-002; do + if remediate_apply "$check_id" "$dry_run" 2>/dev/null | grep -q '"success": true'; then + applied=$((applied + 1)) + else + failed=$((failed + 1)) + fi + done + + echo "{\"applied\": $applied, \"failed\": $failed, \"dry_run\": $([ "$dry_run" = "1" ] && echo "true" || echo "false")}" +} + +# Get remediation suggestions using LocalAI +remediate_suggest() { + local check_id="$1" + + local localai_enabled localai_url localai_model + localai_enabled=$(uci -q get config-advisor.localai.enabled || echo "0") + localai_url=$(uci -q get config-advisor.localai.url || echo "http://127.0.0.1:8091") + localai_model=$(uci -q get config-advisor.localai.model || echo "mistral") + + if [ "$localai_enabled" != "1" ]; then + # Return static suggestion + local rule_info + rule_info=$(jsonfilter -i /usr/share/config-advisor/anssi-rules.json \ + -e '@.categories[*].rules[*]' 2>/dev/null | grep "\"id\":\"$check_id\"" | head -1) + + if [ -n "$rule_info" ]; then + local remediation + remediation=$(echo "$rule_info" | jsonfilter -e '@.remediation' 2>/dev/null) + echo "{\"check_id\": \"$check_id\", \"suggestion\": \"$remediation\", \"source\": \"static\"}" + else + echo "{\"check_id\": \"$check_id\", \"suggestion\": \"No remediation available\", \"source\": \"none\"}" + fi + return + fi + + # Get AI suggestion + local prompt="You are a security configuration advisor. The security check '$check_id' has failed. Provide a concise remediation recommendation for OpenWrt. Be specific and actionable." + + local response + response=$(curl -s -X POST "$localai_url/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -d "{\"model\":\"$localai_model\",\"messages\":[{\"role\":\"user\",\"content\":\"$prompt\"}],\"max_tokens\":200}" \ + --connect-timeout 10 2>/dev/null) + + if [ -n "$response" ]; then + local suggestion + suggestion=$(echo "$response" | jsonfilter -e '@.choices[0].message.content' 2>/dev/null) + echo "{\"check_id\": \"$check_id\", \"suggestion\": \"$suggestion\", \"source\": \"ai\"}" + else + # Fallback to static + remediate_suggest "$check_id" + fi +} + +# Get remediation log +remediate_get_log() { + local lines="${1:-50}" + + if [ -f "$REMEDIATION_LOG" ]; then + tail -n "$lines" "$REMEDIATION_LOG" + else + echo "No remediation log available" + fi +} + +# Initialize on source +remediate_init diff --git a/package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/scoring.sh b/package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/scoring.sh new file mode 100755 index 00000000..07d9a7c0 --- /dev/null +++ b/package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/scoring.sh @@ -0,0 +1,274 @@ +#!/bin/sh +# Config Advisor - Risk Scoring Module + +. /lib/functions.sh + +SCORE_FILE="/var/lib/config-advisor/score.json" +HISTORY_FILE="/var/lib/config-advisor/score_history.json" + +# Get severity weights from config +_get_weight() { + local severity="$1" + uci -q get "config-advisor.scoring.weight_$severity" || \ + case "$severity" in + critical) echo "40" ;; + high) echo "25" ;; + medium) echo "20" ;; + low) echo "10" ;; + info) echo "5" ;; + *) echo "0" ;; + esac +} + +# Get passing score threshold +_get_passing_score() { + uci -q get config-advisor.scoring.passing_score || echo "70" +} + +# Calculate security score +scoring_calculate() { + local results_file="/var/lib/config-advisor/results.json" + + if [ ! -f "$results_file" ]; then + echo '{"error": "No check results available"}' + return 1 + fi + + local total_weight=0 + local earned_weight=0 + local critical_fails=0 + local high_fails=0 + local medium_fails=0 + local low_fails=0 + + # Read rules file for severity mapping + local rules_file="/usr/share/config-advisor/anssi-rules.json" + + # Process each result + while read -r result; do + [ -z "$result" ] && continue + + local check_id status + check_id=$(echo "$result" | jsonfilter -e '@.id' 2>/dev/null) + status=$(echo "$result" | jsonfilter -e '@.status' 2>/dev/null) + + # Get severity from rules + local severity="medium" + if [ -f "$rules_file" ]; then + severity=$(jsonfilter -i "$rules_file" -e '@.categories[*].rules[*]' 2>/dev/null | \ + grep "\"id\":\"$check_id\"" | \ + head -1 | \ + jsonfilter -e '@.severity' 2>/dev/null || echo "medium") + fi + + local weight + weight=$(_get_weight "$severity") + total_weight=$((total_weight + weight)) + + if [ "$status" = "pass" ]; then + earned_weight=$((earned_weight + weight)) + else + case "$severity" in + critical) critical_fails=$((critical_fails + 1)) ;; + high) high_fails=$((high_fails + 1)) ;; + medium) medium_fails=$((medium_fails + 1)) ;; + low) low_fails=$((low_fails + 1)) ;; + esac + fi + done < <(jsonfilter -i "$results_file" -e '@[*]' 2>/dev/null) + + # Calculate score (0-100) + local score=0 + if [ "$total_weight" -gt 0 ]; then + score=$(echo "scale=0; $earned_weight * 100 / $total_weight" | bc 2>/dev/null || echo "0") + fi + + # Determine grade + local grade + if [ "$score" -ge 90 ]; then + grade="A" + elif [ "$score" -ge 80 ]; then + grade="B" + elif [ "$score" -ge 70 ]; then + grade="C" + elif [ "$score" -ge 60 ]; then + grade="D" + else + grade="F" + fi + + # Determine risk level + local risk_level + if [ "$critical_fails" -gt 0 ]; then + risk_level="critical" + elif [ "$high_fails" -gt 0 ]; then + risk_level="high" + elif [ "$medium_fails" -gt 0 ]; then + risk_level="medium" + elif [ "$low_fails" -gt 0 ]; then + risk_level="low" + else + risk_level="minimal" + fi + + local timestamp + timestamp=$(date +%s) + + # Save score + cat > "$SCORE_FILE" < "$HISTORY_FILE" + return + fi + + local tmp_file="/tmp/score_history_$$.json" + if [ "$(cat "$HISTORY_FILE")" = "[]" ]; then + echo "[$entry]" > "$tmp_file" + else + sed 's/]$/,'"$entry"']/' "$HISTORY_FILE" > "$tmp_file" + fi + mv "$tmp_file" "$HISTORY_FILE" + + # Keep last 100 entries + local count + count=$(jsonfilter -i "$HISTORY_FILE" -e '@[*]' 2>/dev/null | wc -l) + if [ "$count" -gt 100 ]; then + jsonfilter -i "$HISTORY_FILE" -e '@[-100:]' > "$tmp_file" 2>/dev/null + mv "$tmp_file" "$HISTORY_FILE" + fi +} + +# Get current score +scoring_get_score() { + if [ -f "$SCORE_FILE" ]; then + cat "$SCORE_FILE" + else + echo '{"error": "No score calculated yet"}' + fi +} + +# Get score history +scoring_get_history() { + local count="${1:-30}" + + if [ -f "$HISTORY_FILE" ]; then + jsonfilter -i "$HISTORY_FILE" -e "@[-$count:]" 2>/dev/null || echo "[]" + else + echo "[]" + fi +} + +# Get score trend +scoring_get_trend() { + if [ ! -f "$HISTORY_FILE" ]; then + echo '{"trend": "unknown", "change": 0}' + return + fi + + local recent_scores + recent_scores=$(jsonfilter -i "$HISTORY_FILE" -e '@[-5:].score' 2>/dev/null | tr '\n' ' ') + + local scores_array=($recent_scores) + local count=${#scores_array[@]} + + if [ "$count" -lt 2 ]; then + echo '{"trend": "stable", "change": 0}' + return + fi + + local first_score=${scores_array[0]} + local last_score=${scores_array[$((count-1))]} + local change=$((last_score - first_score)) + + local trend + if [ "$change" -gt 5 ]; then + trend="improving" + elif [ "$change" -lt -5 ]; then + trend="declining" + else + trend="stable" + fi + + echo "{\"trend\": \"$trend\", \"change\": $change, \"samples\": $count}" +} + +# Get risk summary +scoring_risk_summary() { + if [ ! -f "$SCORE_FILE" ]; then + echo '{"error": "No score available"}' + return 1 + fi + + local score grade risk_level + score=$(jsonfilter -i "$SCORE_FILE" -e '@.score' 2>/dev/null) + grade=$(jsonfilter -i "$SCORE_FILE" -e '@.grade' 2>/dev/null) + risk_level=$(jsonfilter -i "$SCORE_FILE" -e '@.risk_level' 2>/dev/null) + + local critical high medium low + critical=$(jsonfilter -i "$SCORE_FILE" -e '@.breakdown.critical_failures' 2>/dev/null) + high=$(jsonfilter -i "$SCORE_FILE" -e '@.breakdown.high_failures' 2>/dev/null) + medium=$(jsonfilter -i "$SCORE_FILE" -e '@.breakdown.medium_failures' 2>/dev/null) + low=$(jsonfilter -i "$SCORE_FILE" -e '@.breakdown.low_failures' 2>/dev/null) + + local trend_info + trend_info=$(scoring_get_trend) + + cat </dev/null || echo "0") + + [ "$score" -ge "$threshold" ] +} diff --git a/package/secubox/secubox-config-advisor/files/usr/sbin/config-advisorctl b/package/secubox/secubox-config-advisor/files/usr/sbin/config-advisorctl new file mode 100755 index 00000000..6ee68279 --- /dev/null +++ b/package/secubox/secubox-config-advisor/files/usr/sbin/config-advisorctl @@ -0,0 +1,272 @@ +#!/bin/sh +# Config Advisor CLI - Security configuration analysis and hardening +# Usage: config-advisorctl [options] + +VERSION="0.1.0" + +# Load libraries +[ -f /usr/lib/config-advisor/checks.sh ] && . /usr/lib/config-advisor/checks.sh +[ -f /usr/lib/config-advisor/anssi.sh ] && . /usr/lib/config-advisor/anssi.sh +[ -f /usr/lib/config-advisor/scoring.sh ] && . /usr/lib/config-advisor/scoring.sh +[ -f /usr/lib/config-advisor/remediate.sh ] && . /usr/lib/config-advisor/remediate.sh + +DAEMON_INTERVAL=3600 + +usage() { + cat < [options] + +Check Commands: + check Run all security checks + check-category Run checks for specific category + results Show check results + +Compliance Commands: + compliance Run ANSSI CSPN compliance check + compliance-status Show compliance status + compliance-report [fmt] Generate report (text/json/markdown) + is-compliant Check if system passes compliance + +Scoring Commands: + score Calculate security score + score-history [n] Show score history (last n entries) + score-trend Show score trend + risk-summary Show risk summary + +Remediation Commands: + remediate Apply remediation for check + remediate-dry Preview remediation (dry run) + remediate-safe Apply all safe remediations + remediate-pending Show pending remediations + suggest Get remediation suggestion (AI) + +Daemon Commands: + daemon Run as daemon (foreground) + status Show advisor status + +Categories: + network, firewall, authentication, encryption, services, logging, updates + +General: + help Show this help + version Show version + +Examples: + config-advisorctl check + config-advisorctl compliance + config-advisorctl remediate FW-002 + config-advisorctl compliance-report markdown > report.md + +EOF +} + +# Get status +cmd_status() { + local enabled framework + enabled=$(uci -q get config-advisor.main.enabled || echo "0") + framework=$(uci -q get config-advisor.compliance.framework || echo "anssi_cspn") + + local last_check=0 + local results_file="/var/lib/config-advisor/results.json" + if [ -f "$results_file" ]; then + last_check=$(stat -c %Y "$results_file" 2>/dev/null || echo "0") + fi + + local score_data="{}" + if [ -f /var/lib/config-advisor/score.json ]; then + score_data=$(cat /var/lib/config-advisor/score.json) + fi + + local compliance_data="{}" + if [ -f /var/lib/config-advisor/compliance.json ]; then + compliance_data=$(cat /var/lib/config-advisor/compliance.json) + fi + + cat </dev/null || echo "null"), + "grade": "$(jsonfilter -i /var/lib/config-advisor/score.json -e '@.grade' 2>/dev/null || echo "?")", + "risk_level": "$(jsonfilter -i /var/lib/config-advisor/score.json -e '@.risk_level' 2>/dev/null || echo "unknown")", + "compliance_rate": $(jsonfilter -i /var/lib/config-advisor/compliance.json -e '@.compliance_rate' 2>/dev/null || echo "null") +} +EOF +} + +# Full check and score +cmd_full_check() { + echo "Running security checks..." + run_all_checks >/dev/null + + echo "Running compliance check..." + anssi_run_compliance >/dev/null + + echo "Calculating score..." + scoring_calculate +} + +# Daemon loop +cmd_daemon() { + local check_interval + check_interval=$(uci -q get config-advisor.main.check_interval || echo "3600") + + logger -t config-advisor "Daemon starting (interval: ${check_interval}s)" + + while true; do + cmd_full_check >/dev/null 2>&1 + + # Check for auto-remediate + local auto_remediate + auto_remediate=$(uci -q get config-advisor.main.auto_remediate || echo "0") + + if [ "$auto_remediate" = "1" ]; then + remediate_apply_safe 0 >/dev/null 2>&1 + fi + + # Send notification if enabled and score is failing + local notification_enabled + notification_enabled=$(uci -q get config-advisor.main.notification_enabled || echo "0") + + if [ "$notification_enabled" = "1" ] && ! scoring_is_passing; then + local score + score=$(jsonfilter -i /var/lib/config-advisor/score.json -e '@.score' 2>/dev/null || echo "0") + logger -t config-advisor "WARNING: Security score is $score (below threshold)" + fi + + sleep "$check_interval" + done +} + +# Main command dispatcher +case "$1" in + # Checks + check) + cmd_full_check + ;; + check-category) + [ -z "$2" ] && { echo "Usage: config-advisorctl check-category "; exit 1; } + checks_init + case "$2" in + network) + check_ipv6_disabled + check_mgmt_restricted + check_syn_flood_protection + ;; + firewall) + check_default_deny + check_drop_invalid + check_wan_ports_closed + ;; + authentication) + check_root_password_set + check_ssh_key_auth + check_ssh_no_root_password + ;; + encryption) + check_https_enabled + check_wireguard_configured + check_dns_encrypted + ;; + services) + check_crowdsec_enabled + check_services_localhost + ;; + logging) + check_syslog_enabled + check_log_rotation + ;; + *) + echo "Unknown category: $2" + exit 1 + ;; + esac + get_results + ;; + results) + get_results + ;; + + # Compliance + compliance) + anssi_run_compliance + ;; + compliance-status) + anssi_get_status + ;; + compliance-report) + anssi_generate_report "${2:-text}" + ;; + is-compliant) + if anssi_is_compliant; then + echo "COMPLIANT" + exit 0 + else + echo "NOT COMPLIANT" + exit 1 + fi + ;; + + # Scoring + score) + scoring_calculate + ;; + score-history) + scoring_get_history "${2:-30}" + ;; + score-trend) + scoring_get_trend + ;; + risk-summary) + scoring_risk_summary + ;; + + # Remediation + remediate) + [ -z "$2" ] && { echo "Usage: config-advisorctl remediate "; exit 1; } + remediate_apply "$2" 0 + ;; + remediate-dry) + [ -z "$2" ] && { echo "Usage: config-advisorctl remediate-dry "; exit 1; } + remediate_apply "$2" 1 + ;; + remediate-safe) + remediate_apply_safe 0 + ;; + remediate-pending) + remediate_get_pending + ;; + suggest) + [ -z "$2" ] && { echo "Usage: config-advisorctl suggest "; exit 1; } + remediate_suggest "$2" + ;; + + # Daemon + daemon) + cmd_daemon + ;; + status) + cmd_status + ;; + + # General + version) + echo "Config Advisor CLI v$VERSION" + ;; + help|--help|-h|"") + usage + ;; + *) + echo "Unknown command: $1" + echo "Run 'config-advisorctl help' for usage" + exit 1 + ;; +esac diff --git a/package/secubox/secubox-config-advisor/files/usr/share/config-advisor/anssi-rules.json b/package/secubox/secubox-config-advisor/files/usr/share/config-advisor/anssi-rules.json new file mode 100644 index 00000000..2ad69f61 --- /dev/null +++ b/package/secubox/secubox-config-advisor/files/usr/share/config-advisor/anssi-rules.json @@ -0,0 +1,257 @@ +{ + "framework": "ANSSI CSPN", + "version": "1.0", + "categories": { + "network": { + "name": "Network Security", + "rules": [ + { + "id": "NET-001", + "name": "Disable IPv6 if not required", + "severity": "medium", + "check": "ipv6_disabled", + "description": "IPv6 should be disabled if not actively used to reduce attack surface", + "remediation": "Set network.globals.ula_prefix to empty and disable IPv6 on interfaces" + }, + { + "id": "NET-002", + "name": "Restrict management access", + "severity": "high", + "check": "mgmt_restricted", + "description": "SSH and LuCI should only be accessible from trusted networks", + "remediation": "Configure firewall rules to restrict SSH (22) and HTTPS (443) to LAN only" + }, + { + "id": "NET-003", + "name": "Disable unused interfaces", + "severity": "low", + "check": "unused_interfaces", + "description": "Unused network interfaces should be disabled", + "remediation": "Disable or remove unused interface configurations" + }, + { + "id": "NET-004", + "name": "Enable SYN flood protection", + "severity": "high", + "check": "syn_flood_protection", + "description": "SYN flood protection should be enabled on WAN interface", + "remediation": "Enable synflood_protect in firewall config" + } + ] + }, + "firewall": { + "name": "Firewall Configuration", + "rules": [ + { + "id": "FW-001", + "name": "Default deny policy", + "severity": "critical", + "check": "default_deny", + "description": "Firewall should have default deny policy for WAN zone", + "remediation": "Set input=REJECT, output=ACCEPT, forward=REJECT for WAN zone" + }, + { + "id": "FW-002", + "name": "Drop invalid packets", + "severity": "high", + "check": "drop_invalid", + "description": "Invalid packets should be dropped", + "remediation": "Enable drop_invalid in firewall defaults" + }, + { + "id": "FW-003", + "name": "No open ports on WAN", + "severity": "high", + "check": "wan_ports_closed", + "description": "No unnecessary ports should be open on WAN interface", + "remediation": "Review and remove unnecessary port forwards and WAN input rules" + }, + { + "id": "FW-004", + "name": "Enable connection tracking", + "severity": "medium", + "check": "conntrack_enabled", + "description": "Connection tracking should be properly configured", + "remediation": "Ensure flow_offloading is configured appropriately" + } + ] + }, + "authentication": { + "name": "Authentication & Access Control", + "rules": [ + { + "id": "AUTH-001", + "name": "Strong root password", + "severity": "critical", + "check": "root_password_set", + "description": "Root password must be set and strong", + "remediation": "Set a strong root password using passwd command" + }, + { + "id": "AUTH-002", + "name": "SSH key authentication", + "severity": "high", + "check": "ssh_key_auth", + "description": "SSH should prefer key-based authentication over password", + "remediation": "Add SSH public keys and consider disabling password auth" + }, + { + "id": "AUTH-003", + "name": "Disable SSH root password login", + "severity": "high", + "check": "ssh_no_root_password", + "description": "SSH root login with password should be disabled", + "remediation": "Set PasswordAuth to off in dropbear config" + }, + { + "id": "AUTH-004", + "name": "Session timeout configured", + "severity": "medium", + "check": "session_timeout", + "description": "LuCI session timeout should be configured", + "remediation": "Set appropriate session timeout in uhttpd/rpcd config" + } + ] + }, + "encryption": { + "name": "Encryption & Cryptography", + "rules": [ + { + "id": "CRYPT-001", + "name": "HTTPS enabled for LuCI", + "severity": "critical", + "check": "https_enabled", + "description": "LuCI must be accessed over HTTPS only", + "remediation": "Enable HTTPS in uhttpd and redirect HTTP to HTTPS" + }, + { + "id": "CRYPT-002", + "name": "Strong TLS configuration", + "severity": "high", + "check": "tls_strong", + "description": "TLS should use strong ciphers and protocols (TLS 1.2+)", + "remediation": "Configure uhttpd/nginx with modern TLS settings" + }, + { + "id": "CRYPT-003", + "name": "WireGuard encryption", + "severity": "medium", + "check": "wireguard_configured", + "description": "VPN tunnels should use WireGuard for mesh connectivity", + "remediation": "Configure WireGuard for secure mesh communication" + }, + { + "id": "CRYPT-004", + "name": "DNS over TLS/HTTPS", + "severity": "medium", + "check": "dns_encrypted", + "description": "DNS queries should be encrypted (DoT/DoH)", + "remediation": "Configure AdGuard Home or stubby for encrypted DNS" + } + ] + }, + "services": { + "name": "Service Hardening", + "rules": [ + { + "id": "SVC-001", + "name": "Disable unnecessary services", + "severity": "medium", + "check": "minimal_services", + "description": "Only required services should be running", + "remediation": "Disable unused services in /etc/init.d/" + }, + { + "id": "SVC-002", + "name": "Services bound to localhost", + "severity": "high", + "check": "services_localhost", + "description": "Internal services should bind to localhost only", + "remediation": "Configure services to listen on 127.0.0.1 instead of 0.0.0.0" + }, + { + "id": "SVC-003", + "name": "CrowdSec protection enabled", + "severity": "high", + "check": "crowdsec_enabled", + "description": "CrowdSec should be running for threat protection", + "remediation": "Install and enable CrowdSec with firewall bouncer" + }, + { + "id": "SVC-004", + "name": "Automatic security updates", + "severity": "medium", + "check": "auto_updates", + "description": "Security updates should be applied automatically or regularly", + "remediation": "Configure opkg-upgrade or scheduled update checks" + } + ] + }, + "logging": { + "name": "Logging & Monitoring", + "rules": [ + { + "id": "LOG-001", + "name": "System logging enabled", + "severity": "high", + "check": "syslog_enabled", + "description": "System logging must be enabled and configured", + "remediation": "Ensure logd/syslog-ng is running and configured" + }, + { + "id": "LOG-002", + "name": "Log rotation configured", + "severity": "medium", + "check": "log_rotation", + "description": "Logs should be rotated to prevent disk exhaustion", + "remediation": "Configure log rotation size and count in system config" + }, + { + "id": "LOG-003", + "name": "Auth logging enabled", + "severity": "high", + "check": "auth_logging", + "description": "Authentication events should be logged", + "remediation": "Enable auth logging in dropbear and uhttpd" + }, + { + "id": "LOG-004", + "name": "Remote logging configured", + "severity": "low", + "check": "remote_logging", + "description": "Logs should be sent to remote syslog for persistence", + "remediation": "Configure remote syslog server in system config" + } + ] + }, + "updates": { + "name": "Updates & Patches", + "rules": [ + { + "id": "UPD-001", + "name": "System up to date", + "severity": "high", + "check": "system_uptodate", + "description": "System packages should be up to date", + "remediation": "Run opkg update && opkg upgrade" + }, + { + "id": "UPD-002", + "name": "No vulnerable packages", + "severity": "critical", + "check": "no_cve_packages", + "description": "No packages with known CVEs should be installed", + "remediation": "Update or remove packages with known vulnerabilities" + }, + { + "id": "UPD-003", + "name": "Firmware version current", + "severity": "medium", + "check": "firmware_current", + "description": "OpenWrt firmware should be reasonably current", + "remediation": "Consider sysupgrade to latest stable release" + } + ] + } + } +}