feat(config-advisor): Add ANSSI CSPN compliance checking packages
secubox-config-advisor: - 7 check categories (network, firewall, auth, encryption, services, logging, updates) - 25+ security rules with severity-weighted scoring (0-100, grade A-F) - Auto-remediation for 7 checks with dry-run mode - LocalAI integration for AI-powered suggestions - config-advisorctl CLI with 20+ commands luci-app-config-advisor: - Dashboard with score circle, grade, risk level, compliance rate - Compliance view by category with pass/fail/warn badges - Remediation view with apply/preview buttons - Settings for framework, weights, categories, LocalAI Part of v1.0.0 ANSSI CSPN certification roadmap. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f2dfb5c144
commit
0f4649c1e0
@ -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).
|
||||
|
||||
@ -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+
|
||||
|
||||
|
||||
31
package/secubox/luci-app-config-advisor/Makefile
Normal file
31
package/secubox/luci-app-config-advisor/Makefile
Normal file
@ -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 <info@secubox.io>
|
||||
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)))
|
||||
@ -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
|
||||
});
|
||||
@ -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 '<span style="color:#22c55e">✔</span>';
|
||||
case 'fail': return '<span style="color:#ef4444">✘</span>';
|
||||
case 'warn': return '<span style="color:#eab308">⚠</span>';
|
||||
case 'info': return '<span style="color:#3b82f6">ℹ</span>';
|
||||
default: return '<span style="color:#6b7280">−</span>';
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
@ -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
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
60
package/secubox/secubox-config-advisor/Makefile
Normal file
60
package/secubox/secubox-config-advisor/Makefile
Normal file
@ -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 <dev@secubox.io>
|
||||
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))
|
||||
@ -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'
|
||||
@ -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"
|
||||
}
|
||||
272
package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/anssi.sh
Executable file
272
package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/anssi.sh
Executable file
@ -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" <<EOF
|
||||
{
|
||||
"framework": "ANSSI CSPN",
|
||||
"timestamp": $timestamp,
|
||||
"summary": {
|
||||
"total": $total,
|
||||
"passed": $passed,
|
||||
"failed": $failed,
|
||||
"warnings": $warnings,
|
||||
"info": $info
|
||||
},
|
||||
"compliance_rate": $(echo "scale=1; $passed * 100 / $total" | bc 2>/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 <<EOF
|
||||
ANSSI CSPN Compliance Report
|
||||
============================
|
||||
Generated: $(date)
|
||||
|
||||
Summary:
|
||||
Total checks: $total
|
||||
Passed: $passed
|
||||
Failed: $failed
|
||||
Warnings: $warnings
|
||||
Compliance: ${compliance_rate}%
|
||||
|
||||
EOF
|
||||
|
||||
if [ "$failed" -gt 0 ]; then
|
||||
echo "Failed Checks:"
|
||||
echo "--------------"
|
||||
anssi_get_failures | 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
|
||||
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 <<EOF
|
||||
# ANSSI CSPN Compliance Report
|
||||
|
||||
**Generated:** $(date)
|
||||
|
||||
## Summary
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Total checks | $total |
|
||||
| Passed | $passed |
|
||||
| Failed | $failed |
|
||||
| Warnings | $warnings |
|
||||
| **Compliance Rate** | **${compliance_rate}%** |
|
||||
|
||||
EOF
|
||||
|
||||
if [ "$failed" -gt 0 ]; then
|
||||
echo "## Failed Checks"
|
||||
echo ""
|
||||
anssi_get_failures | while read -r result; do
|
||||
local rule_id category severity
|
||||
rule_id=$(echo "$result" | jsonfilter -e '@.rule_id' 2>/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
|
||||
}
|
||||
319
package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/checks.sh
Executable file
319
package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/checks.sh
Executable file
@ -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
|
||||
}
|
||||
294
package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/remediate.sh
Executable file
294
package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/remediate.sh
Executable file
@ -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
|
||||
274
package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/scoring.sh
Executable file
274
package/secubox/secubox-config-advisor/files/usr/lib/config-advisor/scoring.sh
Executable file
@ -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" <<EOF
|
||||
{
|
||||
"timestamp": $timestamp,
|
||||
"score": $score,
|
||||
"grade": "$grade",
|
||||
"risk_level": "$risk_level",
|
||||
"passing_threshold": $(_get_passing_score),
|
||||
"is_passing": $([ "$score" -ge "$(_get_passing_score)" ] && echo "true" || echo "false"),
|
||||
"breakdown": {
|
||||
"total_weight": $total_weight,
|
||||
"earned_weight": $earned_weight,
|
||||
"critical_failures": $critical_fails,
|
||||
"high_failures": $high_fails,
|
||||
"medium_failures": $medium_fails,
|
||||
"low_failures": $low_fails
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Record history
|
||||
_record_history "$timestamp" "$score" "$grade" "$risk_level"
|
||||
|
||||
cat "$SCORE_FILE"
|
||||
}
|
||||
|
||||
# Record score history
|
||||
_record_history() {
|
||||
local timestamp="$1"
|
||||
local score="$2"
|
||||
local grade="$3"
|
||||
local risk_level="$4"
|
||||
|
||||
local entry="{\"timestamp\":$timestamp,\"score\":$score,\"grade\":\"$grade\",\"risk_level\":\"$risk_level\"}"
|
||||
|
||||
if [ ! -f "$HISTORY_FILE" ]; then
|
||||
echo "[$entry]" > "$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 <<EOF
|
||||
{
|
||||
"score": $score,
|
||||
"grade": "$grade",
|
||||
"risk_level": "$risk_level",
|
||||
"failures": {
|
||||
"critical": $critical,
|
||||
"high": $high,
|
||||
"medium": $medium,
|
||||
"low": $low
|
||||
},
|
||||
"trend": $(echo "$trend_info")
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Check if score meets threshold
|
||||
scoring_is_passing() {
|
||||
local threshold
|
||||
threshold=$(_get_passing_score)
|
||||
|
||||
local score
|
||||
score=$(jsonfilter -i "$SCORE_FILE" -e '@.score' 2>/dev/null || echo "0")
|
||||
|
||||
[ "$score" -ge "$threshold" ]
|
||||
}
|
||||
272
package/secubox/secubox-config-advisor/files/usr/sbin/config-advisorctl
Executable file
272
package/secubox/secubox-config-advisor/files/usr/sbin/config-advisorctl
Executable file
@ -0,0 +1,272 @@
|
||||
#!/bin/sh
|
||||
# Config Advisor CLI - Security configuration analysis and hardening
|
||||
# Usage: config-advisorctl <command> [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 <<EOF
|
||||
Config Advisor CLI v$VERSION - Security Configuration Analysis
|
||||
|
||||
Usage: config-advisorctl <command> [options]
|
||||
|
||||
Check Commands:
|
||||
check Run all security checks
|
||||
check-category <cat> 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 <check_id> Apply remediation for check
|
||||
remediate-dry <check_id> Preview remediation (dry run)
|
||||
remediate-safe Apply all safe remediations
|
||||
remediate-pending Show pending remediations
|
||||
suggest <check_id> 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 <<EOF
|
||||
{
|
||||
"version": "$VERSION",
|
||||
"enabled": $enabled,
|
||||
"framework": "$framework",
|
||||
"last_check": $last_check,
|
||||
"localai": {
|
||||
"enabled": $(uci -q get config-advisor.localai.enabled || echo "0"),
|
||||
"url": "$(uci -q get config-advisor.localai.url || echo "http://127.0.0.1:8091")"
|
||||
},
|
||||
"score": $(jsonfilter -i /var/lib/config-advisor/score.json -e '@.score' 2>/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 <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 <check_id>"; exit 1; }
|
||||
remediate_apply "$2" 0
|
||||
;;
|
||||
remediate-dry)
|
||||
[ -z "$2" ] && { echo "Usage: config-advisorctl remediate-dry <check_id>"; 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 <check_id>"; 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
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user