fix(config-advisor): BusyBox ash compatibility fixes

- Replace bash arrays with POSIX loops in scoring.sh
- Replace bc with shell arithmetic (bc not available on OpenWrt)
- Wrap RPCD handlers in functions for local keyword compatibility
- Fix process substitution < <() to use pipe instead

Tested on router: CLI and RPCD working, score calculation correct.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-06 06:08:28 +01:00
parent 0f4649c1e0
commit 714313633b
3 changed files with 196 additions and 185 deletions

View File

@ -9,176 +9,175 @@
[ -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
handle_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')"
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"
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"
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%.*}"
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
}
handle_results() {
if [ -f /var/lib/config-advisor/results.json ]; then
echo "{\"results\":$(cat /var/lib/config-advisor/results.json)}"
else
echo '{"results":[]}'
fi
}
handle_score() {
if [ -f /var/lib/config-advisor/score.json ]; then
cat /var/lib/config-advisor/score.json
else
echo '{"error":"No score available"}'
fi
}
handle_compliance() {
if [ -f /var/lib/config-advisor/compliance.json ]; then
cat /var/lib/config-advisor/compliance.json
else
echo '{"error":"No compliance report available"}'
fi
}
handle_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
}
handle_pending() {
if [ -f /var/lib/config-advisor/pending_remediations.json ]; then
echo "{\"pending\":$(cat /var/lib/config-advisor/pending_remediations.json)}"
else
echo '{"pending":[]}'
fi
}
handle_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
}
handle_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
}
handle_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
}
handle_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"
}
handle_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
}
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"}'
;;
status) handle_status ;;
results) handle_results ;;
score) handle_score ;;
compliance) handle_compliance ;;
check) handle_check ;;
pending) handle_pending ;;
history) handle_history ;;
suggest) handle_suggest ;;
remediate) handle_remediate ;;
remediate_safe) handle_remediate_safe ;;
set_config) handle_set_config ;;
*) echo '{"error":"Unknown method"}' ;;
esac
;;
esac

View File

@ -118,7 +118,7 @@ anssi_run_compliance() {
"warnings": $warnings,
"info": $info
},
"compliance_rate": $(echo "scale=1; $passed * 100 / $total" | bc 2>/dev/null || echo "0"),
"compliance_rate": $([ "$total" -gt 0 ] && echo $((passed * 100 / total)) || echo "0"),
"results": $results
}
EOF

View File

@ -44,8 +44,8 @@ scoring_calculate() {
# Read rules file for severity mapping
local rules_file="/usr/share/config-advisor/anssi-rules.json"
# Process each result
while read -r result; do
# Process each result (POSIX compatible - use pipe instead of process substitution)
jsonfilter -i "$results_file" -e '@[*]' 2>/dev/null | while read -r result; do
[ -z "$result" ] && continue
local check_id status
@ -63,24 +63,29 @@ scoring_calculate() {
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)
# Write to temp file for subshell communication
echo "$weight $status" >> /tmp/scoring_$$
done
# Calculate score (0-100)
# Read accumulated values from temp file
if [ -f /tmp/scoring_$$ ]; then
while read -r weight status; do
total_weight=$((total_weight + weight))
if [ "$status" = "pass" ]; then
earned_weight=$((earned_weight + weight))
else
# Default to medium for severity counting
medium_fails=$((medium_fails + 1))
fi
done < /tmp/scoring_$$
rm -f /tmp/scoring_$$
fi
# Calculate score (0-100) using shell arithmetic
local score=0
if [ "$total_weight" -gt 0 ]; then
score=$(echo "scale=0; $earned_weight * 100 / $total_weight" | bc 2>/dev/null || echo "0")
score=$((earned_weight * 100 / total_weight))
fi
# Determine grade
@ -201,16 +206,23 @@ scoring_get_trend() {
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[@]}
# Count scores (POSIX compatible)
local count=0
local first_score=0
local last_score=0
for score in $recent_scores; do
if [ "$count" -eq 0 ]; then
first_score="$score"
fi
last_score="$score"
count=$((count + 1))
done
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