From e13b6e4c8cb98dc1a8684bd849e551676e83d740 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Thu, 5 Feb 2026 10:16:19 +0100 Subject: [PATCH] feat(vhost-manager): Add centralized VHost manager - Create secubox-app-vhost-manager package for unified vhost orchestration - Single CLI tool (secubox-vhost) manages HAProxy, DNS, Tor, Mesh, mitmproxy - Unified UCI config (/etc/config/vhosts) as single source of truth - Backend adapters for each component (haproxy.sh, dns.sh, tor.sh, mesh.sh, mitmproxy.sh) - Centralized backend resolution function (backends.sh) - Import tool for existing HAProxy vhosts - Validation of backend reachability before creation Also includes: - FAQ-TROUBLESHOOTING.md with LXC cgroup v1/v2 fixes - Fix mitmproxyctl cgroup v1 -> v2 syntax for container compatibility - HAProxy backend resolution bugfixes CLI commands: secubox-vhost add [--ssl] [--tor] [--mesh] secubox-vhost remove/list/status/enable/disable/set/sync/validate/import Co-Authored-By: Claude Opus 4.5 --- .claude/FAQ-TROUBLESHOOTING.md | 332 ++++++++++ .claude/HISTORY.md | 20 + .claude/WIP.md | 16 + .claude/settings.local.json | 3 +- .../root/usr/libexec/rpcd/luci.haproxy | 42 +- .../secubox/luci-app-threat-analyst/Makefile | 1 + .../resources/threat-analyst/api.js | 95 ++- .../resources/threat-analyst/dashboard.css | 454 ++++++++++++++ .../view/threat-analyst/dashboard.js | 395 +++++++----- .../root/usr/libexec/rpcd/luci.threat-analyst | 15 +- .../root/www/secubox-feed/Packages | 166 ++--- .../root/www/secubox-feed/Packages.gz | Bin 10376 -> 10380 bytes .../root/www/secubox-feed/apps-local.json | 166 ++--- .../luci-app-auth-guardian_0.4.0-r3_all.ipk | Bin 11735 -> 11736 bytes ...uci-app-bandwidth-manager_0.5.0-r2_all.ipk | Bin 61539 -> 61539 bytes .../luci-app-cdn-cache_0.5.0-r3_all.ipk | Bin 23185 -> 23182 bytes .../luci-app-client-guardian_0.4.0-r7_all.ipk | Bin 54535 -> 54536 bytes ...i-app-crowdsec-dashboard_0.7.0-r32_all.ipk | Bin 33740 -> 33739 bytes .../luci-app-cyberfeed_0.1.1-r1_all.ipk | Bin 12839 -> 12837 bytes .../luci-app-device-intel_1.0.0-r1_all.ipk | Bin 10862 -> 10859 bytes .../luci-app-dns-provider_1.0.0-r1_all.ipk | Bin 7127 -> 7130 bytes .../luci-app-dnsguard_1.1.0-r1_all.ipk | Bin 12413 -> 12413 bytes .../luci-app-exposure_1.0.0-r3_all.ipk | Bin 9838 -> 9841 bytes .../luci-app-gitea_1.0.0-r2_all.ipk | Bin 15299 -> 15300 bytes .../luci-app-glances_1.0.0-r2_all.ipk | Bin 6968 -> 6969 bytes .../luci-app-haproxy_1.0.0-r8_all.ipk | Bin 34560 -> 34560 bytes .../luci-app-hexojs_1.0.0-r3_all.ipk | Bin 30311 -> 30307 bytes .../luci-app-jellyfin_1.0.0-r1_all.ipk | Bin 6060 -> 6061 bytes .../luci-app-jitsi_1.0.0-r1_all.ipk | Bin 5139 -> 5134 bytes .../luci-app-ksm-manager_0.4.0-r2_all.ipk | Bin 18724 -> 18719 bytes .../luci-app-localai_0.1.0-r15_all.ipk | Bin 13181 -> 13182 bytes .../luci-app-lyrion_1.0.0-r1_all.ipk | Bin 6727 -> 6725 bytes .../luci-app-mac-guardian_0.5.0-r1_all.ipk | Bin 6512 -> 6509 bytes .../luci-app-magicmirror2_0.4.0-r6_all.ipk | Bin 12277 -> 12277 bytes .../luci-app-mailinabox_1.0.0-r1_all.ipk | Bin 5486 -> 5486 bytes .../luci-app-master-link_1.0.0-r1_all.ipk | Bin 6248 -> 6245 bytes .../luci-app-media-flow_0.6.4-r1_all.ipk | Bin 25416 -> 25418 bytes .../luci-app-metablogizer_1.0.0-r5_all.ipk | Bin 24773 -> 24773 bytes .../luci-app-metabolizer_1.0.0-r2_all.ipk | Bin 4760 -> 4760 bytes .../luci-app-mitmproxy_0.5.0-r2_all.ipk | Bin 11152 -> 11149 bytes .../luci-app-mmpm_0.2.0-r3_all.ipk | Bin 7901 -> 7899 bytes .../luci-app-mqtt-bridge_0.4.0-r4_all.ipk | Bin 22779 -> 22780 bytes .../luci-app-ndpid_1.1.2-r2_all.ipk | Bin 22653 -> 22654 bytes ...uci-app-netdata-dashboard_0.5.0-r2_all.ipk | Bin 20488 -> 20484 bytes .../luci-app-network-modes_0.5.0-r3_all.ipk | Bin 54148 -> 54149 bytes .../luci-app-network-tweaks_1.0.0-r7_all.ipk | Bin 14960 -> 14957 bytes .../luci-app-nextcloud_1.0.0-r1_all.ipk | Bin 6486 -> 6489 bytes .../luci-app-ollama_0.1.0-r1_all.ipk | Bin 12355 -> 12352 bytes .../luci-app-picobrew_1.0.0-r1_all.ipk | Bin 9458 -> 9459 bytes .../luci-app-secubox-admin_1.0.0-r19_all.ipk | Bin 57247 -> 57245 bytes ...luci-app-secubox-crowdsec_1.0.0-r3_all.ipk | Bin 13917 -> 13923 bytes .../luci-app-secubox-netdiag_1.0.0-r1_all.ipk | Bin 15308 -> 15306 bytes .../luci-app-secubox-netifyd_1.2.1-r1_all.ipk | Bin 36544 -> 36544 bytes .../luci-app-secubox-p2p_0.1.0-r1_all.ipk | Bin 44085 -> 44086 bytes .../luci-app-secubox-portal_0.7.0-r2_all.ipk | Bin 24646 -> 24644 bytes ...-secubox-security-threats_1.0.0-r4_all.ipk | Bin 21895 -> 21895 bytes .../luci-app-secubox_0.7.1-r4_all.ipk | Bin 77681 -> 77681 bytes ...luci-app-service-registry_1.0.0-r1_all.ipk | Bin 39826 -> 39828 bytes .../luci-app-simplex_1.0.0-r1_all.ipk | Bin 6999 -> 7000 bytes .../luci-app-streamlit_1.0.0-r11_all.ipk | Bin 14748 -> 14749 bytes .../luci-app-system-hub_0.5.1-r4_all.ipk | Bin 61106 -> 61105 bytes .../luci-app-threat-analyst_1.0.0-r1_all.ipk | Bin 6951 -> 9753 bytes .../luci-app-tor-shield_1.0.0-r10_all.ipk | Bin 22361 -> 22362 bytes .../luci-app-traffic-shaper_0.4.0-r2_all.ipk | Bin 14537 -> 14535 bytes .../luci-app-vhost-manager_0.5.0-r5_all.ipk | Bin 26181 -> 26186 bytes ...i-app-wireguard-dashboard_0.7.0-r5_all.ipk | Bin 39609 -> 39607 bytes .../luci-app-zigbee2mqtt_1.0.0-r2_all.ipk | Bin 6815 -> 6813 bytes .../luci-theme-secubox_0.4.7-r1_all.ipk | Bin 110241 -> 110239 bytes .../secubox-app-adguardhome_1.0.0-r2_all.ipk | Bin 2882 -> 2880 bytes .../secubox-app-auth-logger_1.2.2-r1_all.ipk | Bin 9375 -> 9379 bytes ...cubox-app-crowdsec-custom_1.1.0-r1_all.ipk | Bin 5758 -> 5761 bytes ...l-bouncer_0.0.31-r4_aarch64_cortex-a72.ipk | Bin 5049326 -> 5049328 bytes .../secubox-app-cyberfeed_0.2.1-r1_all.ipk | Bin 12453 -> 12454 bytes .../secubox-app-device-intel_1.0.0-r1_all.ipk | Bin 13005 -> 13004 bytes .../secubox-app-dns-provider_1.0.0-r1_all.ipk | Bin 5590 -> 6702 bytes .../secubox-app-domoticz_1.0.0-r2_all.ipk | Bin 2546 -> 2548 bytes .../secubox-app-exposure_1.0.0-r1_all.ipk | Bin 6936 -> 6934 bytes .../secubox-app-gitea_1.0.0-r5_all.ipk | Bin 9402 -> 9407 bytes .../secubox-app-glances_1.0.0-r1_all.ipk | Bin 5536 -> 5538 bytes .../secubox-app-haproxy_1.0.0-r23_all.ipk | Bin 15683 -> 15687 bytes .../secubox-app-hexojs_1.0.0-r8_all.ipk | Bin 94937 -> 94940 bytes .../secubox-app-jellyfin_1.0.0-r1_all.ipk | Bin 6182 -> 6185 bytes .../secubox-app-jitsi_1.0.0-r1_all.ipk | Bin 8913 -> 8914 bytes .../secubox-app-localai-wb_2.25.0-r1_all.ipk | Bin 7953 -> 7951 bytes .../secubox-app-localai_2.25.0-r1_all.ipk | Bin 5722 -> 5720 bytes .../secubox-app-lyrion_2.0.2-r1_all.ipk | Bin 7288 -> 7287 bytes .../secubox-app-mac-guardian_0.5.0-r1_all.ipk | Bin 12095 -> 12099 bytes .../secubox-app-magicmirror2_0.4.0-r8_all.ipk | Bin 9254 -> 9253 bytes .../secubox-app-mailinabox_2.0.0-r1_all.ipk | Bin 7571 -> 7572 bytes .../secubox-app-metabolizer_1.0.0-r3_all.ipk | Bin 13980 -> 13980 bytes .../secubox-app-mitmproxy_0.5.0-r19_all.ipk | Bin 22954 -> 22962 bytes .../secubox-app-mmpm_0.2.0-r5_all.ipk | Bin 3977 -> 3975 bytes .../secubox-app-nextcloud_1.0.0-r2_all.ipk | Bin 2955 -> 2958 bytes .../secubox-app-ollama_0.1.0-r1_all.ipk | Bin 5736 -> 5739 bytes .../secubox-app-picobrew_1.0.0-r7_all.ipk | Bin 5539 -> 5540 bytes .../secubox-app-simplex_1.0.0-r1_all.ipk | Bin 9233 -> 9235 bytes .../secubox-app-streamlit_1.0.0-r5_all.ipk | Bin 11722 -> 11722 bytes .../secubox-app-tor_1.0.0-r1_all.ipk | Bin 7370 -> 7371 bytes .../secubox-app-webapp_1.5.0-r7_all.ipk | Bin 39173 -> 39175 bytes .../secubox-app-zigbee2mqtt_1.0.0-r3_all.ipk | Bin 5507 -> 5510 bytes .../secubox-feed/secubox-app_1.0.0-r2_all.ipk | Bin 11182 -> 11184 bytes .../secubox-core_0.10.0-r11_all.ipk | Bin 87977 -> 87975 bytes .../secubox-dns-guard_1.0.0-r1_all.ipk | Bin 12476 -> 12473 bytes .../secubox-master-link_1.0.0-r1_all.ipk | Bin 12453 -> 12456 bytes .../secubox-mcp-server_1.0.0-r1_all.ipk | Bin 11430 -> 11431 bytes .../secubox-feed/secubox-p2p_0.6.0-r3_all.ipk | Bin 47675 -> 47677 bytes .../secubox-threat-analyst_1.0.0-r1_all.ipk | Bin 9868 -> 9867 bytes .../files/usr/sbin/secubox-subdomain | 335 ++++++++++ .../files/usr/sbin/mitmproxyctl | 19 +- .../secubox-app-vhost-manager/Makefile | 39 ++ .../files/etc/config/vhosts | 23 + .../files/usr/lib/vhost-manager/backends.sh | 85 +++ .../files/usr/lib/vhost-manager/dns.sh | 62 ++ .../files/usr/lib/vhost-manager/haproxy.sh | 81 +++ .../files/usr/lib/vhost-manager/mesh.sh | 50 ++ .../files/usr/lib/vhost-manager/mitmproxy.sh | 79 +++ .../files/usr/lib/vhost-manager/tor.sh | 73 +++ .../files/usr/sbin/secubox-vhost | 573 ++++++++++++++++++ 118 files changed, 2751 insertions(+), 373 deletions(-) create mode 100644 .claude/FAQ-TROUBLESHOOTING.md create mode 100644 package/secubox/luci-app-threat-analyst/htdocs/luci-static/resources/threat-analyst/dashboard.css create mode 100644 package/secubox/secubox-app-dns-provider/files/usr/sbin/secubox-subdomain create mode 100644 package/secubox/secubox-app-vhost-manager/Makefile create mode 100644 package/secubox/secubox-app-vhost-manager/files/etc/config/vhosts create mode 100644 package/secubox/secubox-app-vhost-manager/files/usr/lib/vhost-manager/backends.sh create mode 100644 package/secubox/secubox-app-vhost-manager/files/usr/lib/vhost-manager/dns.sh create mode 100644 package/secubox/secubox-app-vhost-manager/files/usr/lib/vhost-manager/haproxy.sh create mode 100644 package/secubox/secubox-app-vhost-manager/files/usr/lib/vhost-manager/mesh.sh create mode 100644 package/secubox/secubox-app-vhost-manager/files/usr/lib/vhost-manager/mitmproxy.sh create mode 100644 package/secubox/secubox-app-vhost-manager/files/usr/lib/vhost-manager/tor.sh create mode 100644 package/secubox/secubox-app-vhost-manager/files/usr/sbin/secubox-vhost diff --git a/.claude/FAQ-TROUBLESHOOTING.md b/.claude/FAQ-TROUBLESHOOTING.md new file mode 100644 index 00000000..0230167a --- /dev/null +++ b/.claude/FAQ-TROUBLESHOOTING.md @@ -0,0 +1,332 @@ +# SecuBox Troubleshooting FAQ + +_Last updated: 2026-02-06_ + +This document collects resolved issues and their solutions for future reference. + +--- + +## LXC Container Issues + +### Issue: LXC containers fail to start with "Failed to mount /sys/fs/cgroup" + +**Symptoms:** +``` +ERROR cgfsng - Failed to create cgroup at_mnt 38() +ERROR conf - Failed to mount "/sys/fs/cgroup" +ERROR conf - Failed to setup remaining automatic mounts +Received container state "ABORTING" instead of "RUNNING" +``` + +**Root Cause:** +OpenWrt uses cgroup v2 (unified hierarchy), but LXC configs may be using cgroup v1 syntax. + +**Solution:** + +1. **Fix global LXC defaults** - Create/edit `/usr/share/lxc/config/common.conf`: +``` +# Comment out all lxc.cgroup.devices lines (cgroup v1 syntax) +# These cause "Failed to mount /sys/fs/cgroup" on cgroup v2 systems +#lxc.cgroup.devices.deny = a +#lxc.cgroup.devices.allow = c *:* m +# ... (all device lines commented out) +``` + +2. **Fix per-container config** - Replace cgroup v1 with v2 syntax: +``` +# OLD (cgroup v1 - breaks on cgroup v2 systems): +lxc.cgroup.memory.limit_in_bytes = 256M + +# NEW (cgroup v2): +lxc.cgroup2.memory.max = 268435456 +``` + +3. **Add cgroup v2 compatibility flags**: +``` +lxc.seccomp.profile = +lxc.tty.max = 0 +lxc.pty.max = 256 +lxc.cap.drop = sys_module mac_admin mac_override sys_time +``` + +**Reference:** [OpenWrt Forum LXC Guide](https://forum.openwrt.org/t/openwrt-arm64-quick-lxc-howto-guide-lms-in-debian-system-in-lxc-container/99835) + +--- + +### Issue: Alpine-based LXC rootfs incompatible with host cgroups + +**Symptoms:** +Container starts but immediately exits, or mounts fail inside container. + +**Solution:** +Use Debian-based rootfs instead of Alpine. Copy from a working container: +```bash +# Create new container from working Debian rootfs +cp -a /srv/lxc/domoticz/rootfs /srv/lxc/newcontainer/rootfs +``` + +--- + +## Networking Issues + +### Issue: Port 80 requests redirected to port 8888 + +**Symptoms:** +HTTP requests on port 80 go to mitmproxy (8888) instead of HAProxy. + +**Root Cause:** +mitmproxy WAN protection mode uses nftables to redirect incoming WAN traffic. + +**Solution:** +```bash +# Check if mitmproxy WAN protection is enabled +uci get mitmproxy.wan_protection.enabled + +# Disable it +uci set mitmproxy.wan_protection.enabled='0' +uci commit mitmproxy + +# Remove nftables rules +nft delete table inet mitmproxy_wan +``` + +--- + +### Issue: DNS rebind attack blocking internal IPs + +**Symptoms:** +BIND (or other DNS server) returns private IP (192.168.x.x), but clients get SERVFAIL. + +**Root Cause:** +dnsmasq has DNS rebind protection that blocks private IPs in DNS responses (security feature against DNS rebinding attacks). + +**Solution:** +Whitelist the domain in dnsmasq config: +``` +# /etc/dnsmasq.d/yourdomain.conf +rebind-domain-ok=/yourdomain.com/ +``` + +Then restart dnsmasq: +```bash +/etc/init.d/dnsmasq restart +``` + +--- + +### Issue: WAN traffic not reaching Docker/LXC containers + +**Symptoms:** +External requests on ports 80/443 timeout, but LAN access works. + +**Root Cause:** +Firewall forward chain missing rules for WAN to Docker bridge. + +**Solution:** +```bash +# Check firewall rules +nft list chain inet fw4 forward_wan + +# Add forward rules for HTTP/HTTPS +# Via LuCI: Network > Firewall > Traffic Rules +# Or via UCI: +uci add firewall rule +uci set firewall.@rule[-1].name='Forward-HAProxy-HTTP' +uci set firewall.@rule[-1].src='wan' +uci set firewall.@rule[-1].dest='docker' +uci set firewall.@rule[-1].proto='tcp' +uci set firewall.@rule[-1].dest_port='80' +uci set firewall.@rule[-1].target='ACCEPT' +uci commit firewall +/etc/init.d/firewall restart +``` + +--- + +## HAProxy Issues + +### Issue: HAProxy fails with "unable to find required use_backend" + +**Symptoms:** +``` +[ALERT] config : Proxy 'https-in': unable to find required use_backend: '127.0.0.1:8091' +``` + +**Root Cause:** +`haproxyctl generate` created invalid backend references using IP:port format instead of backend names. + +**Solution:** +1. Check for invalid backends: +```bash +grep -n 'use_backend.*127.0.0.1' /srv/haproxy/config/haproxy.cfg +``` + +2. Fix by either: + - Manually edit the config to use proper backend names + - Delete the vhost config files and regenerate + - Create missing backend definitions + +``` +# Example fix - add missing backend definition: +backend localai + mode http + server localai 127.0.0.1:8091 check inter 10s +``` + +--- + +## Mitmproxy WAF Issues + +### Issue: Mitmproxy container stops after haproxy-enable + +**Symptoms:** +`mitmproxyctl haproxy-enable` completes but container is STOPPED. + +**Root Cause:** +The enable command restarts services which regenerates the LXC config with cgroup v1 syntax. + +**Solution:** +Patch `/usr/sbin/mitmproxyctl` to use cgroup v2 syntax: +```bash +sed -i "s/lxc.cgroup.memory.limit_in_bytes/lxc.cgroup2.memory.max/" /usr/sbin/mitmproxyctl +``` + +Also add seccomp disable after the cgroup line: +```bash +sed -i "/lxc.cgroup2.memory.max/a lxc.seccomp.profile =" /usr/sbin/mitmproxyctl +``` + +Then manually fix the container config and restart: +```bash +# Edit /srv/lxc/mitmproxy/config with cgroup v2 syntax +lxc-start -n mitmproxy +``` + +--- + +### Issue: Mitmproxy not detecting threats + +**Symptoms:** +`/srv/mitmproxy/threats.log` is empty or not being updated. + +**Checklist:** +1. Container running: `lxc-info -n mitmproxy` +2. Port 8889 listening: `netstat -tlnp | grep 8889` +3. HAProxy routing through mitmproxy: `grep mitmproxy_inspector /srv/haproxy/config/haproxy.cfg` +4. Routes synced: `cat /srv/mitmproxy/haproxy-routes.json` + +**Solution:** +```bash +mitmproxyctl sync-routes +mitmproxyctl haproxy-enable +``` + +--- + +## DNS Provider Issues + +### Issue: Let's Encrypt DNS-01 fails with CAA timeout + +**Symptoms:** +ACME challenge fails because CAA record lookup times out. + +**Root Cause:** +Router is authoritative for the domain but dnsmasq cannot serve CAA records. + +**Solutions:** + +1. **Remove local authority** - Let external DNS (Gandi/Cloudflare) handle everything: +``` +# /etc/dnsmasq.d/yourdomain.conf +# Remove: local=/yourdomain.com/ +# Keep only: server=/yourdomain.com/127.0.0.1#5353 (for BIND) +# Or forward to external: server=/yourdomain.com/8.8.8.8 +``` + +2. **Use BIND instead of dnsmasq** for authoritative DNS (supports CAA records). + +--- + +## Quick Diagnostic Commands + +```bash +# Check all LXC containers +for d in /srv/lxc/*/; do n=$(basename "$d"); lxc-info -n "$n" 2>/dev/null | head -3; done + +# Check listening ports +netstat -tlnp | grep -E "80|443|8889|8089" + +# Check firewall forward rules +nft list chain inet fw4 forward_wan + +# Check DNS resolution +nslookup yourdomain.com 127.0.0.1 + +# Check mitmproxy status +mitmproxyctl status + +# Recent threats +tail -20 /srv/mitmproxy/threats.log + +# HAProxy config test +haproxy -c -f /srv/haproxy/config/haproxy.cfg +``` + +--- + +### Issue: haproxyctl generate creates invalid backend references + +**Symptoms:** +HAProxy config contains `use_backend 127.0.0.1:8091` instead of a named backend. + +**Root Cause:** +UCI vhost entries were created with `backend='127.0.0.1:8091'` (IP:port) instead of a named backend like `backend='localai'`. + +This happens when: +1. `haproxyctl vhost add` is used with a non-existent backend name +2. Manual UCI edits use IP:port instead of backend name +3. Scripts create vhosts without first creating the backend + +**Solution:** +1. Create the backend first: +```bash +haproxyctl backend add localai +haproxyctl server add localai 127.0.0.1:8091 +``` + +2. Then fix the vhost to use the backend name: +```bash +uci set haproxy..backend='localai' +uci set haproxy..original_backend='localai' +uci commit haproxy +haproxyctl generate +``` + +3. Add missing backends to haproxy.cfg: +``` +backend localai + mode http + server localai 127.0.0.1:8091 check inter 10s +``` + +**Prevention:** +Always create named backends before adding vhosts that reference them. + +--- + +## Package-Specific Fixes Applied + +| Package | Issue | Fix | +|---------|-------|-----| +| `mitmproxyctl` | cgroup v1 syntax | Changed to `lxc.cgroup2.memory.max` | +| `dnsmasq` | DNS rebind blocking | Added `rebind-domain-ok` | +| `haproxy` | Invalid backend names | Manual config repair | +| LXC common.conf | cgroup v1 device rules | Commented out device lines | + +--- + +## References + +- [OpenWrt LXC ARM64 Guide](https://forum.openwrt.org/t/openwrt-arm64-quick-lxc-howto-guide-lms-in-debian-system-in-lxc-container/99835) +- [LXC cgroup v2 migration](https://linuxcontainers.org/lxc/manpages/man5/lxc.container.conf.5.html) +- [dnsmasq man page - rebind-domain-ok](https://thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html) diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 86a963c6..7bb7bc53 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -327,3 +327,23 @@ _Last updated: 2026-02-06_ - Service mirroring via reverse proxy chaining - Gossip-based exposure config sync - Submastering/multimixslaving architecture + +32. **Threat Analyst KISS Dashboard v0.1.0 (2026-02-05)** + - Regenerated `luci-app-threat-analyst` following CrowdSec dashboard KISS template pattern. + - **Architectural changes**: + - `api.js`: Migrated from plain object to `baseclass.extend()` pattern + - `dashboard.css`: External CSS file (loaded dynamically in view) + - `dashboard.js`: View-only JS following CrowdSec pattern with `view.extend()` + - **CVE integration**: + - System Health: New "CVE Alerts" indicator with warning icon (yellow) when CVEs detected + - Threats table: New CVE column with hyperlinks to NVD (`https://nvd.nist.gov/vuln/detail/CVE-XXXX-XXXXX`) + - CVE extraction: `extractCVE()` function in API parses CVE-YYYY-NNNNN patterns from scenarios + - CVE row styling: Red-tinted background for CVE-related threats + - **RPCD updates**: + - Status method now returns `cve_alerts` count from CrowdSec alerts + - Fixed output bug (grep `|| echo 0` causing double output) + - **CSS additions**: + - `.ta-health-icon.warning` for CVE alerts in health section + - `.ta-cve-link` for NVD hyperlinks (red badge style) + - `.ta-cve-row` for highlighted CVE threat rows + - Following LuCI UI Generation Model Template v0.1.0 for future KISS modules. diff --git a/.claude/WIP.md b/.claude/WIP.md index f6934ef5..764933fb 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -45,6 +45,15 @@ _Last updated: 2026-02-06_ _None currently active_ +### Just Completed + +- **Subdomain Generator Tool** — DONE (2026-02-05) + - `secubox-subdomain` CLI for generative subdomain management + - Automates: DNS A record + HAProxy vhost + UCI registration + - Uses wildcard certificate (*.zone) for instant SSL + - Quick-add shortcuts for common services (gitea, grafana, jellyfin, etc.) + - Part of Punk Exposure infrastructure + ### Next Up — Couche 1 1. **Guacamole Pre-built Binaries** @@ -89,6 +98,13 @@ _None currently active_ - Created `luci-app-threat-analyst` with AI chatbot dashboard - RPCD handler with 10 methods for status, chat, rules, approval +- **Threat Analyst KISS Dashboard v0.1.0** — DONE (2026-02-05) + - Regenerated LuCI dashboard following CrowdSec KISS template pattern + - External CSS loading, baseclass.extend() API pattern + - CVE alerts in System Health section + - CVE column in threats table with NVD hyperlinks + - AI Security Assistant chat interface + - **MCP Server Implementation** — DONE (2026-02-06) - Created `secubox-mcp-server` package with JSON-RPC 2.0 over stdio - 9 core tools: crowdsec.alerts/decisions, waf.logs, dns.queries, network.flows, system.metrics, wireguard.status, uci.get/set diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3600fdb0..0958327c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -278,7 +278,8 @@ "Bash(jsonfilter:*)", "WebFetch(domain:zigbeefordomoticz.github.io)", "WebFetch(domain:rustdesk.com)", - "WebFetch(domain:deepwiki.com)" + "WebFetch(domain:deepwiki.com)", + "Bash(traceroute:*)" ] } } diff --git a/package/secubox/luci-app-haproxy/root/usr/libexec/rpcd/luci.haproxy b/package/secubox/luci-app-haproxy/root/usr/libexec/rpcd/luci.haproxy index ac9d18bc..89e85143 100755 --- a/package/secubox/luci-app-haproxy/root/usr/libexec/rpcd/luci.haproxy +++ b/package/secubox/luci-app-haproxy/root/usr/libexec/rpcd/luci.haproxy @@ -54,25 +54,35 @@ method_status() { stats_port=$(get_uci main stats_port 8404) stats_enabled=$(get_uci main stats_enabled 1) - # Check container status - prefer lxc-info, fallback to pgrep lxc-start - if command -v lxc-info >/dev/null 2>&1; then - container_running=$(lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0") - else - # Fallback: check if lxc-start is running for haproxy - container_running=$(pgrep -f "lxc-start.*-n haproxy" >/dev/null 2>&1 && echo "1" || echo "0") + # Check container status - Docker first, then LXC + container_running="0" + haproxy_running="0" + + # Check Docker container (secubox-haproxy or haproxy) + if command -v docker >/dev/null 2>&1; then + if docker ps --format '{{.Names}}' 2>/dev/null | grep -qE '^(secubox-haproxy|haproxy)$'; then + container_running="1" + haproxy_running="1" + fi fi - # Check HAProxy process - if [ "$container_running" = "1" ]; then - # Try lxc-attach first, fallback to direct pgrep - if command -v lxc-attach >/dev/null 2>&1; then - haproxy_running=$(lxc-attach -n haproxy -- pgrep haproxy >/dev/null 2>&1 && echo "1" || echo "0") - else - # Fallback: check if haproxy process exists (it runs in container but visible from host) - haproxy_running=$(pgrep -f "haproxy.*haproxy.cfg" >/dev/null 2>&1 && echo "1" || echo "0") + # If not Docker, check LXC + if [ "$container_running" = "0" ]; then + if command -v lxc-info >/dev/null 2>&1; then + if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then + container_running="1" + # Check HAProxy process inside LXC + haproxy_running=$(lxc-attach -n haproxy -- pgrep haproxy >/dev/null 2>&1 && echo "1" || echo "0") + fi + fi + fi + + # Final fallback: check if HAProxy port is listening + if [ "$container_running" = "0" ]; then + if netstat -tln 2>/dev/null | grep -q ":80 "; then + container_running="1" + haproxy_running="1" fi - else - haproxy_running="0" fi json_init diff --git a/package/secubox/luci-app-threat-analyst/Makefile b/package/secubox/luci-app-threat-analyst/Makefile index e581db4f..5dd0dfeb 100644 --- a/package/secubox/luci-app-threat-analyst/Makefile +++ b/package/secubox/luci-app-threat-analyst/Makefile @@ -28,6 +28,7 @@ define Package/luci-app-threat-analyst/install $(INSTALL_DIR) $(1)/www/luci-static/resources/threat-analyst $(INSTALL_DATA) ./htdocs/luci-static/resources/threat-analyst/api.js $(1)/www/luci-static/resources/threat-analyst/ + $(INSTALL_DATA) ./htdocs/luci-static/resources/threat-analyst/dashboard.css $(1)/www/luci-static/resources/threat-analyst/ endef $(eval $(call BuildPackage,luci-app-threat-analyst)) diff --git a/package/secubox/luci-app-threat-analyst/htdocs/luci-static/resources/threat-analyst/api.js b/package/secubox/luci-app-threat-analyst/htdocs/luci-static/resources/threat-analyst/api.js index 98fc7687..639d6152 100644 --- a/package/secubox/luci-app-threat-analyst/htdocs/luci-static/resources/threat-analyst/api.js +++ b/package/secubox/luci-app-threat-analyst/htdocs/luci-static/resources/threat-analyst/api.js @@ -1,6 +1,19 @@ 'use strict'; +'require baseclass'; 'require rpc'; +/** + * Threat Analyst API + * Package: luci-app-threat-analyst + * RPCD object: luci.threat-analyst + * Version: 0.1.0 + * + * Generative AI-powered threat filtering for: + * - CrowdSec autoban scenarios + * - mitmproxy filter rules + * - WAF rules + */ + var callStatus = rpc.declare({ object: 'luci.threat-analyst', method: 'status', @@ -14,13 +27,6 @@ var callGetThreats = rpc.declare({ expect: { } }); -var callGetAlerts = rpc.declare({ - object: 'luci.threat-analyst', - method: 'get_alerts', - params: ['limit'], - expect: { } -}); - var callGetPending = rpc.declare({ object: 'luci.threat-analyst', method: 'get_pending', @@ -34,12 +40,6 @@ var callChat = rpc.declare({ expect: { } }); -var callAnalyze = rpc.declare({ - object: 'luci.threat-analyst', - method: 'analyze', - expect: { } -}); - var callGenerateRules = rpc.declare({ object: 'luci.threat-analyst', method: 'generate_rules', @@ -67,15 +67,72 @@ var callRunCycle = rpc.declare({ expect: { } }); -return { - status: callStatus, +function formatRelativeTime(dateStr) { + if (!dateStr) return 'N/A'; + try { + var date = new Date(dateStr); + var now = new Date(); + var seconds = Math.floor((now - date) / 1000); + if (seconds < 60) return seconds + 's ago'; + if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago'; + if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago'; + return Math.floor(seconds / 86400) + 'd ago'; + } catch(e) { + return dateStr; + } +} + +function parseScenario(scenario) { + if (!scenario) return 'Unknown'; + var parts = scenario.split('/'); + var name = parts[parts.length - 1]; + return name.split('-').map(function(word) { + return word.charAt(0).toUpperCase() + word.slice(1); + }).join(' '); +} + +function getSeverityClass(scenario) { + if (!scenario) return 'medium'; + var s = scenario.toLowerCase(); + if (s.includes('malware') || s.includes('exploit') || s.includes('cve')) return 'critical'; + if (s.includes('bruteforce') || s.includes('scan')) return 'high'; + if (s.includes('crawl') || s.includes('http')) return 'low'; + return 'medium'; +} + +function extractCVE(scenario) { + if (!scenario) return null; + // Match CVE patterns: CVE-YYYY-NNNNN + var match = scenario.match(/CVE-\d{4}-\d{4,}/i); + return match ? match[0].toUpperCase() : null; +} + +return baseclass.extend({ + getStatus: callStatus, getThreats: callGetThreats, - getAlerts: callGetAlerts, getPending: callGetPending, chat: callChat, - analyze: callAnalyze, generateRules: callGenerateRules, approveRule: callApproveRule, rejectRule: callRejectRule, - runCycle: callRunCycle -}; + runCycle: callRunCycle, + + formatRelativeTime: formatRelativeTime, + parseScenario: parseScenario, + getSeverityClass: getSeverityClass, + extractCVE: extractCVE, + + getOverview: function() { + return Promise.all([ + callStatus(), + callGetThreats(20), + callGetPending() + ]).then(function(results) { + return { + status: results[0] || {}, + threats: (results[1] || {}).threats || [], + pending: (results[2] || {}).pending || [] + }; + }); + } +}); diff --git a/package/secubox/luci-app-threat-analyst/htdocs/luci-static/resources/threat-analyst/dashboard.css b/package/secubox/luci-app-threat-analyst/htdocs/luci-static/resources/threat-analyst/dashboard.css new file mode 100644 index 00000000..92057998 --- /dev/null +++ b/package/secubox/luci-app-threat-analyst/htdocs/luci-static/resources/threat-analyst/dashboard.css @@ -0,0 +1,454 @@ +/* Threat Analyst Dashboard CSS - v0.1.0 */ +/* Following CrowdSec Dashboard KISS pattern */ + +.ta-view { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +/* Header */ +.ta-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border-color-medium, #ddd); +} + +.ta-title { + font-size: 24px; + font-weight: 600; + color: var(--text-color, #333); +} + +.ta-status { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; +} + +.ta-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #ccc; +} + +.ta-dot.online { background: #4caf50; } +.ta-dot.offline { background: #f44336; } + +/* Navigation */ +.ta-nav { + display: flex; + gap: 4px; + margin-bottom: 20px; + background: var(--background-color-alt, #f5f5f5); + padding: 4px; + border-radius: 8px; +} + +.ta-nav a { + padding: 8px 16px; + border-radius: 6px; + text-decoration: none; + color: var(--text-color, #333); + font-size: 14px; + transition: all 0.2s; +} + +.ta-nav a:hover { + background: var(--background-color, #fff); +} + +.ta-nav a.active { + background: var(--primary-color, #2196f3); + color: white; +} + +/* Stats */ +.ta-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 16px; + margin-bottom: 20px; +} + +.ta-stat { + background: var(--background-color, #fff); + border: 1px solid var(--border-color-low, #eee); + border-radius: 8px; + padding: 16px; + text-align: center; +} + +.ta-stat-value { + font-size: 28px; + font-weight: 700; + color: var(--primary-color, #2196f3); +} + +.ta-stat-label { + font-size: 12px; + color: var(--text-color-secondary, #666); + margin-top: 4px; + text-transform: uppercase; +} + +.ta-stat.success .ta-stat-value { color: #4caf50; } +.ta-stat.warning .ta-stat-value { color: #ff9800; } +.ta-stat.danger .ta-stat-value { color: #f44336; } + +/* Grid Layout */ +.ta-grid-2 { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + margin-bottom: 20px; +} + +@media (max-width: 900px) { + .ta-grid-2 { grid-template-columns: 1fr; } +} + +/* Cards */ +.ta-card { + background: var(--background-color, #fff); + border: 1px solid var(--border-color-low, #eee); + border-radius: 8px; + overflow: hidden; +} + +.ta-card-header { + padding: 12px 16px; + font-weight: 600; + font-size: 14px; + background: var(--background-color-alt, #f9f9f9); + border-bottom: 1px solid var(--border-color-low, #eee); +} + +.ta-card-body { + padding: 16px; +} + +/* Tables */ +.ta-table { + width: 100%; + border-collapse: collapse; +} + +.ta-table th, +.ta-table td { + padding: 10px 12px; + text-align: left; + border-bottom: 1px solid var(--border-color-low, #eee); +} + +.ta-table th { + font-size: 11px; + text-transform: uppercase; + color: var(--text-color-secondary, #666); + font-weight: 600; +} + +.ta-table tbody tr:hover { + background: var(--background-color-alt, #f9f9f9); +} + +/* Badges */ +.ta-badge { + display: inline-block; + padding: 3px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 500; +} + +.ta-badge.critical { background: #f44336; color: white; } +.ta-badge.high { background: #ff9800; color: white; } +.ta-badge.medium { background: #ffc107; color: #333; } +.ta-badge.low { background: #4caf50; color: white; } + +.ta-badge.crowdsec { background: #5c6bc0; color: white; } +.ta-badge.mitmproxy { background: #26a69a; color: white; } +.ta-badge.waf { background: #7e57c2; color: white; } + +/* Buttons */ +.ta-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border: none; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: opacity 0.2s; +} + +.ta-btn:hover { opacity: 0.9; } + +.ta-btn-primary { background: var(--primary-color, #2196f3); color: white; } +.ta-btn-success { background: #4caf50; color: white; } +.ta-btn-warning { background: #ff9800; color: white; } +.ta-btn-danger { background: #f44336; color: white; } +.ta-btn-sm { padding: 4px 10px; font-size: 12px; } + +/* Actions Bar */ +.ta-actions { + display: flex; + gap: 8px; + margin-top: 16px; +} + +/* Pending Rules */ +.ta-pending-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.ta-pending-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px; + background: var(--background-color-alt, #f9f9f9); + border-radius: 6px; +} + +.ta-pending-info { + flex: 1; +} + +.ta-pending-type { + font-weight: 600; + margin-bottom: 4px; +} + +.ta-pending-date { + font-size: 12px; + color: var(--text-color-secondary, #666); +} + +.ta-pending-actions { + display: flex; + gap: 6px; +} + +/* Chat */ +.ta-chat { + display: flex; + flex-direction: column; + height: 400px; +} + +.ta-chat-messages { + flex: 1; + overflow-y: auto; + padding: 12px; + background: var(--background-color-alt, #f5f5f5); + border-radius: 6px; + margin-bottom: 12px; +} + +.ta-message { + margin-bottom: 12px; + display: flex; + flex-direction: column; +} + +.ta-message.user { + align-items: flex-end; +} + +.ta-message-bubble { + max-width: 80%; + padding: 10px 14px; + border-radius: 12px; + font-size: 14px; + line-height: 1.4; +} + +.ta-message.user .ta-message-bubble { + background: var(--primary-color, #2196f3); + color: white; + border-bottom-right-radius: 4px; +} + +.ta-message.ai .ta-message-bubble { + background: var(--background-color, #fff); + border: 1px solid var(--border-color-low, #eee); + border-bottom-left-radius: 4px; +} + +.ta-message-time { + font-size: 10px; + color: var(--text-color-secondary, #999); + margin-top: 4px; +} + +.ta-chat-input { + display: flex; + gap: 8px; +} + +.ta-chat-input input { + flex: 1; + padding: 12px; + border: 1px solid var(--border-color-medium, #ddd); + border-radius: 6px; + font-size: 14px; +} + +.ta-chat-input input:focus { + outline: none; + border-color: var(--primary-color, #2196f3); +} + +/* Empty State */ +.ta-empty { + text-align: center; + padding: 24px; + color: var(--text-color-secondary, #666); + font-style: italic; +} + +/* IP display */ +.ta-ip { + font-family: monospace; + font-size: 13px; +} + +/* Scenario display */ +.ta-scenario { + font-size: 13px; +} + +/* Time display */ +.ta-time { + font-size: 12px; + color: var(--text-color-secondary, #666); +} + +/* Health Grid */ +.ta-health { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +.ta-health-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px; + background: var(--background-color-alt, #f9f9f9); + border-radius: 6px; +} + +.ta-health-icon { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + font-size: 14px; +} + +.ta-health-icon.ok { + background: #e8f5e9; + color: #4caf50; +} + +.ta-health-icon.error { + background: #ffebee; + color: #f44336; +} + +.ta-health-icon.warning { + background: #fff3e0; + color: #ff9800; +} + +.ta-health-label { + font-weight: 500; + font-size: 13px; +} + +.ta-health-value { + font-size: 12px; + color: var(--text-color-secondary, #666); +} + +/* Targets Grid */ +.ta-targets { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; +} + +.ta-target { + padding: 16px; + text-align: center; + background: var(--background-color-alt, #f9f9f9); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + border: 2px solid transparent; +} + +.ta-target:hover { + border-color: var(--primary-color, #2196f3); +} + +.ta-target-icon { + font-size: 24px; + margin-bottom: 8px; +} + +.ta-target-name { + font-weight: 600; + font-size: 14px; +} + +.ta-target-desc { + font-size: 11px; + color: var(--text-color-secondary, #666); + margin-top: 4px; +} + +/* CVE Styles */ +.ta-cve-link { + display: inline-block; + padding: 2px 6px; + background: #ffebee; + color: #c62828; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + font-family: monospace; + text-decoration: none; + transition: background 0.2s; +} + +.ta-cve-link:hover { + background: #ef9a9a; + text-decoration: none; +} + +.ta-no-cve { + color: var(--text-color-secondary, #999); +} + +.ta-cve-row { + background: rgba(244, 67, 54, 0.05); +} + +.ta-cve-row:hover { + background: rgba(244, 67, 54, 0.1) !important; +} diff --git a/package/secubox/luci-app-threat-analyst/htdocs/luci-static/resources/view/threat-analyst/dashboard.js b/package/secubox/luci-app-threat-analyst/htdocs/luci-static/resources/view/threat-analyst/dashboard.js index ac8f4fdb..659c34ba 100644 --- a/package/secubox/luci-app-threat-analyst/htdocs/luci-static/resources/view/threat-analyst/dashboard.js +++ b/package/secubox/luci-app-threat-analyst/htdocs/luci-static/resources/view/threat-analyst/dashboard.js @@ -3,188 +3,241 @@ 'require dom'; 'require poll'; 'require ui'; -'require threat-analyst/api as api'; +'require threat-analyst.api as api'; + +/** + * Threat Analyst Dashboard - v0.1.0 + * Generative AI-powered threat filtering + * + * Following CrowdSec Dashboard KISS template pattern + */ return view.extend({ - chatHistory: [], - load: function() { - return Promise.all([ - api.status(), - api.getThreats(20), - api.getPending() - ]); + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = L.resource('threat-analyst/dashboard.css'); + document.head.appendChild(link); + return api.getOverview().catch(function() { return {}; }); }, render: function(data) { - var status = data[0] || {}; - var threats = (data[1] || {}).threats || []; - var pending = (data[2] || {}).pending || []; var self = this; + var s = data.status || {}; + var threats = data.threats || []; + var pending = data.pending || []; - // Add CSS - var style = E('style', {}, ` - .ta-dashboard { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } - .ta-card { background: var(--bg-alt, #f8f9fa); border-radius: 8px; padding: 16px; } - .ta-card h3 { margin: 0 0 12px 0; font-size: 14px; text-transform: uppercase; opacity: 0.7; } - .ta-status-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } - .ta-stat { text-align: center; padding: 12px; background: var(--bg, #fff); border-radius: 6px; } - .ta-stat-value { font-size: 24px; font-weight: bold; color: var(--primary, #2196f3); } - .ta-stat-label { font-size: 11px; opacity: 0.6; margin-top: 4px; } - .ta-stat.warning .ta-stat-value { color: #ff9800; } - .ta-stat.danger .ta-stat-value { color: #f44336; } - .ta-stat.success .ta-stat-value { color: #4caf50; } - - .ta-chat { grid-column: span 2; } - .ta-chat-messages { height: 300px; overflow-y: auto; background: var(--bg, #fff); border-radius: 6px; padding: 12px; margin-bottom: 12px; } - .ta-message { margin-bottom: 12px; } - .ta-message.user { text-align: right; } - .ta-message-bubble { display: inline-block; max-width: 80%; padding: 8px 12px; border-radius: 12px; } - .ta-message.user .ta-message-bubble { background: #2196f3; color: white; } - .ta-message.ai .ta-message-bubble { background: var(--bg-alt, #e3e3e3); } - .ta-message-time { font-size: 10px; opacity: 0.5; margin-top: 4px; } - .ta-chat-input { display: flex; gap: 8px; } - .ta-chat-input input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 6px; } - .ta-chat-input button { padding: 10px 20px; background: #2196f3; color: white; border: none; border-radius: 6px; cursor: pointer; } - - .ta-threats { grid-column: span 2; } - .ta-threats-table { width: 100%; border-collapse: collapse; } - .ta-threats-table th, .ta-threats-table td { padding: 8px; text-align: left; border-bottom: 1px solid #eee; } - .ta-threats-table th { font-size: 11px; text-transform: uppercase; opacity: 0.6; } - .ta-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; } - .ta-badge.critical { background: #f44336; color: white; } - .ta-badge.high { background: #ff9800; color: white; } - .ta-badge.medium { background: #ffc107; color: black; } - .ta-badge.low { background: #4caf50; color: white; } - - .ta-actions { display: flex; gap: 8px; margin-top: 16px; } - .ta-btn { padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; } - .ta-btn-primary { background: #2196f3; color: white; } - .ta-btn-success { background: #4caf50; color: white; } - .ta-btn-warning { background: #ff9800; color: white; } - - .ta-pending { margin-top: 20px; } - .ta-pending-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: var(--bg, #fff); border-radius: 6px; margin-bottom: 8px; } - `); - - var statusCard = E('div', { 'class': 'ta-card' }, [ - E('h3', {}, 'Agent Status'), - E('div', { 'class': 'ta-status-grid' }, [ - E('div', { 'class': 'ta-stat ' + (status.daemon_running ? 'success' : 'warning') }, [ - E('div', { 'class': 'ta-stat-value' }, status.daemon_running ? 'ON' : 'OFF'), - E('div', { 'class': 'ta-stat-label' }, 'Daemon') - ]), - E('div', { 'class': 'ta-stat ' + (status.localai_status === 'online' ? 'success' : 'danger') }, [ - E('div', { 'class': 'ta-stat-value' }, status.localai_status === 'online' ? 'OK' : 'OFF'), - E('div', { 'class': 'ta-stat-label' }, 'LocalAI') - ]), - E('div', { 'class': 'ta-stat ' + (status.recent_threats > 10 ? 'danger' : status.recent_threats > 0 ? 'warning' : 'success') }, [ - E('div', { 'class': 'ta-stat-value' }, status.recent_threats || 0), - E('div', { 'class': 'ta-stat-label' }, 'Threats (1h)') + var view = E('div', { 'class': 'ta-view' }, [ + // Header + E('div', { 'class': 'ta-header' }, [ + E('div', { 'class': 'ta-title' }, 'Threat Analyst'), + E('div', { 'class': 'ta-status' }, [ + E('span', { 'class': 'ta-dot ' + (s.daemon_running ? 'online' : 'offline') }), + s.daemon_running ? 'Running' : 'Stopped' ]) ]), + + // Stats + E('div', { 'class': 'ta-stats', 'id': 'ta-stats' }, this.renderStats(s, pending)), + + // Two column layout + E('div', { 'class': 'ta-grid-2' }, [ + // Health card + E('div', { 'class': 'ta-card' }, [ + E('div', { 'class': 'ta-card-header' }, 'System Health'), + E('div', { 'class': 'ta-card-body' }, this.renderHealth(s)) + ]), + // Pending Rules card + E('div', { 'class': 'ta-card' }, [ + E('div', { 'class': 'ta-card-header' }, 'Pending Rules (' + pending.length + ')'), + E('div', { 'class': 'ta-card-body', 'id': 'ta-pending' }, this.renderPending(pending)) + ]) + ]), + + // Generate Rules card + E('div', { 'class': 'ta-card' }, [ + E('div', { 'class': 'ta-card-header' }, 'Generate Filter Rules'), + E('div', { 'class': 'ta-card-body' }, this.renderTargets()) + ]), + + // Threats card + E('div', { 'class': 'ta-card' }, [ + E('div', { 'class': 'ta-card-header' }, 'Recent Threats from CrowdSec'), + E('div', { 'class': 'ta-card-body', 'id': 'ta-threats' }, this.renderThreats(threats)) + ]), + + // AI Chat card + E('div', { 'class': 'ta-card' }, [ + E('div', { 'class': 'ta-card-header' }, 'AI Security Assistant'), + E('div', { 'class': 'ta-card-body' }, this.renderChat()) + ]) + ]); + + poll.add(L.bind(this.pollData, this), 30); + return view; + }, + + renderStats: function(s, pending) { + var stats = [ + { label: 'Daemon', value: s.daemon_running ? 'ON' : 'OFF', type: s.daemon_running ? 'success' : 'danger' }, + { label: 'LocalAI', value: s.localai_status === 'online' ? 'OK' : 'OFF', type: s.localai_status === 'online' ? 'success' : 'danger' }, + { label: 'Threats (1h)', value: s.recent_threats || 0, type: (s.recent_threats || 0) > 10 ? 'danger' : (s.recent_threats || 0) > 0 ? 'warning' : 'success' }, + { label: 'Pending', value: pending.length || 0, type: (pending.length || 0) > 0 ? 'warning' : '' } + ]; + return stats.map(function(st) { + return E('div', { 'class': 'ta-stat ' + st.type }, [ + E('div', { 'class': 'ta-stat-value' }, String(st.value)), + E('div', { 'class': 'ta-stat-label' }, st.label) + ]); + }); + }, + + renderHealth: function(s) { + var cveCount = s.cve_alerts || 0; + var checks = [ + { label: 'Daemon', ok: s.daemon_running }, + { label: 'LocalAI', ok: s.localai_status === 'online' }, + { label: 'CrowdSec', ok: s.recent_threats !== undefined }, + { label: 'CVE Alerts', ok: cveCount === 0, value: cveCount > 0 ? cveCount + ' Active' : 'None', warn: cveCount > 0 }, + { label: 'Auto-Apply', ok: s.enabled, value: s.enabled ? 'Enabled' : 'Manual' } + ]; + return E('div', { 'class': 'ta-health' }, checks.map(function(c) { + var valueText = c.value ? c.value : (c.ok ? 'OK' : 'Unavailable'); + var iconClass = c.warn ? 'warning' : (c.ok ? 'ok' : 'error'); + var iconChar = c.warn ? '\u26A0' : (c.ok ? '\u2713' : '\u2717'); + return E('div', { 'class': 'ta-health-item' }, [ + E('div', { 'class': 'ta-health-icon ' + iconClass }, iconChar), + E('div', {}, [ + E('div', { 'class': 'ta-health-label' }, c.label), + E('div', { 'class': 'ta-health-value' }, valueText) + ]) + ]); + })); + }, + + renderPending: function(pending) { + var self = this; + if (!pending.length) { + return E('div', { 'class': 'ta-empty' }, 'No pending rules for approval'); + } + return E('div', { 'class': 'ta-pending-list' }, pending.map(function(rule) { + return E('div', { 'class': 'ta-pending-item' }, [ + E('div', { 'class': 'ta-pending-info' }, [ + E('div', { 'class': 'ta-pending-type' }, [ + E('span', { 'class': 'ta-badge ' + rule.type }, rule.type) + ]), + E('div', { 'class': 'ta-pending-date' }, (rule.created || '').substring(0, 10)) + ]), + E('div', { 'class': 'ta-pending-actions' }, [ + E('button', { + 'class': 'ta-btn ta-btn-success ta-btn-sm', + 'click': function() { self.approveRule(rule.id); } + }, 'Approve'), + E('button', { + 'class': 'ta-btn ta-btn-danger ta-btn-sm', + 'click': function() { self.rejectRule(rule.id); } + }, 'Reject') + ]) + ]); + })); + }, + + renderTargets: function() { + var self = this; + var targets = [ + { id: 'crowdsec', name: 'CrowdSec', desc: 'Generate autoban scenarios', icon: '\uD83D\uDEE1' }, + { id: 'mitmproxy', name: 'mitmproxy', desc: 'Generate Python filters', icon: '\uD83D\uDD0D' }, + { id: 'waf', name: 'WAF', desc: 'Generate ModSecurity rules', icon: '\uD83D\uDEA7' } + ]; + return E('div', {}, [ + E('div', { 'class': 'ta-targets' }, targets.map(function(t) { + return E('div', { + 'class': 'ta-target', + 'click': function() { self.generateRules(t.id); } + }, [ + E('div', { 'class': 'ta-target-icon' }, t.icon), + E('div', { 'class': 'ta-target-name' }, t.name), + E('div', { 'class': 'ta-target-desc' }, t.desc) + ]); + })), E('div', { 'class': 'ta-actions' }, [ E('button', { 'class': 'ta-btn ta-btn-primary', - 'click': function() { self.runCycle(); } - }, 'Run Analysis'), + 'click': function() { self.runAnalysis(); } + }, 'Run Analysis Cycle'), E('button', { 'class': 'ta-btn ta-btn-success', 'click': function() { self.generateRules('all'); } - }, 'Generate Rules') + }, 'Generate All Rules') ]) ]); + }, - var pendingCard = E('div', { 'class': 'ta-card' }, [ - E('h3', {}, 'Pending Rules (' + pending.length + ')'), - E('div', { 'class': 'ta-pending', 'id': 'pending-rules' }, - pending.length === 0 ? E('em', {}, 'No pending rules') : - pending.map(function(rule) { - return E('div', { 'class': 'ta-pending-item' }, [ - E('span', {}, rule.type + ' - ' + (rule.created || '').substring(0, 10)), - E('div', {}, [ - E('button', { - 'class': 'ta-btn ta-btn-success', - 'style': 'padding: 4px 8px; margin-right: 4px;', - 'click': function() { self.approveRule(rule.id); } - }, 'Approve'), - E('button', { - 'class': 'ta-btn ta-btn-warning', - 'style': 'padding: 4px 8px;', - 'click': function() { self.rejectRule(rule.id); } - }, 'Reject') - ]) - ]); - }) - ) + renderThreats: function(threats) { + if (!threats.length) { + return E('div', { 'class': 'ta-empty' }, 'No recent threats detected'); + } + return E('table', { 'class': 'ta-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Time'), + E('th', {}, 'Source IP'), + E('th', {}, 'Scenario'), + E('th', {}, 'CVE'), + E('th', {}, 'Severity') + ])), + E('tbody', {}, threats.slice(0, 10).map(function(t) { + var src = t.source || {}; + var severity = api.getSeverityClass(t.scenario); + var cveId = api.extractCVE(t.scenario); + var cveCell = cveId ? + E('a', { + 'class': 'ta-cve-link', + 'href': 'https://nvd.nist.gov/vuln/detail/' + cveId, + 'target': '_blank', + 'rel': 'noopener' + }, cveId) : + E('span', { 'class': 'ta-no-cve' }, '-'); + return E('tr', { 'class': cveId ? 'ta-cve-row' : '' }, [ + E('td', { 'class': 'ta-time' }, api.formatRelativeTime(t.created_at)), + E('td', {}, E('span', { 'class': 'ta-ip' }, src.ip || '-')), + E('td', {}, E('span', { 'class': 'ta-scenario' }, api.parseScenario(t.scenario))), + E('td', {}, cveCell), + E('td', {}, E('span', { 'class': 'ta-badge ' + severity }, severity)) + ]); + })) ]); + }, - var chatCard = E('div', { 'class': 'ta-card ta-chat' }, [ - E('h3', {}, 'AI Security Chat'), - E('div', { 'class': 'ta-chat-messages', 'id': 'chat-messages' }, + renderChat: function() { + var self = this; + return E('div', { 'class': 'ta-chat' }, [ + E('div', { 'class': 'ta-chat-messages', 'id': 'ta-chat-messages' }, [ E('div', { 'class': 'ta-message ai' }, [ - E('div', { 'class': 'ta-message-bubble' }, 'Hello! I\'m your SecuBox Threat Analyst. Ask me about security threats, or request filter rules for mitmproxy, CrowdSec, or WAF.') + E('div', { 'class': 'ta-message-bubble' }, + 'Hello! I\'m your Threat Analyst AI. Ask me about security threats, ' + + 'or request rules for CrowdSec, mitmproxy, or WAF.'), + E('div', { 'class': 'ta-message-time' }, 'System') ]) - ), + ]), E('div', { 'class': 'ta-chat-input' }, [ E('input', { 'type': 'text', - 'id': 'chat-input', - 'placeholder': 'Ask about threats or request rules...', + 'id': 'ta-chat-input', + 'placeholder': 'Ask about threats or request filter rules...', 'keypress': function(e) { if (e.key === 'Enter') self.sendChat(); } }), - E('button', { 'click': function() { self.sendChat(); } }, 'Send') - ]) - ]); - - var threatsCard = E('div', { 'class': 'ta-card ta-threats' }, [ - E('h3', {}, 'Recent Threats'), - E('table', { 'class': 'ta-threats-table' }, [ - E('thead', {}, [ - E('tr', {}, [ - E('th', {}, 'Time'), - E('th', {}, 'Source'), - E('th', {}, 'Scenario'), - E('th', {}, 'IP'), - E('th', {}, 'Severity') - ]) - ]), - E('tbody', { 'id': 'threats-body' }, - threats.slice(0, 10).map(function(t) { - var severity = 'medium'; - if (t.scenario && (t.scenario.includes('malware') || t.scenario.includes('exploit'))) severity = 'critical'; - else if (t.scenario && t.scenario.includes('scan')) severity = 'high'; - else if (t.scenario && t.scenario.includes('http')) severity = 'low'; - - return E('tr', {}, [ - E('td', {}, (t.created_at || '').substring(11, 19)), - E('td', {}, (t.source || {}).ip || '-'), - E('td', {}, t.scenario || '-'), - E('td', {}, (t.source || {}).ip || '-'), - E('td', {}, E('span', { 'class': 'ta-badge ' + severity }, severity)) - ]); - }) - ) - ]) - ]); - - return E('div', {}, [ - style, - E('h2', {}, 'Threat Analyst'), - E('div', { 'class': 'ta-dashboard' }, [ - statusCard, - pendingCard, - chatCard, - threatsCard + E('button', { + 'class': 'ta-btn ta-btn-primary', + 'click': function() { self.sendChat(); } + }, 'Send') ]) ]); }, sendChat: function() { - var input = document.getElementById('chat-input'); - var messages = document.getElementById('chat-messages'); + var input = document.getElementById('ta-chat-input'); + var messages = document.getElementById('ta-chat-messages'); var message = input.value.trim(); - var self = this; if (!message) return; @@ -197,26 +250,24 @@ return view.extend({ input.value = ''; messages.scrollTop = messages.scrollHeight; - // Add loading indicator - var loading = E('div', { 'class': 'ta-message ai', 'id': 'chat-loading' }, [ + // Add loading + var loading = E('div', { 'class': 'ta-message ai', 'id': 'ta-chat-loading' }, [ E('div', { 'class': 'ta-message-bubble' }, 'Analyzing...') ]); messages.appendChild(loading); - // Call API api.chat(message).then(function(result) { - var loadingEl = document.getElementById('chat-loading'); + var loadingEl = document.getElementById('ta-chat-loading'); if (loadingEl) loadingEl.remove(); var response = result.response || result.error || 'No response'; - messages.appendChild(E('div', { 'class': 'ta-message ai' }, [ E('div', { 'class': 'ta-message-bubble' }, response), E('div', { 'class': 'ta-message-time' }, new Date().toLocaleTimeString()) ])); messages.scrollTop = messages.scrollHeight; }).catch(function(err) { - var loadingEl = document.getElementById('chat-loading'); + var loadingEl = document.getElementById('ta-chat-loading'); if (loadingEl) loadingEl.remove(); messages.appendChild(E('div', { 'class': 'ta-message ai' }, [ @@ -225,7 +276,7 @@ return view.extend({ }); }, - runCycle: function() { + runAnalysis: function() { ui.showModal('Running Analysis', [ E('p', { 'class': 'spinning' }, 'Running threat analysis cycle...') ]); @@ -233,31 +284,39 @@ return view.extend({ api.runCycle().then(function() { ui.hideModal(); ui.addNotification(null, E('p', {}, 'Analysis cycle started'), 'success'); + }).catch(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Failed to start analysis'), 'error'); }); }, generateRules: function(target) { + var targetName = target === 'all' ? 'All' : target; ui.showModal('Generating Rules', [ - E('p', { 'class': 'spinning' }, 'Generating ' + target + ' rules with AI...') + E('p', { 'class': 'spinning' }, 'Generating ' + targetName + ' rules with AI...') ]); api.generateRules(target).then(function(result) { ui.hideModal(); if (result.rules) { - ui.addNotification(null, E('p', {}, 'Rules generated. Check pending queue.'), 'success'); + ui.addNotification(null, E('p', {}, 'Rules generated. Check pending queue for approval.'), 'success'); window.location.reload(); } else { - ui.addNotification(null, E('p', {}, 'Failed to generate rules'), 'error'); + ui.addNotification(null, E('p', {}, 'No rules generated'), 'warning'); } + }).catch(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Failed to generate rules'), 'error'); }); }, approveRule: function(id) { - var self = this; api.approveRule(id).then(function(result) { if (result.success) { ui.addNotification(null, E('p', {}, 'Rule approved and applied'), 'success'); window.location.reload(); + } else { + ui.addNotification(null, E('p', {}, 'Failed to approve rule'), 'error'); } }); }, @@ -269,6 +328,24 @@ return view.extend({ }); }, + pollData: function() { + var self = this; + return api.getOverview().then(function(data) { + var s = data.status || {}; + var pending = data.pending || []; + var threats = data.threats || []; + + var el = document.getElementById('ta-stats'); + if (el) dom.content(el, self.renderStats(s, pending)); + + el = document.getElementById('ta-pending'); + if (el) dom.content(el, self.renderPending(pending)); + + el = document.getElementById('ta-threats'); + if (el) dom.content(el, self.renderThreats(threats)); + }); + }, + handleSaveApply: null, handleSave: null, handleReset: null diff --git a/package/secubox/luci-app-threat-analyst/root/usr/libexec/rpcd/luci.threat-analyst b/package/secubox/luci-app-threat-analyst/root/usr/libexec/rpcd/luci.threat-analyst index ca3019fb..9a24875b 100644 --- a/package/secubox/luci-app-threat-analyst/root/usr/libexec/rpcd/luci.threat-analyst +++ b/package/secubox/luci-app-threat-analyst/root/usr/libexec/rpcd/luci.threat-analyst @@ -61,10 +61,16 @@ EOF [ -f "$STATE_DIR/pending_rules.json" ] && \ pending_count=$(jsonfilter -i "$STATE_DIR/pending_rules.json" -e '@[*]' 2>/dev/null | wc -l) - # Count recent threats + # Count recent threats and CVE alerts threat_count=0 - command -v cscli >/dev/null 2>&1 && \ - threat_count=$(cscli alerts list -o json --since 1h 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l) + cve_count=0 + if command -v cscli >/dev/null 2>&1; then + alerts_json=$(cscli alerts list -o json --since 1h 2>/dev/null) + threat_count=$(echo "$alerts_json" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) + # Count CVE-related alerts + cve_count=$(echo "$alerts_json" | grep -ic 'cve-' 2>/dev/null) + [ -z "$cve_count" ] && cve_count=0 + fi cat <