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:
CyberMind-FR 2026-02-06 05:56:17 +01:00
parent f2dfb5c144
commit 0f4649c1e0
19 changed files with 2975 additions and 1 deletions

View File

@ -594,3 +594,48 @@ _Last updated: 2026-02-07_
- **UCI configuration**: sources enable/disable, signing, validation settings, application method, auto-apply
- **Daemon**: Configurable collect_interval (default 300s), auto_collect, auto_share, auto_apply
- Part of v0.19 MirrorNetworking roadmap (Couche 3).
42. **Config Advisor - ANSSI CSPN Compliance (2026-02-07)**
- Created `secubox-config-advisor` — security configuration analysis and hardening tool.
- **ANSSI CSPN compliance framework**:
- 7 check categories: network, firewall, authentication, encryption, services, logging, updates
- 25+ security check rules with severity levels (critical, high, medium, low, info)
- JSON rules database in `/usr/share/config-advisor/anssi-rules.json`
- **Security check modules** (`checks.sh`):
- Network: IPv6, management access restriction, SYN flood protection
- Firewall: default deny policy, drop invalid packets, WAN port exposure
- Authentication: root password, SSH key auth, SSH password auth
- Encryption: HTTPS enabled, WireGuard configured, DNS encryption
- Services: CrowdSec running, services bound to localhost
- Logging: syslog enabled, log rotation configured
- **Risk scoring module** (`scoring.sh`):
- 0-100 score with severity weights (critical=40, high=25, medium=20, low=10, info=5)
- Grade calculation (A-F) based on thresholds (90/80/70/60)
- Risk level classification: critical, high, medium, low, minimal
- Score history tracking and trend analysis
- **ANSSI compliance module** (`anssi.sh`):
- Compliance rate calculation (percentage of passing rules)
- Report generation in text, JSON, and Markdown formats
- Category filtering and strict mode
- **Remediation module** (`remediate.sh`):
- Auto-remediation for 7 checks: NET-002, NET-004, FW-001, FW-002, AUTH-003, CRYPT-001, LOG-002
- Safe vs manual remediation separation
- Dry-run mode for preview
- LocalAI integration for AI-powered suggestions
- Pending approvals queue
- **CLI** (`config-advisorctl`):
- Check commands: `check`, `check-category`, `results`
- Compliance commands: `compliance`, `compliance-status`, `compliance-report`, `is-compliant`
- Scoring commands: `score`, `score-history`, `score-trend`, `risk-summary`
- Remediation commands: `remediate`, `remediate-dry`, `remediate-safe`, `remediate-pending`, `suggest`
- Daemon mode with configurable check interval
- Created `luci-app-config-advisor` — LuCI dashboard.
- Dashboard: score circle, grade, risk level, compliance rate, last check time
- Check results table with status icons
- Score history table
- Compliance view: summary cards, progress bar, results by category
- Remediation view: quick actions, failed checks with apply buttons, pending approvals
- Settings: framework selection, scoring weights, category toggles, LocalAI config
- **RPCD methods**: status, results, score, compliance, check, pending, history, suggest, remediate, remediate_safe, set_config
- **UCI configuration**: main (enabled, check_interval, auto_remediate), compliance (framework, strict_mode), scoring (passing_score, weights), categories (enable/disable), localai (url, model)
- Part of v1.0.0 certification roadmap (ANSSI CSPN compliance tooling).

View File

@ -283,9 +283,35 @@ Required components:
| LocalAI 3.9 | DONE |
| LocalAI Emancipation | DONE (Tor + DNS + mDNS) |
### v1.0.0 Progress
| Item | Status |
|------|--------|
| Config Advisor | DONE |
| ANSSI CSPN Compliance | DONE |
| Remediation Engine | DONE |
| LuCI Dashboard | DONE |
### Just Completed (2026-02-07)
- **Config Advisor Package** — DONE
- Created `secubox-config-advisor` - ANSSI CSPN compliance checking daemon
- 7 check categories, 25+ security rules
- Risk scoring (0-100) with grade (A-F) and risk level
- Auto-remediation for 7 checks with dry-run mode
- LocalAI integration for AI-powered suggestions
- `config-advisorctl` CLI with 20+ commands
- **Config Advisor Dashboard** — DONE
- Created `luci-app-config-advisor` - LuCI dashboard
- Score display with grade circle and risk level
- Compliance view by category with pass/fail/warn badges
- Remediation view with apply/preview buttons
- Settings for framework, weights, categories, LocalAI
### Certifications
- ANSSI CSPN: Data Classifier + Mistral EU + offline mode
- ANSSI CSPN: Config Advisor compliance tool DONE
- GDPR: Currently compliant
- ISO 27001, NIS2, SOC2: Planned for v1.1+

View 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)))

View File

@ -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
});

View File

@ -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">&#x2714;</span>';
case 'fail': return '<span style="color:#ef4444">&#x2718;</span>';
case 'warn': return '<span style="color:#eab308">&#x26A0;</span>';
case 'info': return '<span style="color:#3b82f6">&#x2139;</span>';
default: return '<span style="color:#6b7280">&#x2212;</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' ? '&#x26A0;' :
status.risk_level === 'high' ? '&#x26A0;' :
status.risk_level === 'minimal' ? '&#x2714;' : '&#x2139;'),
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'
}, '&#x2611;'),
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'
}, '&#x1F550;'),
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
});

View File

@ -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;' }, '&#x2714;'),
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
});

View File

@ -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();
}
});

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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"]
}
}
}

View 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))

View File

@ -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'

View File

@ -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"
}

View 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
}

View 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
}

View 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

View 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" ]
}

View 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

View File

@ -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"
}
]
}
}
}