#!/bin/sh # SecuBox Report Generator # Generates and distributes status reports via HTML and email # Copyright (C) 2026 CyberMind.fr VERSION="1.0.0" SCRIPT_DIR="/usr/share/secubox-reporter" LIB_DIR="$SCRIPT_DIR/lib" TPL_DIR="$SCRIPT_DIR/templates" OUTPUT_DIR="/www/reports" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' # Load libraries . /lib/functions.sh [ -f "$LIB_DIR/collectors.sh" ] && . "$LIB_DIR/collectors.sh" [ -f "$LIB_DIR/formatters.sh" ] && . "$LIB_DIR/formatters.sh" [ -f "$LIB_DIR/mailer.sh" ] && . "$LIB_DIR/mailer.sh" [ -f "$LIB_DIR/system-collector.sh" ] && . "$LIB_DIR/system-collector.sh" # Load config config_load secubox-reporter config_get OUTPUT_DIR global output_dir "/www/reports" config_get THEME global theme "dark" config_get REPO_PATH sources repo_path "/root/secubox-openwrt" log_info() { echo -e "${CYAN}[INFO]${NC} $1"; } log_ok() { echo -e "${GREEN}[OK]${NC} $1"; } log_err() { echo -e "${RED}[ERROR]${NC} $1" >&2; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } usage() { cat << EOF SecuBox Report Generator v$VERSION Usage: secubox-reportctl [options] COMMANDS: generate Generate report (dev|services|all) send Generate and email report schedule Set cron schedule (daily|weekly|off) status Show generator status preview Generate and output to stdout list List generated reports clean [days] Remove reports older than N days (default: 30) help Show this help REPORT TYPES: dev Development Status Report (progress, roadmap, health) services Distribution/Services Status Report (exposures, channels) system System Hardware Report (CPU, memory, power, carbon impact) meta Meta Dashboard (combined overview) all All reports OPTIONS: --email Also send via email --theme Theme: dark, light, cyberpunk (default: dark) --output Custom output path --json Output status as JSON EXAMPLES: secubox-reportctl generate dev secubox-reportctl send services secubox-reportctl schedule dev daily secubox-reportctl generate all --theme cyberpunk EOF } # Get hostname get_hostname() { uci -q get system.@system[0].hostname 2>/dev/null || hostname } # Get SecuBox version get_version() { cat /etc/secubox_version 2>/dev/null || echo "0.19.x" } # Generate development status report generate_dev_report() { local output_file="$1" local theme="${2:-dark}" log_info "Generating Development Status Report..." mkdir -p "$(dirname "$output_file")" local hostname=$(get_hostname) local version=$(get_version) local timestamp=$(date '+%Y-%m-%d %H:%M:%S') local health_score=0 # Get health score from RPCD if available if command -v ubus >/dev/null 2>&1; then health_score=$(ubus call luci.secubox-core get_full_health_report 2>/dev/null | \ jsonfilter -e '@.health_score' 2>/dev/null || echo "0") fi [ -z "$health_score" ] && health_score=0 # Collect data from .claude files local history_entries="" local wip_entries="" local roadmap_data="" if [ -d "$REPO_PATH/.claude" ]; then # Recent history (last 10 dated sections) history_entries=$(collect_history "$REPO_PATH/.claude/HISTORY.md") # WIP items wip_entries=$(collect_wip "$REPO_PATH/.claude/WIP.md") # Roadmap progress roadmap_data=$(collect_roadmap "$REPO_PATH/.claude/ROADMAP.md") else history_entries="

Repository not found at $REPO_PATH

" wip_entries="

Repository not found

" roadmap_data="

Repository not found

" fi # Generate HTML from template if [ -f "$TPL_DIR/dev-status.html.tpl" ]; then sed -e "s|{{HOSTNAME}}|$hostname|g" \ -e "s|{{VERSION}}|$version|g" \ -e "s|{{TIMESTAMP}}|$timestamp|g" \ -e "s|{{HEALTH_SCORE}}|$health_score|g" \ -e "s|{{THEME}}|$theme|g" \ -e "s|{{HISTORY_ENTRIES}}|$history_entries|g" \ -e "s|{{WIP_ENTRIES}}|$wip_entries|g" \ -e "s|{{ROADMAP_DATA}}|$roadmap_data|g" \ "$TPL_DIR/dev-status.html.tpl" > "$output_file" else # Fallback inline template generate_dev_html_inline "$output_file" "$hostname" "$version" "$timestamp" \ "$health_score" "$history_entries" "$wip_entries" "$roadmap_data" fi # Ensure web-readable permissions chmod 644 "$output_file" log_ok "Generated: $output_file" echo "$output_file" } # Generate services status report generate_services_report() { local output_file="$1" local theme="${2:-dark}" log_info "Generating Services Status Report..." mkdir -p "$(dirname "$output_file")" local hostname=$(get_hostname) local timestamp=$(date '+%Y-%m-%d %H:%M:%S') # Count services by channel local tor_count=0 local dns_count=0 local mesh_count=0 # Tor hidden services if [ -d /var/lib/tor/hidden_services ]; then tor_count=$(ls -1 /var/lib/tor/hidden_services 2>/dev/null | wc -l) fi # HAProxy vhosts dns_count=$(uci show haproxy 2>/dev/null | grep '=vhost$' | wc -l) # Mesh services (from P2P if available) if command -v secubox-p2p >/dev/null 2>&1; then mesh_count=$(secubox-p2p shared-services 2>/dev/null | wc -l) fi # Collect service details local tor_services=$(collect_tor_services) local dns_services=$(collect_dns_services) local mesh_services=$(collect_mesh_services) # Health check local health_up=0 local health_total=$((tor_count + dns_count)) # Generate HTML if [ -f "$TPL_DIR/services-status.html.tpl" ]; then sed -e "s|{{HOSTNAME}}|$hostname|g" \ -e "s|{{TIMESTAMP}}|$timestamp|g" \ -e "s|{{TOR_COUNT}}|$tor_count|g" \ -e "s|{{DNS_COUNT}}|$dns_count|g" \ -e "s|{{MESH_COUNT}}|$mesh_count|g" \ -e "s|{{HEALTH_UP}}|$health_up|g" \ -e "s|{{HEALTH_TOTAL}}|$health_total|g" \ -e "s|{{TOR_SERVICES}}|$tor_services|g" \ -e "s|{{DNS_SERVICES}}|$dns_services|g" \ -e "s|{{MESH_SERVICES}}|$mesh_services|g" \ -e "s|{{THEME}}|$theme|g" \ "$TPL_DIR/services-status.html.tpl" > "$output_file" else generate_services_html_inline "$output_file" "$hostname" "$timestamp" \ "$tor_count" "$dns_count" "$mesh_count" \ "$tor_services" "$dns_services" "$mesh_services" fi # Ensure web-readable permissions chmod 644 "$output_file" log_ok "Generated: $output_file" echo "$output_file" } # Generate meta status report (combined dashboard) generate_meta_report() { local output_file="$1" local theme="${2:-dark}" log_info "Generating Meta Status Report..." mkdir -p "$(dirname "$output_file")" local hostname=$(get_hostname) local version=$(get_version) local timestamp=$(date '+%Y-%m-%d %H:%M:%S') # Health score local health_score=0 if command -v ubus >/dev/null 2>&1; then health_score=$(ubus call luci.secubox-core get_full_health_report 2>/dev/null | \ jsonfilter -e '@.health_score' 2>/dev/null || echo "0") fi [ -z "$health_score" ] && health_score=0 # Service counts local tor_count=0 local dns_count=0 local mesh_count=0 [ -d /var/lib/tor/hidden_services ] && tor_count=$(ls -1 /var/lib/tor/hidden_services 2>/dev/null | wc -l) dns_count=$(uci show haproxy 2>/dev/null | grep '=vhost$' | wc -l) command -v secubox-p2p >/dev/null 2>&1 && mesh_count=$(secubox-p2p shared-services 2>/dev/null | wc -l) local total_services=$((tor_count + dns_count + mesh_count)) local services_pct=100 [ $total_services -gt 0 ] && services_pct=$((total_services * 100 / 300)) [ $services_pct -gt 100 ] && services_pct=100 # Calculate percentages for bars local tor_pct=0 dns_pct=0 mesh_pct=0 [ $total_services -gt 0 ] && { tor_pct=$((tor_count * 100 / total_services)) dns_pct=$((dns_count * 100 / total_services)) mesh_pct=$((mesh_count * 100 / total_services)) } # System stats local packages_count=$(opkg list-installed 2>/dev/null | wc -l) local containers_count=$(lxc-ls 2>/dev/null | wc -w) local uptime_pct=99 # Dev stats local features_done=0 local wip_count=0 [ -f "$REPO_PATH/.claude/HISTORY.md" ] && features_done=$(grep -c "^\*\*" "$REPO_PATH/.claude/HISTORY.md" 2>/dev/null || echo 0) [ -f "$REPO_PATH/.claude/WIP.md" ] && wip_count=$(grep -c "^- \*\*" "$REPO_PATH/.claude/WIP.md" 2>/dev/null || echo 0) # Collect formatted data local history_entries=$(collect_history "$REPO_PATH/.claude/HISTORY.md" 2>/dev/null || echo '

No history data

') local wip_entries=$(collect_wip "$REPO_PATH/.claude/WIP.md" 2>/dev/null || echo '

No WIP data

') local roadmap_data=$(collect_roadmap "$REPO_PATH/.claude/ROADMAP.md" 2>/dev/null || echo '

No roadmap data

') local tor_services=$(collect_tor_services 2>/dev/null || echo '

No Tor services

') local dns_services=$(collect_dns_services 10 2>/dev/null || echo '

No DNS services

') # Generate from template if [ -f "$TPL_DIR/meta-status.html.tpl" ]; then sed -e "s|{{HOSTNAME}}|$hostname|g" \ -e "s|{{VERSION}}|$version|g" \ -e "s|{{TIMESTAMP}}|$timestamp|g" \ -e "s|{{HEALTH_SCORE}}|$health_score|g" \ -e "s|{{TOTAL_SERVICES}}|$total_services|g" \ -e "s|{{SERVICES_PCT}}|$services_pct|g" \ -e "s|{{UPTIME_PCT}}|$uptime_pct|g" \ -e "s|{{TOR_COUNT}}|$tor_count|g" \ -e "s|{{DNS_COUNT}}|$dns_count|g" \ -e "s|{{MESH_COUNT}}|$mesh_count|g" \ -e "s|{{TOR_PCT}}|$tor_pct|g" \ -e "s|{{DNS_PCT}}|$dns_pct|g" \ -e "s|{{MESH_PCT}}|$mesh_pct|g" \ -e "s|{{PACKAGES_COUNT}}|$packages_count|g" \ -e "s|{{CONTAINERS_COUNT}}|$containers_count|g" \ -e "s|{{FEATURES_DONE}}|$features_done|g" \ -e "s|{{WIP_COUNT}}|$wip_count|g" \ -e "s|{{HISTORY_ENTRIES}}|$history_entries|g" \ -e "s|{{WIP_ENTRIES}}|$wip_entries|g" \ -e "s|{{ROADMAP_DATA}}|$roadmap_data|g" \ -e "s|{{TOR_SERVICES}}|$tor_services|g" \ -e "s|{{DNS_SERVICES}}|$dns_services|g" \ "$TPL_DIR/meta-status.html.tpl" > "$output_file" else log_err "Meta template not found: $TPL_DIR/meta-status.html.tpl" return 1 fi chmod 644 "$output_file" log_ok "Generated: $output_file" echo "$output_file" } # Generate system/hardware status report generate_system_report() { local output_file="$1" local theme="${2:-dark}" log_info "Generating System Hardware Report..." mkdir -p "$(dirname "$output_file")" local hostname=$(get_hostname) local timestamp=$(date '+%Y-%m-%d %H:%M:%S') # Collect system data with defaults local cpu_pct=$(get_cpu_usage 2>/dev/null || echo 10) [ -z "$cpu_pct" ] && cpu_pct=10 local mem_info=$(get_memory_info 2>/dev/null || echo "512 1024") local mem_used=$(echo "$mem_info" | awk '{print $1}') local mem_total=$(echo "$mem_info" | awk '{print $2}') [ -z "$mem_used" ] && mem_used=512 [ -z "$mem_total" ] && mem_total=1024 [ "$mem_total" -eq 0 ] && mem_total=1024 local mem_pct=$((mem_used * 100 / mem_total)) local disk_info=$(get_disk_info 2>/dev/null || echo "1G 8G 20") local disk_used=$(echo "$disk_info" | awk '{print $1}') local disk_total=$(echo "$disk_info" | awk '{print $2}') local disk_pct=$(echo "$disk_info" | awk '{print $3}') [ -z "$disk_pct" ] && disk_pct=20 local temp=$(get_temperature 2>/dev/null || echo 45) [ -z "$temp" ] && temp=45 local temp_pct=$temp local cpu_freq=$(get_cpu_freq 2>/dev/null || echo "1000 MHz") local cpu_model=$(get_cpu_model 2>/dev/null || echo "ARM Processor") local cpu_cores=$(get_cpu_cores 2>/dev/null || echo "4") local device_model=$(get_device_model 2>/dev/null || echo "SecuBox") local board=$(get_board_name 2>/dev/null || echo "secubox") local openwrt_ver=$(get_openwrt_version 2>/dev/null || echo "23.05") local kernel=$(get_kernel_version 2>/dev/null || echo "6.1") local arch=$(get_architecture 2>/dev/null || echo "aarch64") local uptime=$(get_uptime_formatted 2>/dev/null || echo "1h 0m") local load_avg=$(get_load_average 2>/dev/null || echo "0.5 0.3 0.2") local process_count=$(get_process_count 2>/dev/null || echo "50") # Status classes local cpu_class=$(get_status_class "$cpu_pct" 2>/dev/null || echo "") local mem_class=$(get_status_class "$mem_pct" 2>/dev/null || echo "") local disk_class=$(get_status_class "$disk_pct" 2>/dev/null || echo "") local temp_class=$(get_status_class "$temp" temp 2>/dev/null || echo "") # Power calculations local power_watts=$(estimate_power_watts "$cpu_pct" 2>/dev/null || echo 8) [ -z "$power_watts" ] && power_watts=8 local daily_kwh="0.19" local monthly_kwh="5.8" local co2_monthly="2.3" # Read template local template="$TPL_DIR/system-status.html.tpl" if [ ! -f "$template" ]; then log_err "Template not found: $template" return 1 fi # Generate dynamic content to temp files local tmpdir="/tmp/sysreport-$$" mkdir -p "$tmpdir" get_top_processes > "$tmpdir/procs.html" 2>/dev/null || echo "No process data" > "$tmpdir/procs.html" get_network_stats > "$tmpdir/network.html" 2>/dev/null || echo "
N/A
" > "$tmpdir/network.html" generate_cpu_histogram > "$tmpdir/histogram.html" 2>/dev/null || echo "" > "$tmpdir/histogram.html" generate_recommendations "$cpu_pct" "$mem_pct" "$disk_pct" "$temp" > "$tmpdir/recs.html" 2>/dev/null || echo "" > "$tmpdir/recs.html" get_debug_log > "$tmpdir/debug.html" 2>/dev/null || echo "
No log data
" > "$tmpdir/debug.html" # Simple substitutions first sed -e "s|{{HOSTNAME}}|$hostname|g" \ -e "s|{{TIMESTAMP}}|$timestamp|g" \ -e "s|{{DEVICE_MODEL}}|$device_model|g" \ -e "s|{{UPTIME}}|$uptime|g" \ -e "s|{{CPU_PCT}}|$cpu_pct|g" \ -e "s|{{CPU_CLASS}}|$cpu_class|g" \ -e "s|{{MEM_PCT}}|$mem_pct|g" \ -e "s|{{MEM_CLASS}}|$mem_class|g" \ -e "s|{{DISK_PCT}}|$disk_pct|g" \ -e "s|{{DISK_CLASS}}|$disk_class|g" \ -e "s|{{TEMP_VAL}}|$temp|g" \ -e "s|{{TEMP_PCT}}|$temp_pct|g" \ -e "s|{{TEMP_CLASS}}|$temp_class|g" \ -e "s|{{CPU_FREQ}}|$cpu_freq|g" \ -e "s|{{MEM_USED}}|${mem_used}MB|g" \ -e "s|{{MEM_TOTAL}}|${mem_total}MB|g" \ -e "s|{{DISK_USED}}|$disk_used|g" \ -e "s|{{DISK_TOTAL}}|$disk_total|g" \ -e "s|{{PROCESS_COUNT}}|$process_count|g" \ -e "s|{{CPU_MODEL}}|$cpu_model|g" \ -e "s|{{CPU_CORES}}|$cpu_cores|g" \ -e "s|{{ARCH}}|$arch|g" \ -e "s|{{KERNEL}}|$kernel|g" \ -e "s|{{BOARD}}|$board|g" \ -e "s|{{OPENWRT_VER}}|$openwrt_ver|g" \ -e "s|{{LOAD_AVG}}|$load_avg|g" \ -e "s|{{POWER_WATTS}}|$power_watts|g" \ -e "s|{{DAILY_KWH}}|$daily_kwh|g" \ -e "s|{{MONTHLY_KWH}}|$monthly_kwh|g" \ -e "s|{{CO2_MONTHLY}}|$co2_monthly|g" \ "$template" > "$tmpdir/step1.html" # Replace multiline placeholders using awk awk ' /\{\{CPU_HISTOGRAM\}\}/ { while ((getline line < "'"$tmpdir/histogram.html"'") > 0) print line; next } /\{\{TOP_PROCESSES\}\}/ { while ((getline line < "'"$tmpdir/procs.html"'") > 0) print line; next } /\{\{NETWORK_STATS\}\}/ { while ((getline line < "'"$tmpdir/network.html"'") > 0) print line; next } /\{\{RECOMMENDATIONS\}\}/ { while ((getline line < "'"$tmpdir/recs.html"'") > 0) print line; next } /\{\{DEBUG_LOG\}\}/ { while ((getline line < "'"$tmpdir/debug.html"'") > 0) print line; next } { print } ' "$tmpdir/step1.html" > "$output_file" # Cleanup rm -rf "$tmpdir" chmod 644 "$output_file" log_ok "Generated: $output_file" echo "$output_file" } # Command: generate cmd_generate() { local report_type="$1" local theme="$THEME" local output_path="$OUTPUT_DIR" shift while [ $# -gt 0 ]; do case "$1" in --theme) theme="$2"; shift 2 ;; --output) output_path="$2"; shift 2 ;; *) shift ;; esac done local timestamp=$(date '+%Y%m%d-%H%M%S') case "$report_type" in dev) generate_dev_report "$output_path/dev-status-$timestamp.html" "$theme" ;; services) generate_services_report "$output_path/services-status-$timestamp.html" "$theme" ;; meta) generate_meta_report "$output_path/meta-status-$timestamp.html" "$theme" ;; system) generate_system_report "$output_path/system-status-$timestamp.html" "$theme" ;; all) generate_dev_report "$output_path/dev-status-$timestamp.html" "$theme" generate_services_report "$output_path/services-status-$timestamp.html" "$theme" generate_meta_report "$output_path/meta-status-$timestamp.html" "$theme" generate_system_report "$output_path/system-status-$timestamp.html" "$theme" ;; *) log_err "Unknown report type: $report_type" echo "Valid types: dev, services, meta, system, all" return 1 ;; esac } # Command: send cmd_send() { local report_type="$1" shift # Generate report first local report_file=$(cmd_generate "$report_type" "$@") if [ -z "$report_file" ] || [ ! -f "$report_file" ]; then log_err "Failed to generate report" return 1 fi # Send via email local recipient="" config_get recipient email recipient "" if [ -z "$recipient" ]; then log_warn "No email recipient configured. Report saved to: $report_file" return 0 fi log_info "Sending report to $recipient..." if send_report_email "$report_type" "$(cat "$report_file")" "$recipient"; then log_ok "Report sent to $recipient" else log_err "Failed to send email" return 1 fi } # Command: schedule cmd_schedule() { local report_type="$1" local frequency="$2" local cron_file="/etc/cron.d/secubox-reporter" case "$frequency" in daily) sed -i "s/^#DAILY_${report_type^^}#//" "$cron_file" 2>/dev/null uci set secubox-reporter.schedule.$report_type='daily' ;; weekly) sed -i "s/^#WEEKLY_${report_type^^}#//" "$cron_file" 2>/dev/null uci set secubox-reporter.schedule.$report_type='weekly' ;; off) # Re-comment the lines sed -i "/secubox-reportctl.*$report_type/s/^[^#]/#DISABLED#/" "$cron_file" 2>/dev/null uci set secubox-reporter.schedule.$report_type='off' ;; *) log_err "Invalid frequency: $frequency" echo "Valid: daily, weekly, off" return 1 ;; esac uci commit secubox-reporter /etc/init.d/cron restart 2>/dev/null || true log_ok "Schedule set: $report_type = $frequency" } # Command: status cmd_status() { local json_output=0 [ "$1" = "--json" ] && json_output=1 local hostname=$(get_hostname) local version=$(get_version) local dev_schedule=$(uci -q get secubox-reporter.schedule.dev || echo "off") local services_schedule=$(uci -q get secubox-reporter.schedule.services || echo "off") local recipient=$(uci -q get secubox-reporter.email.recipient || echo "") local report_count=$(ls -1 "$OUTPUT_DIR"/*.html 2>/dev/null | wc -l) if [ $json_output -eq 1 ]; then cat << EOF {"hostname":"$hostname","version":"$version","schedules":{"dev":"$dev_schedule","services":"$services_schedule"},"email_recipient":"$recipient","report_count":$report_count} EOF else echo "SecuBox Report Generator v$VERSION" echo "==================================" echo "Hostname: $hostname" echo "Version: $version" echo "Output Dir: $OUTPUT_DIR" echo "Reports: $report_count files" echo "" echo "Schedules:" echo " Dev: $dev_schedule" echo " Services: $services_schedule" echo "" echo "Email: ${recipient:-Not configured}" fi } # Command: list cmd_list() { local json_output=0 [ "$1" = "--json" ] && json_output=1 if [ $json_output -eq 1 ]; then echo '{"reports":[' local first=1 for f in $(ls -1t "$OUTPUT_DIR"/*.html 2>/dev/null); do [ $first -eq 0 ] && echo "," first=0 local name=$(basename "$f") local size=$(stat -c%s "$f" 2>/dev/null || echo 0) local mtime=$(stat -c%Y "$f" 2>/dev/null || echo 0) echo "{\"name\":\"$name\",\"size\":$size,\"mtime\":$mtime}" done echo ']}' else echo "Generated Reports:" echo "==================" ls -lh "$OUTPUT_DIR"/*.html 2>/dev/null || echo "No reports found" fi } # Command: clean cmd_clean() { local days="${1:-30}" log_info "Removing reports older than $days days..." local count=$(find "$OUTPUT_DIR" -name "*.html" -mtime +$days 2>/dev/null | wc -l) find "$OUTPUT_DIR" -name "*.html" -mtime +$days -delete 2>/dev/null log_ok "Removed $count old reports" } # Command: preview cmd_preview() { local report_type="$1" local tmpfile="/tmp/secubox-report-preview-$$.html" case "$report_type" in dev) generate_dev_report "$tmpfile" "$THEME" >/dev/null ;; services) generate_services_report "$tmpfile" "$THEME" >/dev/null ;; *) log_err "Unknown report type: $report_type" return 1 ;; esac cat "$tmpfile" rm -f "$tmpfile" } # Main case "$1" in generate) shift cmd_generate "$@" ;; send) shift cmd_send "$@" ;; schedule) shift cmd_schedule "$@" ;; status) shift cmd_status "$@" ;; list) shift cmd_list "$@" ;; clean) shift cmd_clean "$@" ;; preview) shift cmd_preview "$@" ;; help|--help|-h|"") usage ;; *) log_err "Unknown command: $1" usage exit 1 ;; esac