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
|
- **UCI configuration**: sources enable/disable, signing, validation settings, application method, auto-apply
|
||||||
- **Daemon**: Configurable collect_interval (default 300s), auto_collect, auto_share, auto_apply
|
- **Daemon**: Configurable collect_interval (default 300s), auto_collect, auto_share, auto_apply
|
||||||
- Part of v0.19 MirrorNetworking roadmap (Couche 3).
|
- 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 3.9 | DONE |
|
||||||
| LocalAI Emancipation | DONE (Tor + DNS + mDNS) |
|
| 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
|
### Certifications
|
||||||
|
|
||||||
- ANSSI CSPN: Data Classifier + Mistral EU + offline mode
|
- ANSSI CSPN: Config Advisor compliance tool DONE
|
||||||
- GDPR: Currently compliant
|
- GDPR: Currently compliant
|
||||||
- ISO 27001, NIS2, SOC2: Planned for v1.1+
|
- 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