From 59dbd714a52ef547285192c171be5655d34c3298 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Tue, 3 Mar 2026 09:44:04 +0100 Subject: [PATCH] fix(tools): Add curl redirect handling to image builder scripts Validated secubox-image.sh and secubox-sysupgrade.sh scripts: - Fixed curl redirect issue: ASU API returns 301 redirects - Added -L flag to 9 curl calls across both scripts - Verified all device profiles valid (mochabin, espressobin, x86-64) - Confirmed POSIX sh compatibility for sysupgrade script - Validated first-boot script syntax Co-Authored-By: Claude Opus 4.5 --- .claude/HISTORY.md | 200 ++++- .claude/TODO.md | 19 +- .claude/WIP.md | 62 +- .claude/settings.local.json | 8 +- package/secubox/luci-app-ai-gateway/Makefile | 21 + .../resources/view/ai-gateway/audit.js | 241 ++++++ .../resources/view/ai-gateway/classify.js | 254 +++++++ .../resources/view/ai-gateway/overview.js | 359 +++++++++ .../resources/view/ai-gateway/providers.js | 295 ++++++++ .../luci/menu.d/luci-app-ai-gateway.json | 44 ++ .../share/rpcd/acl.d/luci-app-ai-gateway.json | 28 + .../view/vortex-firewall/dnsguard.js | 253 +++++++ .../resources/view/vortex-firewall/mesh.js | 306 ++++++++ .../view/vortex-firewall/sinkhole.js | 368 ++++++++++ .../luci/menu.d/luci-app-vortex-firewall.json | 35 +- .../rpcd/acl.d/luci-app-vortex-firewall.json | 19 +- .../view/wireguard-dashboard/uplinks.js | 613 ++++++++++++++++ .../resources/wireguard-dashboard/api.js | 71 ++ .../menu.d/luci-app-wireguard-dashboard.json | 8 + .../root/usr/lib/secubox/threat-intel.sh | 113 ++- .../secubox/secubox-vortex-firewall/Makefile | 12 +- .../root/etc/init.d/vortex-firewall | 14 +- .../vortex-firewall/sinkhole-http-handler.sh | 145 ++++ .../usr/lib/vortex-firewall/sinkhole-http.sh | 151 ++++ .../usr/lib/vortex-firewall/sinkhole-https.sh | 75 ++ .../usr/libexec/rpcd/luci.vortex-firewall | 502 ++++++++++++- .../root/usr/sbin/vortex-firewall | 694 +++++++++++++++++- .../rpcd/acl.d/luci-vortex-firewall.json | 27 +- secubox-tools/secubox-image.sh | 10 +- secubox-tools/secubox-sysupgrade.sh | 8 +- 30 files changed, 4892 insertions(+), 63 deletions(-) create mode 100644 package/secubox/luci-app-ai-gateway/Makefile create mode 100644 package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/audit.js create mode 100644 package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/classify.js create mode 100644 package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/overview.js create mode 100644 package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/providers.js create mode 100644 package/secubox/luci-app-ai-gateway/root/usr/share/luci/menu.d/luci-app-ai-gateway.json create mode 100644 package/secubox/luci-app-ai-gateway/root/usr/share/rpcd/acl.d/luci-app-ai-gateway.json create mode 100644 package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/dnsguard.js create mode 100644 package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/mesh.js create mode 100644 package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/sinkhole.js create mode 100644 package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/uplinks.js create mode 100644 package/secubox/secubox-vortex-firewall/root/usr/lib/vortex-firewall/sinkhole-http-handler.sh create mode 100644 package/secubox/secubox-vortex-firewall/root/usr/lib/vortex-firewall/sinkhole-http.sh create mode 100644 package/secubox/secubox-vortex-firewall/root/usr/lib/vortex-firewall/sinkhole-https.sh diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 1833a957..64102e3c 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -1,6 +1,6 @@ # SecuBox UI & Theme History -_Last updated: 2026-02-28 (AI Gateway Deployed)_ +_Last updated: 2026-03-03 (Vortex Sinkhole Server)_ 1. **Unified Dashboard Refresh (2025-12-20)** - Dashboard received the "sh-page-header" layout, hero stats, and SecuNav top tabs. @@ -4106,3 +4106,201 @@ git checkout HEAD -- index.html - Fixed mitmproxy routing for matrix.gk2.secubox.in and alerte.gk2.secubox.in - Identified corrupted c3box-vm images from Feb 23 - need rebuild - ASU firmware builder working with MochaBin preseeds embedded + +62. **Reverse MWAN WireGuard v2 - Phase 2 (2026-03-02)** + - **LuCI Dashboard for Mesh Uplinks:** + - New "Mesh Uplinks" tab in WireGuard Dashboard (`uplinks.js`) + - Status cards: Uplink Status, Active Uplinks count, Mesh Offers, Provider Mode + - Quick actions: Offer Uplink, Withdraw Uplink, Toggle Auto-Failover + - **Active Uplinks Table:** + - Interface, Peer, Endpoint, Priority/Weight columns + - Status badges (active/testing/unknown) + - Actions: Test connectivity, Set priority, Remove uplink + - **Peer Offers Grid:** + - Card-based display of available mesh uplink offers + - Shows node ID, bandwidth (Mbps), latency (ms), public key + - "Use as Uplink" button to add peer as backup route + - **API Additions (`api.js`):** + - `getUplinkStatus`, `getUplinks` - Status retrieval + - `addUplink`, `removeUplink` - Uplink management + - `testUplink` - Connectivity testing + - `offerUplink`, `withdrawUplink` - Provider mode + - `setUplinkPriority`, `setUplinkFailover` - Configuration + - **Menu Entry:** Added "Mesh Uplinks" at order 45 (after Traffic Stats) + - **10-second polling** for live status updates + - **Help section** explaining mesh uplink architecture + - Completes Reverse MWAN WireGuard v2 feature + +63. **AI Gateway LuCI Dashboard (2026-03-03)** + - **Created `luci-app-ai-gateway` package** for Data Sovereignty Engine web interface + - **4 Views with KISS Theme:** + - **Overview:** Status cards (port, providers, requests), classification tier legend, provider hierarchy grid, audit statistics, service controls (start/stop/restart), offline mode toggle + - **Providers:** 6 provider cards (LocalAI, Mistral, Claude, OpenAI, Gemini, xAI), enable/disable toggles, API key management, test connectivity buttons, tier badges (LOCAL/EU/CLOUD) + - **Classifier:** Interactive classification testing tool, example inputs with expected tiers, real-time classification with pattern matching display, destination routing explanation + - **Audit Log:** ANSSI CSPN compliance audit viewer, classification distribution chart, stats grid (LOCAL_ONLY/SANITIZED/CLOUD_DIRECT), JSONL log viewer with color-coded entries + - **Menu Structure:** Admin > Services > AI Gateway with 4 tabs + - **ACL Permissions:** Read methods (status, config, providers, audit, classify) and write methods (set_provider, offline_mode, test, start/stop/restart) + - **Dark Mode Support:** Full dark theme compatibility across all views + - **Live Polling:** 10-30 second auto-refresh for status and audit stats + - **ANSSI CSPN Emphasis:** Information boxes explaining data sovereignty compliance + - Completes AI Gateway full-stack implementation (backend + LuCI) + +64. **Vortex DNS Firewall Phase 2 - Sinkhole Server (2026-03-03)** + - **Sinkhole HTTP/HTTPS server** for capturing blocked domain connections + - **Architecture:** + - HTTP handler (`sinkhole-http-handler.sh`) via socat TCP listener + - HTTPS support with OpenSSL TLS termination + - Extracts domain from Host header + - Records events to SQLite database + - Returns warning page with block details + - **Warning Page Features:** + - Modern responsive design with dark gradient theme + - Displays: blocked domain, threat type, client IP, timestamp + - Explains why connection was blocked + - SecuBox branding + - **CLI Commands:** + - `sinkhole start/stop/status` - Server management + - `sinkhole logs [N]` - View last N events + - `sinkhole export [file]` - Export events to JSON + - `sinkhole gencert` - Generate self-signed HTTPS certificate + - `sinkhole clear` - Clear event log + - **RPCD Methods (5 new):** + - `sinkhole_status` - Server status and event statistics + - `sinkhole_events` - Retrieve captured events + - `sinkhole_stats` - Top clients, top domains, event types + - `sinkhole_toggle` - Enable/disable sinkhole server + - `sinkhole_clear` - Clear event database + - **LuCI Sinkhole Dashboard:** + - Status card with toggle switch for enable/disable + - Stats cards: total events, today's events, infected clients, unique domains + - Top infected clients table with activity bars + - Top blocked domains table + - Event log viewer with clear function + - 15-second polling for live updates + - **Infected Client Detection:** + - Clients attempting blocked domain connections are flagged + - SOC visibility into compromised devices + - Malware behavior analysis capability + - **Dependencies added:** socat, openssl-util + - Transforms Vortex from passive blocker to active threat analyzer + +65. **Vortex DNS Firewall Phase 3 - DNS Guard Integration (2026-03-03)** + - Integrated DNS Guard AI detection engine with Vortex Firewall. + - **Enhanced Import with Metadata:** + - Reads alerts.json with full detection context (type, confidence, reason) + - Maps DNS Guard types: dga, tunneling, known_bad, tld_anomaly, rate_anomaly + - Preserves confidence scores in blocklist database + - Fallback to basic import from threat_domains.txt + - **CLI Commands (4 new):** + - `dnsguard status` - Show DNS Guard service and integration health + - `dnsguard sync` - Force sync detections from DNS Guard + - `dnsguard export` - Push Vortex intel back to DNS Guard blocklists + - `dnsguard alerts [N]` - View recent DNS Guard alerts + - **Bidirectional Feed:** + - Vortex imports DNS Guard detections automatically + - Vortex can export threat intel back to DNS Guard blocklists + - Enables unified threat database across both systems + - **RPCD Methods (3 new):** + - `dnsguard_status` - Service status, alert/pending counts, detection breakdown + - `dnsguard_alerts` - Retrieve recent alerts with metadata + - `dnsguard_sync` - Trigger sync from DNS Guard + - **LuCI DNS Guard Dashboard:** + - Service status card (running/stopped/not installed) + - Stats cards: alert count, pending approvals, imported to Vortex + - Detection types breakdown with colored badges + - Sync button with last sync timestamp + - Recent alerts table with confidence bars + - Phase 3 completes the integration between DNS Guard (AI detection) and Vortex Firewall (DNS blocking). + +66. **Vortex DNS Firewall Phase 4 - Mesh Threat Sharing (2026-03-03)** + - Integrated Vortex Firewall with secubox-p2p threat intelligence system. + - **Domain IOC Support:** + - Extended threat-intel.sh to support domain-based IOCs (not just IPs) + - Added `ti_collect_vortex()` function to extract high-confidence domains + - Domain IOCs applied to Vortex Firewall blocklist on receipt + - **CLI Commands (5 new):** + - `mesh status` - Show mesh threat sharing status + - `mesh publish` - Publish local domains to mesh + - `mesh sync` - Sync and apply threats from mesh + - `mesh received [N]` - Show threats received from mesh + - `mesh peers` - Show peer contribution statistics + - **RPCD Methods (5 new):** + - `mesh_status` - Mesh sharing status and stats + - `mesh_received` - List received IOCs with trust scores + - `mesh_publish` - Trigger publish operation + - `mesh_sync` - Trigger sync and apply + - `mesh_peers` - Peer contribution data + - **LuCI Mesh Dashboard:** + - Status cards: local/received/applied IOCs, domains shared, peers + - Publish and Sync action buttons + - Peer contributors grid with trust badges + - Received threats table with severity/trust/status + - **Trust Model Integration:** + - Direct peers: Full trust, apply all threats + - Transitive peers: Apply high severity only + - Unknown: Skip (logged for review) + - **Collection Criteria:** + - Domains with confidence >= 85% + - Domains with hit_count > 0 (locally verified) + - Excludes private/local domains + - Completes the Vortex DNS Firewall 4-phase implementation. + +67. **Vortex Sinkhole Server Fix (2026-03-03)** + - Fixed sinkhole server startup issues discovered via LuCI dashboard screenshot. + - **HAProxy Bind Configuration:** + - Changed HAProxy from wildcard `*:80`/`*:443` to specific IP `192.168.255.1:80`/`192.168.255.1:443` + - Allows sinkhole to bind to dedicated IP `192.168.255.253:80`/`192.168.255.253:443` + - **Missing Scripts Deploy:** + - Created `/usr/lib/vortex-firewall/` directory on router + - Deployed sinkhole-http.sh, sinkhole-http-handler.sh, sinkhole-https.sh + - **Process Detection Fix:** + - Changed pgrep patterns from `vortex-sinkhole-http` to `sinkhole-http-handler` + - HTTPS detection updated to check PID file + SSL backend availability + - **HTTPS Server Limitation:** + - Socat package compiled without SSL support on this router + - HTTPS sinkhole now shows "Limited (no SSL)" status when full SSL unavailable + - Added `https_limited` field to RPCD response + - Updated LuCI view to show warning color for limited mode + - **Final Status:** + - HTTP Server: Running (full functionality) + - HTTPS Server: Limited mode (blocked HTTPS domains show browser cert warning) + +68. **WAF Auto-Ban Tuning (2026-03-03)** + - Identified false positive pattern: Amazonbot (legitimate crawler) being banned for "waf_bypass" + - **Root cause**: Gitea URL parameters (`whitespace=ignore-xxx`, `display=source`) incorrectly triggering WAF bypass detection + - **Autoban configuration tuning:** + - Added Amazon, OpenAI, Meta to `whitelist_bots` (previously only Facebook, Google, Bing, Twitter, LinkedIn) + - Changed sensitivity from `strict` to `moderate` + - Increased moderate threshold from 3 to 5 attempts + - Extended moderate window from 300s to 600s (10 minutes) + - **CrowdSec scenario tuning:** + - Updated `secubox-mitmproxy-waf-bypass.yaml`: + - Added filter `evt.Parsed.is_bot != 'true'` to skip known bots + - Increased capacity from 5 to 10 + - Extended leakspeed from 60s to 120s + - Reduced blackhole from 30m to 15m + - **Cleared incorrectly banned IPs:** Removed all waf_bypass decisions + - **Result:** Legitimate crawlers (Amazon, Meta, OpenAI) no longer banned for normal Gitea browsing + +69. **Image Builder Validation (2026-03-03)** + - Validated `secubox-tools/secubox-image.sh` and `secubox-sysupgrade.sh` scripts + - **Syntax validation:** + - `secubox-image.sh`: Bash syntax OK + - `secubox-sysupgrade.sh`: POSIX sh compatible (uses jsonfilter, not jq) + - `resize-openwrt-image.sh`: Bash syntax OK + - **ASU API testing:** + - Verified API connectivity to sysupgrade.openwrt.org + - Confirmed all device profiles are valid: + - `globalscale_mochabin` (mvebu/cortexa72) ✓ + - `globalscale_espressobin` (mvebu/cortexa53) ✓ + - `generic` (x86/64) ✓ + - Successfully queued test builds for all profiles + - **Bug fix - Curl redirect handling:** + - ASU API returns 301 redirects for some endpoints + - Added `-L` flag to all curl calls in both scripts + - Fixed: `secubox-image.sh` (5 curl calls) + - Fixed: `secubox-sysupgrade.sh` (4 curl calls) + - **First-boot script validation:** + - Extracted and validated shell syntax + - 63 lines, 7 opkg calls, 10 log statements + - **Tools available:** All required tools (gunzip, gzip, fdisk, sfdisk, parted, e2fsck, resize2fs, losetup, blkid, truncate) present diff --git a/.claude/TODO.md b/.claude/TODO.md index 9f35acef..fae717fa 100644 --- a/.claude/TODO.md +++ b/.claude/TODO.md @@ -1,6 +1,6 @@ # SecuBox TODOs (Claude Edition) -_Last updated: 2026-02-06_ +_Last updated: 2026-03-03_ > **Architecture Reference**: SecuBox Fanzine v3 — Les 4 Couches @@ -45,12 +45,19 @@ _Last updated: 2026-02-06_ - Threat IOC propagation tested (116 blocks synced) - Automatic SSH-based mesh sync configured (5-min cron) -2. **WAF Auto-Ban Tuning** - - Sensitivity thresholds may need adjustment based on real traffic patterns. - - CVE detection patterns (including CVE-2025-15467) need false-positive analysis. +2. **WAF Auto-Ban Tuning** — DONE (2026-03-03) + - ~~Sensitivity thresholds may need adjustment based on real traffic patterns.~~ + - ~~CVE detection patterns (including CVE-2025-15467) need false-positive analysis.~~ + - Added Amazon/OpenAI/Meta to bot whitelist + - Changed sensitivity from strict to moderate (5 attempts in 600s) + - Updated waf_bypass scenario to skip known bots -3. **Image Builder Validation** - - `secubox-tools/` image builder and sysupgrade scripts need testing on physical hardware. +3. **Image Builder Validation** — DONE (2026-03-03) + - ~~`secubox-tools/` image builder and sysupgrade scripts need testing on physical hardware.~~ + - Syntax validation passed for all scripts (bash/POSIX sh) + - ASU API connectivity verified, all device profiles valid + - Fixed curl redirect handling (added `-L` flag to 9 curl calls) + - First-boot script validated for correct package installation ### Innovation CVE Layer 7 diff --git a/.claude/WIP.md b/.claude/WIP.md index 8aebe369..9af90bd0 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -1,6 +1,6 @@ # Work In Progress (Claude) -_Last updated: 2026-03-01 (Reverse MWAN WireGuard Phase 1)_ +_Last updated: 2026-03-03 (Image Builder Validation)_ > **Architecture Reference**: SecuBox Fanzine v3 — Les 4 Couches @@ -43,16 +43,18 @@ _Last updated: 2026-03-01 (Reverse MWAN WireGuard Phase 1)_ ### In Progress -- **Vortex DNS Firewall Phase 1** — DONE (2026-02-11) +- **Vortex DNS Firewall Phases 1-4** — DONE (2026-03-03) - Created `secubox-vortex-firewall` package for DNS-level threat blocking - Threat intel aggregator (URLhaus, OpenPhish, Malware Domains feeds) - SQLite blocklist database with domain deduplication - dnsmasq integration via sinkhole hosts file - ×47 vitality multiplier concept - - CLI tool: `vortex-firewall intel/stats/start/stop` - - RPCD handler with 8 methods for LuCI integration - - Tested: 765 domains blocked from 3 feeds - - **Next phases**: Sinkhole server (Phase 2), DNS Guard integration (Phase 3), Mesh threat sharing (Phase 4), LuCI dashboard (Phase 5) + - CLI tool: `vortex-firewall intel/stats/start/stop/sinkhole/dnsguard/mesh` + - RPCD handler with 21 methods for LuCI integration + - Phase 2: HTTP/HTTPS sinkhole server for infected client detection + - Phase 3: DNS Guard AI detection integration with metadata import + - Phase 4: Mesh threat sharing via secubox-p2p blockchain + - LuCI dashboard with Overview, Sinkhole, DNS Guard, and Mesh tabs - **Vortex DNS** - Meshed multi-dynamic subdomain delegation (DONE 2026-02-05) - Created `secubox-vortex-dns` package with `vortexctl` CLI @@ -62,6 +64,51 @@ _Last updated: 2026-03-01 (Reverse MWAN WireGuard Phase 1)_ - Gossip-based exposure config sync via secubox-p2p - Created `luci-app-vortex-dns` dashboard +### Just Completed (2026-03-03) + +- **Vortex DNS Firewall Phase 3 - DNS Guard Integration** — DONE (2026-03-03) + - Integrated DNS Guard AI detection engine with Vortex Firewall + - Enhanced import with metadata (type, confidence, reason) from alerts.json + - CLI: `dnsguard status/sync/export/alerts` + - RPCD: 3 new methods (dnsguard_status/alerts/sync) + - LuCI DNS Guard Dashboard: status, detection types, alerts table + - Bidirectional feed: Vortex imports DNS Guard, can export back + +- **Vortex DNS Firewall Phase 2 - Sinkhole Server** — DONE (2026-03-03) + - HTTP/HTTPS sinkhole captures blocked domain connections + - Warning page with threat type, client IP, domain, timestamp + - CLI: `sinkhole start/stop/status/logs/export/gencert/clear` + - RPCD: 5 new methods (sinkhole_status/events/stats/toggle/clear) + - LuCI Sinkhole Dashboard: infected clients table, event log, toggle + - Transforms Vortex from passive blocker to active threat analyzer + +- **AI Gateway LuCI Dashboard** — DONE (2026-03-03) + - Created `luci-app-ai-gateway` package with 4 KISS-themed views + - Overview: Status cards, provider grid, classification legend, audit stats + - Providers: API key management, enable/disable toggles, test buttons + - Classifier: Interactive testing tool with example inputs + - Audit Log: ANSSI CSPN compliance viewer with distribution chart + - Completes AI Gateway full-stack implementation + +- **Image Builder Validation** — DONE (2026-03-03) + - Validated `secubox-image.sh`, `secubox-sysupgrade.sh`, `resize-openwrt-image.sh` + - Confirmed all device profiles valid (mochabin, espressobin, x86-64) + - Fixed curl redirect issue: Added `-L` flag to 9 curl calls + - First-boot script validated for correct shell syntax + - ASU API connectivity tested successfully + +### Just Completed (2026-03-02) + +- **Reverse MWAN WireGuard v2 - Phase 2** — DONE (2026-03-02) + - LuCI Dashboard for Mesh Uplinks (`uplinks.js`) + - Status cards: Uplink Status, Active Uplinks, Mesh Offers, Provider Mode + - Active Uplinks table with test/priority/remove actions + - Peer Offers grid with "Use as Uplink" button + - API additions: 9 RPC methods for uplink management + - Menu entry: "Mesh Uplinks" tab in WireGuard Dashboard + - 10-second live polling for status updates + - Completes full Reverse MWAN WireGuard v2 feature + ### Just Completed (2026-03-01) - **Reverse MWAN WireGuard v2 - Phase 1** — DONE (2026-03-01) @@ -70,7 +117,6 @@ _Last updated: 2026-03-01 (Reverse MWAN WireGuard Phase 1)_ - Uplink library (`/usr/lib/wireguard-dashboard/uplink.sh`) with gossip integration - RPCD backend: 9 new methods for uplink management - UCI config (`/etc/config/wireguard_uplink`) for global and per-uplink settings - - Phase 2 pending: LuCI dashboard integration - **Nextcloud Integration Enhancements** — DONE (2026-03-01) - WAF-safe SSL routing via mitmproxy_inspector @@ -1186,7 +1232,7 @@ Implementing 3 evolutions inspired by SysWarden patterns: - ~~Tor Shield / opkg bug~~ — FIXED (2026-02-28) - dnsmasq bypass for excluded domains - ~~Nextcloud self-hosted cloud storage (v2)~~ — ENHANCED (2026-03-01) - WAF-safe SSL, scheduled backups, email, connections - SSMTP / mail host / MX record management (v2) -- ~~Reverse MWAN WireGuard peers (v2)~~ — Phase 1 DONE (2026-03-01) - CLI + library + RPCD; Phase 2 (LuCI) pending +- ~~Reverse MWAN WireGuard peers (v2)~~ — COMPLETE (2026-03-02) - CLI, library, RPCD, LuCI dashboard --- diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 345a5ea9..bcdee8ff 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -500,7 +500,13 @@ "WebFetch(domain:matrix.gk2.secubox.in)", "Bash(# Stop the failed VM VBoxManage controlvm ''C3Box-SecuBox'' poweroff || true # Check the c3box-vm-builder for the proper build method grep -A20 \"\"build_firmware\"\" /home/reepost/CyberMindStudio/secubox-openwrt/secubox-tools/c3box-vm-builder.sh)", "Bash(sudo umount:*)", - "Bash(__NEW_LINE_c35a46b8074eb5e8__ sudo losetup -d \"$LOOP_DEV\")" + "Bash(__NEW_LINE_c35a46b8074eb5e8__ sudo losetup -d \"$LOOP_DEV\")", + "Bash(# Test Matrix registration flow curl -s -X POST \"\"https://matrix.gk2.secubox.in/_matrix/client/v3/register\"\" \\\\ -H \"\"Content-Type: application/json\"\" \\\\ -d ''{\"\"username\"\":\"\"testuser123\"\",\"\"password\"\":\"\"TestPass123!\"\",\"\"auth\"\":{\"\"type\"\":\"\"m.login.dummy\"\"}}'')", + "Bash(# Step 1: Get registration flows echo \"\"=== Registration Flows ===\"\" curl -s -X POST \"\"https://matrix.gk2.secubox.in/_matrix/client/v3/register\"\" \\\\ -H \"\"Content-Type: application/json\"\" \\\\ -d ''{}'')", + "Bash(# Test registration again echo \"\"=== Registration Flows ===\"\" curl -s -X POST \"\"https://matrix.gk2.secubox.in/_matrix/client/v3/register\"\" \\\\ -H \"\"Content-Type: application/json\"\" \\\\ -d ''{}'')", + "Bash(# Test registration now echo \"\"=== Registration Flows ===\"\" curl -s -X POST \"\"https://matrix.gk2.secubox.in/_matrix/client/v3/register\"\" \\\\ -H \"\"Content-Type: application/json\"\" \\\\ -d ''{}'')", + "Bash(# Final registration test curl -s -X POST \"\"https://matrix.gk2.secubox.in/_matrix/client/v3/register\"\" \\\\ -H \"\"Content-Type: application/json\"\" \\\\ -d ''{}'')", + "WebFetch(domain:pf.gk2.secubox.in)" ] } } diff --git a/package/secubox/luci-app-ai-gateway/Makefile b/package/secubox/luci-app-ai-gateway/Makefile new file mode 100644 index 00000000..a202d94d --- /dev/null +++ b/package/secubox/luci-app-ai-gateway/Makefile @@ -0,0 +1,21 @@ +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI AI Gateway Dashboard +LUCI_DESCRIPTION:=Data Sovereignty Engine with multi-tier classification for ANSSI CSPN compliance +LUCI_DEPENDS:=+luci-base +secubox-ai-gateway +LUCI_PKGARCH:=all +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-ai-gateway/install + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/ai-gateway + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/ai-gateway/*.js $(1)/www/luci-static/resources/view/ai-gateway/ + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/ + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/ +endef + +$(eval $(call BuildPackage,luci-app-ai-gateway)) diff --git a/package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/audit.js b/package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/audit.js new file mode 100644 index 00000000..7d52847b --- /dev/null +++ b/package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/audit.js @@ -0,0 +1,241 @@ +'use strict'; +'require view'; +'require rpc'; +'require poll'; +'require fs'; + +var callGetAuditStats = rpc.declare({ + object: 'luci.ai-gateway', + method: 'get_audit_stats', + expect: {} +}); + +var kissCSS = ` + .audit-container { padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } + .audit-container h2 { margin: 0 0 8px 0; } + .audit-container .subtitle { color: var(--text-secondary, #64748b); margin-bottom: 24px; } + + .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 24px; } + .stat-card { background: var(--bg-secondary, #f8fafc); border: 1px solid var(--border-color, #e2e8f0); border-radius: 12px; padding: 20px; text-align: center; } + .stat-card .value { font-size: 2em; font-weight: 700; } + .stat-card .label { color: var(--text-secondary, #64748b); font-size: 0.9em; margin-top: 4px; } + .stat-local { color: #22c55e; } + .stat-sanitized { color: #f59e0b; } + .stat-cloud { color: #3b82f6; } + + .chart-section { margin-bottom: 24px; } + .chart-section h3 { margin-bottom: 16px; font-size: 1.1em; } + .chart-bar { display: flex; height: 32px; border-radius: 8px; overflow: hidden; background: #e2e8f0; } + .chart-bar .segment { display: flex; align-items: center; justify-content: center; color: white; font-weight: 500; font-size: 0.85em; transition: width 0.3s; } + .segment-local { background: #22c55e; } + .segment-sanitized { background: #f59e0b; } + .segment-cloud { background: #3b82f6; } + .chart-legend { display: flex; gap: 24px; margin-top: 12px; } + .legend-item { display: flex; align-items: center; gap: 8px; font-size: 0.9em; } + .legend-dot { width: 12px; height: 12px; border-radius: 50%; } + + .info-box { padding: 16px 20px; background: #f0fdf4; border: 1px solid #86efac; border-radius: 8px; margin-bottom: 24px; } + .info-box h4 { margin: 0 0 8px 0; color: #166534; } + .info-box p { margin: 0; color: #14532d; font-size: 0.9em; } + .info-box code { background: #dcfce7; padding: 2px 6px; border-radius: 4px; font-size: 0.85em; } + + .log-section { margin-top: 24px; } + .log-section h3 { margin-bottom: 12px; } + .log-info { color: var(--text-secondary, #64748b); font-size: 0.9em; margin-bottom: 12px; } + .log-viewer { background: #1e293b; border-radius: 8px; padding: 16px; font-family: monospace; font-size: 0.85em; color: #e2e8f0; max-height: 400px; overflow-y: auto; } + .log-line { padding: 4px 0; border-bottom: 1px solid #334155; } + .log-line:last-child { border-bottom: none; } + .log-time { color: #64748b; } + .log-local { color: #4ade80; } + .log-sanitized { color: #fbbf24; } + .log-cloud { color: #60a5fa; } + + .btn { padding: 8px 16px; border-radius: 6px; font-weight: 500; cursor: pointer; border: none; } + .btn-secondary { background: #64748b; color: white; } + .btn-secondary:hover { background: #475569; } + + @media (prefers-color-scheme: dark) { + .stat-card { background: #1e293b; border-color: #334155; } + .info-box { background: #14532d; border-color: #22c55e; } + .info-box h4, .info-box p { color: #bbf7d0; } + .info-box code { background: #166534; color: #dcfce7; } + } +`; + +return view.extend({ + title: 'Audit Log', + + load: function() { + return Promise.all([ + callGetAuditStats(), + fs.read('/var/log/ai-gateway-audit.jsonl').catch(function() { return ''; }) + ]); + }, + + render: function(data) { + var stats = data[0].result || data[0] || {}; + var logContent = data[1] || ''; + + var container = E('div', { 'class': 'audit-container' }); + container.appendChild(E('style', {}, kissCSS)); + + container.appendChild(E('h2', {}, 'Audit Log')); + container.appendChild(E('p', { 'class': 'subtitle' }, + 'ANSSI CSPN compliance audit trail. All AI Gateway classification decisions are logged.')); + + // ANSSI Info Box + container.appendChild(E('div', { 'class': 'info-box' }, [ + E('h4', {}, 'ANSSI CSPN Compliance'), + E('p', {}, [ + 'Audit logs are stored at ', + E('code', {}, '/var/log/ai-gateway-audit.jsonl'), + ' in JSON Lines format. Export for compliance review with: ', + E('code', {}, 'aigatewayctl audit export') + ]) + ])); + + // Stats Grid + var localOnly = stats.local_only || 0; + var sanitized = stats.sanitized || 0; + var cloudDirect = stats.cloud_direct || 0; + var total = localOnly + sanitized + cloudDirect; + + var statsGrid = E('div', { 'class': 'stats-grid' }); + + statsGrid.appendChild(E('div', { 'class': 'stat-card' }, [ + E('div', { 'class': 'value' }, String(total)), + E('div', { 'class': 'label' }, 'Total Requests') + ])); + + statsGrid.appendChild(E('div', { 'class': 'stat-card' }, [ + E('div', { 'class': 'value stat-local' }, String(localOnly)), + E('div', { 'class': 'label' }, 'LOCAL_ONLY') + ])); + + statsGrid.appendChild(E('div', { 'class': 'stat-card' }, [ + E('div', { 'class': 'value stat-sanitized' }, String(sanitized)), + E('div', { 'class': 'label' }, 'SANITIZED') + ])); + + statsGrid.appendChild(E('div', { 'class': 'stat-card' }, [ + E('div', { 'class': 'value stat-cloud' }, String(cloudDirect)), + E('div', { 'class': 'label' }, 'CLOUD_DIRECT') + ])); + + container.appendChild(statsGrid); + + // Distribution Chart + if (total > 0) { + var chartSection = E('div', { 'class': 'chart-section' }); + chartSection.appendChild(E('h3', {}, 'Classification Distribution')); + + var localPct = Math.round((localOnly / total) * 100); + var sanitizedPct = Math.round((sanitized / total) * 100); + var cloudPct = 100 - localPct - sanitizedPct; + + var chartBar = E('div', { 'class': 'chart-bar' }); + + if (localPct > 0) { + chartBar.appendChild(E('div', { + 'class': 'segment segment-local', + 'style': 'width: ' + localPct + '%;' + }, localPct + '%')); + } + + if (sanitizedPct > 0) { + chartBar.appendChild(E('div', { + 'class': 'segment segment-sanitized', + 'style': 'width: ' + sanitizedPct + '%;' + }, sanitizedPct + '%')); + } + + if (cloudPct > 0) { + chartBar.appendChild(E('div', { + 'class': 'segment segment-cloud', + 'style': 'width: ' + cloudPct + '%;' + }, cloudPct + '%')); + } + + chartSection.appendChild(chartBar); + + chartSection.appendChild(E('div', { 'class': 'chart-legend' }, [ + E('div', { 'class': 'legend-item' }, [ + E('span', { 'class': 'legend-dot', 'style': 'background: #22c55e;' }), + E('span', {}, 'Local Only (' + localOnly + ')') + ]), + E('div', { 'class': 'legend-item' }, [ + E('span', { 'class': 'legend-dot', 'style': 'background: #f59e0b;' }), + E('span', {}, 'Sanitized (' + sanitized + ')') + ]), + E('div', { 'class': 'legend-item' }, [ + E('span', { 'class': 'legend-dot', 'style': 'background: #3b82f6;' }), + E('span', {}, 'Cloud Direct (' + cloudDirect + ')') + ]) + ])); + + container.appendChild(chartSection); + } + + // Log Viewer + var logSection = E('div', { 'class': 'log-section' }); + logSection.appendChild(E('h3', {}, 'Recent Audit Entries')); + logSection.appendChild(E('p', { 'class': 'log-info' }, + 'Last 50 classification decisions (newest first)')); + + var logViewer = E('div', { 'class': 'log-viewer', 'id': 'log-viewer' }); + + if (logContent) { + var lines = logContent.trim().split('\n').slice(-50).reverse(); + lines.forEach(function(line) { + if (!line.trim()) return; + try { + var entry = JSON.parse(line); + var classClass = 'log-' + (entry.classification || 'local').replace('_only', '').replace('_direct', ''); + var time = entry.timestamp ? entry.timestamp.split('T')[1].split('.')[0] : ''; + + logViewer.appendChild(E('div', { 'class': 'log-line' }, [ + E('span', { 'class': 'log-time' }, '[' + time + '] '), + E('span', { 'class': classClass }, (entry.classification || 'unknown').toUpperCase()), + E('span', {}, ' - ' + (entry.reason || entry.classification_reason || 'classified')), + entry.provider ? E('span', { 'style': 'color: #94a3b8;' }, ' → ' + entry.provider) : '' + ])); + } catch (e) { + logViewer.appendChild(E('div', { 'class': 'log-line' }, line)); + } + }); + } else { + logViewer.appendChild(E('div', { 'class': 'log-line', 'style': 'color: #64748b;' }, + 'No audit log entries yet. Entries appear when requests are processed through the AI Gateway.')); + } + + logSection.appendChild(logViewer); + + logSection.appendChild(E('div', { 'style': 'margin-top: 12px;' }, [ + E('button', { + 'class': 'btn btn-secondary', + 'click': function() { window.location.reload(); } + }, 'Refresh') + ])); + + container.appendChild(logSection); + + // Setup polling + poll.add(this.pollStats.bind(this), 30); + + return container; + }, + + pollStats: function() { + return callGetAuditStats().then(function(stats) { + var s = stats.result || stats || {}; + var cards = document.querySelectorAll('.stat-card .value'); + if (cards.length >= 4) { + var total = (s.local_only || 0) + (s.sanitized || 0) + (s.cloud_direct || 0); + cards[0].textContent = String(total); + cards[1].textContent = String(s.local_only || 0); + cards[2].textContent = String(s.sanitized || 0); + cards[3].textContent = String(s.cloud_direct || 0); + } + }); + } +}); diff --git a/package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/classify.js b/package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/classify.js new file mode 100644 index 00000000..ec515bfe --- /dev/null +++ b/package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/classify.js @@ -0,0 +1,254 @@ +'use strict'; +'require view'; +'require rpc'; + +var callClassify = rpc.declare({ + object: 'luci.ai-gateway', + method: 'classify', + params: ['text'], + expect: {} +}); + +var kissCSS = ` + .classify-container { padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; } + .classify-container h2 { margin: 0 0 8px 0; } + .classify-container .subtitle { color: var(--text-secondary, #64748b); margin-bottom: 24px; } + + .classify-form { margin-bottom: 24px; } + .classify-form textarea { + width: 100%; min-height: 120px; padding: 12px; border: 1px solid var(--border-color, #e2e8f0); + border-radius: 8px; font-size: 0.95em; font-family: monospace; resize: vertical; + background: var(--bg-primary, white); color: var(--text-primary, #1e293b); + } + .classify-form .btn-row { margin-top: 12px; display: flex; gap: 12px; } + + .btn { padding: 10px 20px; border-radius: 8px; font-weight: 500; cursor: pointer; border: none; } + .btn-primary { background: #3b82f6; color: white; } + .btn-primary:hover { background: #2563eb; } + .btn-secondary { background: #64748b; color: white; } + .btn-secondary:hover { background: #475569; } + + .result-card { background: var(--bg-secondary, #f8fafc); border: 1px solid var(--border-color, #e2e8f0); border-radius: 12px; padding: 20px; margin-bottom: 16px; } + .result-card h3 { margin: 0 0 16px 0; font-size: 1.1em; } + + .classification-badge { display: inline-block; padding: 8px 16px; border-radius: 8px; font-weight: 600; font-size: 1.1em; } + .badge-local_only { background: #dcfce7; color: #166534; } + .badge-sanitized { background: #fef3c7; color: #92400e; } + .badge-cloud_direct { background: #dbeafe; color: #1e40af; } + + .result-details { margin-top: 16px; } + .detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid var(--border-color, #e2e8f0); } + .detail-row:last-child { border-bottom: none; } + .detail-label { width: 150px; font-weight: 500; color: var(--text-secondary, #64748b); } + .detail-value { flex: 1; font-family: monospace; } + + .examples-section { margin-top: 32px; } + .examples-section h3 { margin-bottom: 16px; } + .example-list { display: flex; flex-direction: column; gap: 8px; } + .example-item { padding: 12px 16px; background: var(--bg-secondary, #f8fafc); border: 1px solid var(--border-color, #e2e8f0); + border-radius: 8px; cursor: pointer; transition: all 0.2s; display: flex; justify-content: space-between; align-items: center; } + .example-item:hover { border-color: #3b82f6; background: #eff6ff; } + .example-text { font-family: monospace; font-size: 0.9em; } + .example-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.75em; font-weight: 500; } + + .tier-explanation { margin-top: 24px; padding: 16px; background: #f0f9ff; border: 1px solid #bae6fd; border-radius: 8px; } + .tier-explanation h4 { margin: 0 0 12px 0; color: #0369a1; } + .tier-explanation ul { margin: 0; padding-left: 20px; } + .tier-explanation li { margin-bottom: 8px; color: #0c4a6e; } + + @media (prefers-color-scheme: dark) { + .classify-form textarea { background: #0f172a; border-color: #334155; color: #f1f5f9; } + .result-card, .example-item { background: #1e293b; border-color: #334155; } + .tier-explanation { background: #0c4a6e; border-color: #0369a1; } + .tier-explanation h4, .tier-explanation li { color: #bae6fd; } + } +`; + +var examples = [ + { text: 'What is the weather today?', expected: 'cloud_direct' }, + { text: 'Server IP is 192.168.1.100', expected: 'local_only' }, + { text: 'User MAC address: AA:BB:CC:DD:EE:FF', expected: 'local_only' }, + { text: 'password=secret123', expected: 'local_only' }, + { text: 'Check /var/log/syslog for errors', expected: 'local_only' }, + { text: 'The user John Smith lives in Paris', expected: 'sanitized' }, + { text: 'Explain how firewalls work', expected: 'cloud_direct' }, + { text: 'API_KEY=sk-1234567890abcdef', expected: 'local_only' }, + { text: 'BEGIN RSA PRIVATE KEY', expected: 'local_only' }, + { text: 'crowdsec detected an attack', expected: 'local_only' } +]; + +return view.extend({ + title: 'Data Classifier', + + render: function() { + var container = E('div', { 'class': 'classify-container' }); + + container.appendChild(E('style', {}, kissCSS)); + + container.appendChild(E('h2', {}, 'Data Classifier')); + container.appendChild(E('p', { 'class': 'subtitle' }, + 'Test the classification engine to see how data is categorized into sovereignty tiers.')); + + // Input form + var form = E('div', { 'class': 'classify-form' }); + var textarea = E('textarea', { + 'id': 'classify-input', + 'placeholder': 'Enter text to classify...\n\nExamples:\n- "Server IP is 192.168.1.100" → LOCAL_ONLY\n- "What is 2+2?" → CLOUD_DIRECT' + }); + form.appendChild(textarea); + + form.appendChild(E('div', { 'class': 'btn-row' }, [ + E('button', { + 'class': 'btn btn-primary', + 'click': this.handleClassify.bind(this) + }, 'Classify'), + E('button', { + 'class': 'btn btn-secondary', + 'click': this.handleClear.bind(this) + }, 'Clear') + ])); + + container.appendChild(form); + + // Result placeholder + container.appendChild(E('div', { 'id': 'classify-result' })); + + // Tier explanation + container.appendChild(E('div', { 'class': 'tier-explanation' }, [ + E('h4', {}, 'Classification Tiers'), + E('ul', {}, [ + E('li', {}, [ + E('strong', {}, 'LOCAL_ONLY: '), + 'Contains sensitive data (IPs, MACs, credentials, logs, keys). Never sent externally.' + ]), + E('li', {}, [ + E('strong', {}, 'SANITIZED: '), + 'Contains PII that can be scrubbed. Sent to EU cloud (Mistral) with opt-in.' + ]), + E('li', {}, [ + E('strong', {}, 'CLOUD_DIRECT: '), + 'Generic queries with no sensitive data. Can be sent to any provider with opt-in.' + ]) + ]) + ])); + + // Examples section + var examplesSection = E('div', { 'class': 'examples-section' }); + examplesSection.appendChild(E('h3', {}, 'Example Inputs')); + + var exampleList = E('div', { 'class': 'example-list' }); + examples.forEach(function(ex) { + var badgeClass = 'badge-' + ex.expected; + exampleList.appendChild(E('div', { + 'class': 'example-item', + 'click': this.handleExampleClick.bind(this, ex.text) + }, [ + E('span', { 'class': 'example-text' }, ex.text), + E('span', { 'class': 'example-badge ' + badgeClass }, ex.expected.toUpperCase()) + ])); + }.bind(this)); + + examplesSection.appendChild(exampleList); + container.appendChild(examplesSection); + + return container; + }, + + handleClassify: function() { + var textarea = document.getElementById('classify-input'); + var text = textarea ? textarea.value.trim() : ''; + + if (!text) { + this.showResult({ error: 'Please enter some text to classify' }); + return; + } + + var resultDiv = document.getElementById('classify-result'); + resultDiv.innerHTML = '

Classifying...

'; + + callClassify(text).then(function(result) { + this.showResult(result); + }.bind(this)).catch(function(err) { + this.showResult({ error: 'Classification error: ' + String(err) }); + }.bind(this)); + }, + + handleClear: function() { + var textarea = document.getElementById('classify-input'); + if (textarea) textarea.value = ''; + var resultDiv = document.getElementById('classify-result'); + if (resultDiv) resultDiv.innerHTML = ''; + }, + + handleExampleClick: function(text) { + var textarea = document.getElementById('classify-input'); + if (textarea) { + textarea.value = text; + this.handleClassify(); + } + }, + + showResult: function(result) { + var resultDiv = document.getElementById('classify-result'); + if (!resultDiv) return; + + if (result.error) { + resultDiv.innerHTML = ''; + resultDiv.appendChild(E('div', { 'class': 'result-card' }, [ + E('p', { 'style': 'color: #dc2626;' }, result.error) + ])); + return; + } + + var classification = result.classification || result.result?.classification || 'unknown'; + var reason = result.reason || result.result?.reason || 'No reason provided'; + var pattern = result.matched_pattern || result.result?.matched_pattern || '-'; + + resultDiv.innerHTML = ''; + + var card = E('div', { 'class': 'result-card' }); + card.appendChild(E('h3', {}, 'Classification Result')); + + card.appendChild(E('div', { 'style': 'margin-bottom: 16px;' }, [ + E('span', { 'class': 'classification-badge badge-' + classification }, + classification.toUpperCase()) + ])); + + var details = E('div', { 'class': 'result-details' }); + + details.appendChild(E('div', { 'class': 'detail-row' }, [ + E('span', { 'class': 'detail-label' }, 'Classification'), + E('span', { 'class': 'detail-value' }, classification.toUpperCase()) + ])); + + details.appendChild(E('div', { 'class': 'detail-row' }, [ + E('span', { 'class': 'detail-label' }, 'Reason'), + E('span', { 'class': 'detail-value' }, reason) + ])); + + if (pattern !== '-') { + details.appendChild(E('div', { 'class': 'detail-row' }, [ + E('span', { 'class': 'detail-label' }, 'Matched Pattern'), + E('span', { 'class': 'detail-value' }, pattern) + ])); + } + + // Destination explanation + var destination = 'Unknown'; + if (classification === 'local_only') { + destination = 'LocalAI only (data never leaves device)'; + } else if (classification === 'sanitized') { + destination = 'Mistral EU (after PII scrubbing, if enabled)'; + } else if (classification === 'cloud_direct') { + destination = 'Any enabled provider (no sensitive data detected)'; + } + + details.appendChild(E('div', { 'class': 'detail-row' }, [ + E('span', { 'class': 'detail-label' }, 'Destination'), + E('span', { 'class': 'detail-value' }, destination) + ])); + + card.appendChild(details); + resultDiv.appendChild(card); + } +}); diff --git a/package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/overview.js b/package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/overview.js new file mode 100644 index 00000000..023dba7d --- /dev/null +++ b/package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/overview.js @@ -0,0 +1,359 @@ +'use strict'; +'require view'; +'require rpc'; +'require poll'; + +var callStatus = rpc.declare({ + object: 'luci.ai-gateway', + method: 'status', + expect: {} +}); + +var callGetProviders = rpc.declare({ + object: 'luci.ai-gateway', + method: 'get_providers', + expect: {} +}); + +var callGetAuditStats = rpc.declare({ + object: 'luci.ai-gateway', + method: 'get_audit_stats', + expect: {} +}); + +var callSetOfflineMode = rpc.declare({ + object: 'luci.ai-gateway', + method: 'set_offline_mode', + params: ['mode'], + expect: {} +}); + +var callStart = rpc.declare({ + object: 'luci.ai-gateway', + method: 'start', + expect: {} +}); + +var callStop = rpc.declare({ + object: 'luci.ai-gateway', + method: 'stop', + expect: {} +}); + +var callRestart = rpc.declare({ + object: 'luci.ai-gateway', + method: 'restart', + expect: {} +}); + +// KISS Theme CSS +var kissCSS = ` + .ai-gateway-container { padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } + .ai-gateway-header { display: flex; align-items: center; gap: 12px; margin-bottom: 24px; } + .ai-gateway-header h2 { margin: 0; font-size: 1.5em; } + .ai-gateway-header .badge { padding: 4px 12px; border-radius: 12px; font-size: 0.8em; font-weight: 600; } + .badge-running { background: #22c55e; color: white; } + .badge-stopped { background: #ef4444; color: white; } + .badge-offline { background: #f59e0b; color: white; } + + .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; } + .stat-card { background: var(--bg-secondary, #f8fafc); border-radius: 12px; padding: 20px; border: 1px solid var(--border-color, #e2e8f0); } + .stat-card .label { color: var(--text-secondary, #64748b); font-size: 0.85em; margin-bottom: 4px; } + .stat-card .value { font-size: 1.8em; font-weight: 700; color: var(--text-primary, #1e293b); } + .stat-card .sublabel { font-size: 0.75em; color: var(--text-secondary, #64748b); margin-top: 4px; } + + .section { margin-bottom: 24px; } + .section-title { font-size: 1.1em; font-weight: 600; margin-bottom: 16px; color: var(--text-primary, #1e293b); } + + .providers-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; } + .provider-card { background: var(--bg-secondary, #f8fafc); border-radius: 12px; padding: 16px; border: 1px solid var(--border-color, #e2e8f0); display: flex; justify-content: space-between; align-items: center; } + .provider-info { display: flex; flex-direction: column; gap: 4px; } + .provider-name { font-weight: 600; font-size: 1.1em; text-transform: capitalize; } + .provider-meta { font-size: 0.85em; color: var(--text-secondary, #64748b); } + .provider-status { padding: 4px 10px; border-radius: 8px; font-size: 0.8em; font-weight: 500; } + .status-available { background: #dcfce7; color: #16a34a; } + .status-configured { background: #dbeafe; color: #2563eb; } + .status-unavailable { background: #fee2e2; color: #dc2626; } + .status-disabled { background: #f1f5f9; color: #64748b; } + .status-no_api_key { background: #fef3c7; color: #d97706; } + + .classification-legend { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; } + .legend-item { display: flex; align-items: center; gap: 8px; padding: 8px 16px; background: var(--bg-secondary, #f8fafc); border-radius: 8px; border: 1px solid var(--border-color, #e2e8f0); } + .legend-dot { width: 12px; height: 12px; border-radius: 50%; } + .dot-local { background: #22c55e; } + .dot-sanitized { background: #f59e0b; } + .dot-cloud { background: #3b82f6; } + + .actions-row { display: flex; gap: 12px; margin-bottom: 24px; flex-wrap: wrap; } + .btn { padding: 10px 20px; border-radius: 8px; font-weight: 500; cursor: pointer; border: none; transition: all 0.2s; } + .btn-primary { background: #3b82f6; color: white; } + .btn-primary:hover { background: #2563eb; } + .btn-success { background: #22c55e; color: white; } + .btn-success:hover { background: #16a34a; } + .btn-danger { background: #ef4444; color: white; } + .btn-danger:hover { background: #dc2626; } + .btn-warning { background: #f59e0b; color: white; } + .btn-warning:hover { background: #d97706; } + .btn-secondary { background: #64748b; color: white; } + .btn-secondary:hover { background: #475569; } + + .audit-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; } + .audit-stat { text-align: center; padding: 16px; background: var(--bg-secondary, #f8fafc); border-radius: 8px; border: 1px solid var(--border-color, #e2e8f0); } + .audit-stat .count { font-size: 1.5em; font-weight: 700; } + .audit-stat .type { font-size: 0.85em; color: var(--text-secondary, #64748b); } + + .info-box { padding: 16px; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; margin-bottom: 16px; } + .info-box.anssi { background: #f0fdf4; border-color: #86efac; } + .info-box h4 { margin: 0 0 8px 0; color: #1e40af; } + .info-box.anssi h4 { color: #166534; } + .info-box p { margin: 0; font-size: 0.9em; color: #1e3a5f; } + .info-box.anssi p { color: #14532d; } + + @media (prefers-color-scheme: dark) { + .stat-card, .provider-card, .legend-item, .audit-stat { background: #1e293b; border-color: #334155; } + .stat-card .label, .provider-meta, .audit-stat .type { color: #94a3b8; } + .stat-card .value, .provider-name, .section-title { color: #f1f5f9; } + .info-box { background: #1e3a5f; border-color: #3b82f6; } + .info-box h4 { color: #93c5fd; } + .info-box p { color: #bfdbfe; } + .info-box.anssi { background: #14532d; border-color: #22c55e; } + .info-box.anssi h4 { color: #86efac; } + .info-box.anssi p { color: #bbf7d0; } + } +`; + +return view.extend({ + title: 'AI Gateway', + + load: function() { + return Promise.all([ + callStatus(), + callGetProviders(), + callGetAuditStats() + ]); + }, + + render: function(data) { + var status = data[0].result || data[0] || {}; + var providersData = data[1].providers || data[1] || []; + var auditStats = data[2].result || data[2] || {}; + + var container = E('div', { 'class': 'ai-gateway-container' }); + + // Inject CSS + var style = E('style', {}, kissCSS); + container.appendChild(style); + + // Header + var statusBadge = status.running ? + (status.offline_mode ? 'badge-offline' : 'badge-running') : 'badge-stopped'; + var statusText = status.running ? + (status.offline_mode ? 'Offline Mode' : 'Running') : 'Stopped'; + + container.appendChild(E('div', { 'class': 'ai-gateway-header' }, [ + E('h2', {}, 'AI Gateway'), + E('span', { 'class': 'badge ' + statusBadge }, statusText) + ])); + + // ANSSI Info Box + container.appendChild(E('div', { 'class': 'info-box anssi' }, [ + E('h4', {}, 'ANSSI CSPN Compliance'), + E('p', {}, 'Data Sovereignty Engine ensures sensitive network data (IPs, MACs, logs, credentials) never leaves the device. Three-tier classification: LOCAL_ONLY (on-device), SANITIZED (EU cloud with PII scrubbing), CLOUD_DIRECT (opt-in external).') + ])); + + // Actions Row + var actionsRow = E('div', { 'class': 'actions-row' }); + + if (status.running) { + actionsRow.appendChild(E('button', { + 'class': 'btn btn-danger', + 'click': this.handleStop.bind(this) + }, 'Stop')); + actionsRow.appendChild(E('button', { + 'class': 'btn btn-secondary', + 'click': this.handleRestart.bind(this) + }, 'Restart')); + } else { + actionsRow.appendChild(E('button', { + 'class': 'btn btn-success', + 'click': this.handleStart.bind(this) + }, 'Start')); + } + + var offlineBtnClass = status.offline_mode ? 'btn-warning' : 'btn-secondary'; + var offlineBtnText = status.offline_mode ? 'Disable Offline Mode' : 'Enable Offline Mode'; + actionsRow.appendChild(E('button', { + 'class': 'btn ' + offlineBtnClass, + 'click': this.handleToggleOffline.bind(this, !status.offline_mode) + }, offlineBtnText)); + + container.appendChild(actionsRow); + + // Stats Grid + var statsGrid = E('div', { 'class': 'stats-grid' }); + + statsGrid.appendChild(E('div', { 'class': 'stat-card' }, [ + E('div', { 'class': 'label' }, 'Proxy Port'), + E('div', { 'class': 'value' }, String(status.port || '4050')), + E('div', { 'class': 'sublabel' }, 'OpenAI-compatible API') + ])); + + statsGrid.appendChild(E('div', { 'class': 'stat-card' }, [ + E('div', { 'class': 'label' }, 'Providers Enabled'), + E('div', { 'class': 'value' }, String(status.providers_enabled || 0)), + E('div', { 'class': 'sublabel' }, 'of 6 available') + ])); + + var totalRequests = (auditStats.local_only || 0) + (auditStats.sanitized || 0) + (auditStats.cloud_direct || 0); + statsGrid.appendChild(E('div', { 'class': 'stat-card' }, [ + E('div', { 'class': 'label' }, 'Total Requests'), + E('div', { 'class': 'value' }, String(totalRequests)), + E('div', { 'class': 'sublabel' }, 'since last restart') + ])); + + statsGrid.appendChild(E('div', { 'class': 'stat-card' }, [ + E('div', { 'class': 'label' }, 'Local Only'), + E('div', { 'class': 'value', 'style': 'color: #22c55e;' }, String(auditStats.local_only || 0)), + E('div', { 'class': 'sublabel' }, 'data stayed on device') + ])); + + container.appendChild(statsGrid); + + // Classification Legend + container.appendChild(E('div', { 'class': 'section' }, [ + E('div', { 'class': 'section-title' }, 'Classification Tiers'), + E('div', { 'class': 'classification-legend' }, [ + E('div', { 'class': 'legend-item' }, [ + E('span', { 'class': 'legend-dot dot-local' }), + E('span', {}, 'LOCAL_ONLY - Never leaves device (IPs, MACs, logs, keys)') + ]), + E('div', { 'class': 'legend-item' }, [ + E('span', { 'class': 'legend-dot dot-sanitized' }), + E('span', {}, 'SANITIZED - PII scrubbed, EU cloud opt-in (Mistral)') + ]), + E('div', { 'class': 'legend-item' }, [ + E('span', { 'class': 'legend-dot dot-cloud' }), + E('span', {}, 'CLOUD_DIRECT - Generic queries, any provider opt-in') + ]) + ]) + ])); + + // Providers Section + var providersGrid = E('div', { 'class': 'providers-grid' }); + + var providerIcons = { + localai: 'On-Device', + mistral: 'EU Sovereign', + claude: 'Anthropic', + openai: 'OpenAI', + gemini: 'Google', + xai: 'xAI (Grok)' + }; + + providersData.forEach(function(provider) { + var statusClass = 'status-' + (provider.status || 'disabled'); + var statusText = (provider.status || 'disabled').replace(/_/g, ' '); + + providersGrid.appendChild(E('div', { 'class': 'provider-card' }, [ + E('div', { 'class': 'provider-info' }, [ + E('div', { 'class': 'provider-name' }, provider.name), + E('div', { 'class': 'provider-meta' }, [ + providerIcons[provider.name] || '', + ' | Priority: ', String(provider.priority), + ' | Tier: ', (provider.classification || '-').toUpperCase() + ].join('')) + ]), + E('span', { 'class': 'provider-status ' + statusClass }, statusText) + ])); + }); + + container.appendChild(E('div', { 'class': 'section' }, [ + E('div', { 'class': 'section-title' }, 'Provider Hierarchy'), + providersGrid + ])); + + // Audit Stats Section + if (auditStats && (auditStats.local_only || auditStats.sanitized || auditStats.cloud_direct)) { + var auditStatsDiv = E('div', { 'class': 'audit-stats' }); + + auditStatsDiv.appendChild(E('div', { 'class': 'audit-stat' }, [ + E('div', { 'class': 'count', 'style': 'color: #22c55e;' }, String(auditStats.local_only || 0)), + E('div', { 'class': 'type' }, 'Local Only') + ])); + auditStatsDiv.appendChild(E('div', { 'class': 'audit-stat' }, [ + E('div', { 'class': 'count', 'style': 'color: #f59e0b;' }, String(auditStats.sanitized || 0)), + E('div', { 'class': 'type' }, 'Sanitized') + ])); + auditStatsDiv.appendChild(E('div', { 'class': 'audit-stat' }, [ + E('div', { 'class': 'count', 'style': 'color: #3b82f6;' }, String(auditStats.cloud_direct || 0)), + E('div', { 'class': 'type' }, 'Cloud Direct') + ])); + + container.appendChild(E('div', { 'class': 'section' }, [ + E('div', { 'class': 'section-title' }, 'Classification Statistics'), + auditStatsDiv + ])); + } + + // Setup polling + poll.add(this.pollData.bind(this), 10); + + return container; + }, + + pollData: function() { + var self = this; + return Promise.all([ + callStatus(), + callGetProviders(), + callGetAuditStats() + ]).then(function(data) { + var container = document.querySelector('.ai-gateway-container'); + if (container) { + var status = data[0].result || data[0] || {}; + var auditStats = data[2].result || data[2] || {}; + + // Update stats + var statValues = container.querySelectorAll('.stat-card .value'); + if (statValues.length >= 4) { + statValues[1].textContent = String(status.providers_enabled || 0); + var totalRequests = (auditStats.local_only || 0) + (auditStats.sanitized || 0) + (auditStats.cloud_direct || 0); + statValues[2].textContent = String(totalRequests); + statValues[3].textContent = String(auditStats.local_only || 0); + } + } + }); + }, + + handleStart: function() { + var self = this; + callStart().then(function() { + window.location.reload(); + }); + }, + + handleStop: function() { + var self = this; + callStop().then(function() { + window.location.reload(); + }); + }, + + handleRestart: function() { + var self = this; + callRestart().then(function() { + window.location.reload(); + }); + }, + + handleToggleOffline: function(enable) { + var self = this; + callSetOfflineMode(enable ? '1' : '0').then(function() { + window.location.reload(); + }); + }, + + handleSaveProvider: function(form, ev) { + ev.preventDefault(); + } +}); diff --git a/package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/providers.js b/package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/providers.js new file mode 100644 index 00000000..5f3278c0 --- /dev/null +++ b/package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/providers.js @@ -0,0 +1,295 @@ +'use strict'; +'require view'; +'require rpc'; +'require ui'; + +var callGetProviders = rpc.declare({ + object: 'luci.ai-gateway', + method: 'get_providers', + expect: {} +}); + +var callSetProvider = rpc.declare({ + object: 'luci.ai-gateway', + method: 'set_provider', + params: ['provider', 'enabled', 'api_key'], + expect: {} +}); + +var callTestProvider = rpc.declare({ + object: 'luci.ai-gateway', + method: 'test_provider', + params: ['provider'], + expect: {} +}); + +var kissCSS = ` + .providers-container { padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } + .providers-container h2 { margin: 0 0 8px 0; } + .providers-container .subtitle { color: var(--text-secondary, #64748b); margin-bottom: 24px; } + + .provider-list { display: flex; flex-direction: column; gap: 16px; } + .provider-item { background: var(--bg-secondary, #f8fafc); border: 1px solid var(--border-color, #e2e8f0); border-radius: 12px; padding: 20px; } + .provider-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } + .provider-title { display: flex; align-items: center; gap: 12px; } + .provider-name { font-size: 1.2em; font-weight: 600; text-transform: capitalize; } + .provider-badge { padding: 4px 10px; border-radius: 6px; font-size: 0.75em; font-weight: 500; } + .badge-local { background: #dcfce7; color: #16a34a; } + .badge-eu { background: #dbeafe; color: #2563eb; } + .badge-cloud { background: #fef3c7; color: #d97706; } + + .provider-meta { display: flex; gap: 16px; font-size: 0.9em; color: var(--text-secondary, #64748b); margin-bottom: 16px; } + .provider-meta span { display: flex; align-items: center; gap: 4px; } + + .provider-controls { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } + .provider-controls input[type="text"], .provider-controls input[type="password"] { + padding: 8px 12px; border: 1px solid var(--border-color, #e2e8f0); border-radius: 6px; + font-size: 0.9em; min-width: 300px; background: var(--bg-primary, white); + } + + .toggle-switch { position: relative; width: 48px; height: 24px; } + .toggle-switch input { opacity: 0; width: 0; height: 0; } + .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; + background: #cbd5e1; border-radius: 24px; transition: 0.3s; } + .toggle-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; + background: white; border-radius: 50%; transition: 0.3s; } + .toggle-switch input:checked + .toggle-slider { background: #22c55e; } + .toggle-switch input:checked + .toggle-slider:before { transform: translateX(24px); } + + .btn { padding: 8px 16px; border-radius: 6px; font-weight: 500; cursor: pointer; border: none; font-size: 0.9em; } + .btn-primary { background: #3b82f6; color: white; } + .btn-primary:hover { background: #2563eb; } + .btn-secondary { background: #64748b; color: white; } + .btn-secondary:hover { background: #475569; } + .btn-success { background: #22c55e; color: white; } + .btn-success:hover { background: #16a34a; } + + .status-indicator { padding: 4px 10px; border-radius: 6px; font-size: 0.8em; font-weight: 500; } + .status-available { background: #dcfce7; color: #16a34a; } + .status-configured { background: #dbeafe; color: #2563eb; } + .status-unavailable { background: #fee2e2; color: #dc2626; } + .status-disabled { background: #f1f5f9; color: #64748b; } + .status-no_api_key { background: #fef3c7; color: #d97706; } + + .test-result { margin-top: 12px; padding: 12px; border-radius: 6px; font-size: 0.9em; } + .test-success { background: #dcfce7; color: #166534; } + .test-failure { background: #fee2e2; color: #991b1b; } + + .info-text { font-size: 0.85em; color: var(--text-secondary, #64748b); margin-top: 8px; } + + @media (prefers-color-scheme: dark) { + .provider-item { background: #1e293b; border-color: #334155; } + .provider-controls input { background: #0f172a; border-color: #334155; color: #f1f5f9; } + } +`; + +var providerInfo = { + localai: { + name: 'LocalAI', + description: 'On-device inference via LocalAI. No API key required.', + tier: 'local', + tierLabel: 'LOCAL_ONLY', + badgeClass: 'badge-local', + needsKey: false + }, + mistral: { + name: 'Mistral AI', + description: 'EU-based AI provider (France). GDPR compliant, sovereign cloud.', + tier: 'sanitized', + tierLabel: 'SANITIZED', + badgeClass: 'badge-eu', + needsKey: true, + keyUrl: 'https://console.mistral.ai/api-keys/' + }, + claude: { + name: 'Claude (Anthropic)', + description: 'Anthropic Claude models. US-based.', + tier: 'cloud', + tierLabel: 'CLOUD_DIRECT', + badgeClass: 'badge-cloud', + needsKey: true, + keyUrl: 'https://console.anthropic.com/settings/keys' + }, + openai: { + name: 'OpenAI (GPT)', + description: 'OpenAI GPT models. US-based.', + tier: 'cloud', + tierLabel: 'CLOUD_DIRECT', + badgeClass: 'badge-cloud', + needsKey: true, + keyUrl: 'https://platform.openai.com/api-keys' + }, + gemini: { + name: 'Google Gemini', + description: 'Google Gemini models. US-based.', + tier: 'cloud', + tierLabel: 'CLOUD_DIRECT', + badgeClass: 'badge-cloud', + needsKey: true, + keyUrl: 'https://aistudio.google.com/app/apikey' + }, + xai: { + name: 'xAI (Grok)', + description: 'xAI Grok models. US-based.', + tier: 'cloud', + tierLabel: 'CLOUD_DIRECT', + badgeClass: 'badge-cloud', + needsKey: true, + keyUrl: 'https://console.x.ai/' + } +}; + +return view.extend({ + title: 'AI Providers', + + load: function() { + return callGetProviders(); + }, + + render: function(data) { + var providers = data.providers || data || []; + var container = E('div', { 'class': 'providers-container' }); + + container.appendChild(E('style', {}, kissCSS)); + + container.appendChild(E('h2', {}, 'AI Providers')); + container.appendChild(E('p', { 'class': 'subtitle' }, + 'Configure AI providers in priority order. LocalAI is always enabled for on-device inference.')); + + var providerList = E('div', { 'class': 'provider-list' }); + + providers.forEach(function(provider) { + var info = providerInfo[provider.name] || {}; + var item = E('div', { 'class': 'provider-item', 'data-provider': provider.name }); + + // Header + var header = E('div', { 'class': 'provider-header' }, [ + E('div', { 'class': 'provider-title' }, [ + E('span', { 'class': 'provider-name' }, info.name || provider.name), + E('span', { 'class': 'provider-badge ' + (info.badgeClass || 'badge-cloud') }, info.tierLabel || 'CLOUD') + ]), + E('span', { 'class': 'status-indicator status-' + (provider.status || 'disabled') }, + (provider.status || 'disabled').replace(/_/g, ' ')) + ]); + item.appendChild(header); + + // Meta + var meta = E('div', { 'class': 'provider-meta' }, [ + E('span', {}, ['Priority: ', String(provider.priority)]), + E('span', {}, ['Classification: ', (provider.classification || '-').toUpperCase()]) + ]); + item.appendChild(meta); + + if (info.description) { + item.appendChild(E('p', { 'class': 'info-text' }, info.description)); + } + + // Controls + var controls = E('div', { 'class': 'provider-controls' }); + + // Enable toggle + var toggleId = 'toggle-' + provider.name; + var toggle = E('label', { 'class': 'toggle-switch' }, [ + E('input', { + 'type': 'checkbox', + 'id': toggleId, + 'checked': provider.enabled, + 'change': this.handleToggle.bind(this, provider.name) + }), + E('span', { 'class': 'toggle-slider' }) + ]); + controls.appendChild(toggle); + controls.appendChild(E('label', { 'for': toggleId, 'style': 'cursor: pointer; margin-right: 16px;' }, + provider.enabled ? 'Enabled' : 'Disabled')); + + // API Key input (if needed) + if (info.needsKey) { + var keyInput = E('input', { + 'type': 'password', + 'id': 'key-' + provider.name, + 'placeholder': 'Enter API key...', + 'autocomplete': 'off' + }); + controls.appendChild(keyInput); + + controls.appendChild(E('button', { + 'class': 'btn btn-primary', + 'click': this.handleSaveKey.bind(this, provider.name) + }, 'Save Key')); + + controls.appendChild(E('button', { + 'class': 'btn btn-secondary', + 'click': this.handleTest.bind(this, provider.name) + }, 'Test')); + + if (info.keyUrl) { + controls.appendChild(E('a', { + 'href': info.keyUrl, + 'target': '_blank', + 'style': 'font-size: 0.85em; color: #3b82f6;' + }, 'Get API Key')); + } + } else { + // LocalAI - just test button + controls.appendChild(E('button', { + 'class': 'btn btn-secondary', + 'click': this.handleTest.bind(this, provider.name) + }, 'Test Connection')); + } + + item.appendChild(controls); + + // Test result placeholder + item.appendChild(E('div', { 'class': 'test-result-container', 'id': 'result-' + provider.name })); + + providerList.appendChild(item); + }.bind(this)); + + container.appendChild(providerList); + + return container; + }, + + handleToggle: function(providerName, ev) { + var enabled = ev.target.checked ? '1' : '0'; + callSetProvider(providerName, enabled, '').then(function() { + ui.addNotification(null, E('p', {}, + providerName + ' ' + (enabled === '1' ? 'enabled' : 'disabled')), 'success'); + }); + }, + + handleSaveKey: function(providerName) { + var keyInput = document.getElementById('key-' + providerName); + var apiKey = keyInput ? keyInput.value : ''; + + if (!apiKey) { + ui.addNotification(null, E('p', {}, 'Please enter an API key'), 'warning'); + return; + } + + callSetProvider(providerName, '', apiKey).then(function() { + keyInput.value = ''; + ui.addNotification(null, E('p', {}, 'API key saved for ' + providerName), 'success'); + window.location.reload(); + }); + }, + + handleTest: function(providerName) { + var resultContainer = document.getElementById('result-' + providerName); + resultContainer.innerHTML = '
Testing...
'; + + callTestProvider(providerName).then(function(result) { + var success = result.success; + var output = result.output || (success ? 'Provider is available' : 'Test failed'); + + resultContainer.innerHTML = ''; + resultContainer.appendChild(E('div', { + 'class': 'test-result ' + (success ? 'test-success' : 'test-failure') + }, output)); + }).catch(function(err) { + resultContainer.innerHTML = ''; + resultContainer.appendChild(E('div', { 'class': 'test-result test-failure' }, + 'Test error: ' + String(err))); + }); + } +}); diff --git a/package/secubox/luci-app-ai-gateway/root/usr/share/luci/menu.d/luci-app-ai-gateway.json b/package/secubox/luci-app-ai-gateway/root/usr/share/luci/menu.d/luci-app-ai-gateway.json new file mode 100644 index 00000000..0ca2f069 --- /dev/null +++ b/package/secubox/luci-app-ai-gateway/root/usr/share/luci/menu.d/luci-app-ai-gateway.json @@ -0,0 +1,44 @@ +{ + "admin/services/ai-gateway": { + "title": "AI Gateway", + "order": 25, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": ["luci-app-ai-gateway"] + } + }, + "admin/services/ai-gateway/overview": { + "title": "Overview", + "order": 10, + "action": { + "type": "view", + "path": "ai-gateway/overview" + } + }, + "admin/services/ai-gateway/providers": { + "title": "Providers", + "order": 20, + "action": { + "type": "view", + "path": "ai-gateway/providers" + } + }, + "admin/services/ai-gateway/classify": { + "title": "Classifier", + "order": 30, + "action": { + "type": "view", + "path": "ai-gateway/classify" + } + }, + "admin/services/ai-gateway/audit": { + "title": "Audit Log", + "order": 40, + "action": { + "type": "view", + "path": "ai-gateway/audit" + } + } +} diff --git a/package/secubox/luci-app-ai-gateway/root/usr/share/rpcd/acl.d/luci-app-ai-gateway.json b/package/secubox/luci-app-ai-gateway/root/usr/share/rpcd/acl.d/luci-app-ai-gateway.json new file mode 100644 index 00000000..b22f50cc --- /dev/null +++ b/package/secubox/luci-app-ai-gateway/root/usr/share/rpcd/acl.d/luci-app-ai-gateway.json @@ -0,0 +1,28 @@ +{ + "luci-app-ai-gateway": { + "description": "Grant access to AI Gateway dashboard", + "read": { + "ubus": { + "luci.ai-gateway": [ + "status", + "get_config", + "get_providers", + "get_audit_stats", + "classify" + ] + } + }, + "write": { + "ubus": { + "luci.ai-gateway": [ + "set_provider", + "set_offline_mode", + "test_provider", + "start", + "stop", + "restart" + ] + } + } + } +} diff --git a/package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/dnsguard.js b/package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/dnsguard.js new file mode 100644 index 00000000..cb695c2f --- /dev/null +++ b/package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/dnsguard.js @@ -0,0 +1,253 @@ +'use strict'; +'require view'; +'require rpc'; +'require poll'; +'require ui'; + +var callDNSGuardStatus = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'dnsguard_status', + expect: {} +}); + +var callDNSGuardAlerts = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'dnsguard_alerts', + params: ['limit'], + expect: { alerts: [] } +}); + +var callDNSGuardSync = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'dnsguard_sync', + expect: {} +}); + +function typeIcon(type) { + var icons = { + 'dga': '\u{1F9EC}', // DNA for DGA + 'dns_tunnel': '\u{1F573}', // Hole for tunneling + 'tunneling': '\u{1F573}', + 'malware': '\u{1F41B}', // Bug for malware + 'known_bad': '\u{1F6AB}', // No entry for known bad + 'suspicious_tld': '\u{26A0}', // Warning for TLD + 'tld_anomaly': '\u{26A0}', + 'rate_anomaly': '\u{23F1}', // Stopwatch for rate + 'ai_detected': '\u{1F916}' // Robot for AI + }; + return icons[type] || '\u{2753}'; +} + +function typeBadge(type) { + var colors = { + 'dga': '#dc3545', + 'dns_tunnel': '#fd7e14', + 'tunneling': '#fd7e14', + 'malware': '#dc3545', + 'known_bad': '#6f42c1', + 'suspicious_tld': '#ffc107', + 'tld_anomaly': '#ffc107', + 'rate_anomaly': '#17a2b8', + 'ai_detected': '#28a745' + }; + var color = colors[type] || '#6c757d'; + return E('span', { + style: 'display:inline-block;padding:2px 8px;border-radius:4px;' + + 'background:' + color + ';color:#fff;font-size:0.85em;font-weight:500;' + }, typeIcon(type) + ' ' + (type || 'unknown').replace('_', ' ')); +} + +function confidenceBar(value) { + var color = value >= 80 ? '#dc3545' : value >= 60 ? '#fd7e14' : '#ffc107'; + return E('div', { style: 'display:flex;align-items:center;gap:8px;' }, [ + E('div', { + style: 'width:80px;height:8px;background:#e0e0e0;border-radius:4px;overflow:hidden;' + }, [ + E('div', { + style: 'height:100%;width:' + value + '%;background:' + color + ';' + }) + ]), + E('span', { style: 'font-size:0.85em;color:#666;' }, value + '%') + ]); +} + +return view.extend({ + load: function() { + return Promise.all([ + callDNSGuardStatus(), + callDNSGuardAlerts(50) + ]); + }, + + render: function(data) { + var status = data[0] || {}; + var alerts = data[1] || []; + + var container = E('div', { class: 'cbi-map' }); + + // Header + container.appendChild(E('h2', { class: 'cbi-section-title' }, [ + '\u{1F9E0} DNS Guard Integration' + ])); + + // Status Cards Row + var cardsRow = E('div', { + style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:24px;' + }); + + // Service Status Card + var serviceStatus = status.installed ? + (status.running ? '\u{2705} Running' : '\u{1F7E1} Stopped') : + '\u{274C} Not Installed'; + var serviceColor = status.running ? '#28a745' : (status.installed ? '#ffc107' : '#dc3545'); + + cardsRow.appendChild(E('div', { + style: 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 2px 4px rgba(0,0,0,0.1);' + }, [ + E('div', { style: 'font-size:0.85em;color:#666;margin-bottom:4px;' }, 'DNS Guard Service'), + E('div', { style: 'font-size:1.5em;font-weight:600;color:' + serviceColor + ';' }, serviceStatus) + ])); + + // Alert Count Card + cardsRow.appendChild(E('div', { + style: 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 2px 4px rgba(0,0,0,0.1);' + }, [ + E('div', { style: 'font-size:0.85em;color:#666;margin-bottom:4px;' }, 'Total Alerts'), + E('div', { style: 'font-size:2em;font-weight:600;color:#dc3545;' }, String(status.alert_count || 0)) + ])); + + // Pending Approvals Card + cardsRow.appendChild(E('div', { + style: 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 2px 4px rgba(0,0,0,0.1);' + }, [ + E('div', { style: 'font-size:0.85em;color:#666;margin-bottom:4px;' }, 'Pending Approvals'), + E('div', { style: 'font-size:2em;font-weight:600;color:#fd7e14;' }, String(status.pending_count || 0)) + ])); + + // Vortex Imported Card + cardsRow.appendChild(E('div', { + style: 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 2px 4px rgba(0,0,0,0.1);' + }, [ + E('div', { style: 'font-size:0.85em;color:#666;margin-bottom:4px;' }, 'Imported to Vortex'), + E('div', { style: 'font-size:2em;font-weight:600;color:#28a745;' }, String(status.vortex_imported || 0)) + ])); + + container.appendChild(cardsRow); + + // Detection Types Breakdown + if (status.detection_types && Object.keys(status.detection_types).length > 0) { + var typesSection = E('div', { + style: 'background:#fff;border-radius:8px;padding:16px;margin-bottom:24px;box-shadow:0 2px 4px rgba(0,0,0,0.1);' + }); + + typesSection.appendChild(E('h3', { style: 'margin:0 0 16px 0;font-size:1.1em;' }, 'Detection Types')); + + var typesGrid = E('div', { + style: 'display:flex;flex-wrap:wrap;gap:12px;' + }); + + for (var type in status.detection_types) { + typesGrid.appendChild(E('div', { + style: 'display:flex;align-items:center;gap:8px;padding:8px 12px;background:#f8f9fa;border-radius:6px;' + }, [ + typeBadge(type), + E('span', { style: 'font-weight:600;' }, String(status.detection_types[type])) + ])); + } + + typesSection.appendChild(typesGrid); + container.appendChild(typesSection); + } + + // Actions Bar + var actionsBar = E('div', { + style: 'display:flex;gap:12px;margin-bottom:24px;' + }); + + var syncBtn = E('button', { + class: 'cbi-button cbi-button-action', + click: ui.createHandlerFn(this, function() { + return callDNSGuardSync().then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', '\u{2705} ' + result.message), 'info'); + window.location.reload(); + } else { + ui.addNotification(null, E('p', '\u{274C} ' + result.message), 'error'); + } + }); + }) + }, '\u{1F504} Sync from DNS Guard'); + + actionsBar.appendChild(syncBtn); + + if (status.vortex_last_sync) { + actionsBar.appendChild(E('span', { + style: 'display:flex;align-items:center;color:#666;font-size:0.9em;' + }, 'Last sync: ' + status.vortex_last_sync)); + } + + container.appendChild(actionsBar); + + // Recent Alerts Table + var alertsSection = E('div', { + style: 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 2px 4px rgba(0,0,0,0.1);' + }); + + alertsSection.appendChild(E('h3', { style: 'margin:0 0 16px 0;font-size:1.1em;' }, + '\u{1F6A8} Recent DNS Guard Alerts')); + + if (alerts.length === 0) { + alertsSection.appendChild(E('p', { style: 'color:#666;font-style:italic;' }, + 'No alerts from DNS Guard')); + } else { + var table = E('table', { + class: 'table', + style: 'width:100%;border-collapse:collapse;' + }); + + // Header + table.appendChild(E('tr', { + style: 'background:#f8f9fa;' + }, [ + E('th', { style: 'padding:10px;text-align:left;border-bottom:2px solid #dee2e6;' }, 'Domain'), + E('th', { style: 'padding:10px;text-align:left;border-bottom:2px solid #dee2e6;' }, 'Type'), + E('th', { style: 'padding:10px;text-align:left;border-bottom:2px solid #dee2e6;' }, 'Confidence'), + E('th', { style: 'padding:10px;text-align:left;border-bottom:2px solid #dee2e6;' }, 'Client'), + E('th', { style: 'padding:10px;text-align:left;border-bottom:2px solid #dee2e6;' }, 'Reason') + ])); + + // Rows + alerts.forEach(function(alert) { + table.appendChild(E('tr', { style: 'border-bottom:1px solid #e0e0e0;' }, [ + E('td', { style: 'padding:10px;font-family:monospace;font-size:0.9em;' }, alert.domain || '-'), + E('td', { style: 'padding:10px;' }, typeBadge(alert.type)), + E('td', { style: 'padding:10px;' }, confidenceBar(alert.confidence || 0)), + E('td', { style: 'padding:10px;font-family:monospace;color:#666;' }, alert.client || '-'), + E('td', { style: 'padding:10px;font-size:0.85em;color:#666;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' }, alert.reason || '-') + ])); + }); + + alertsSection.appendChild(table); + } + + container.appendChild(alertsSection); + + // Info box + container.appendChild(E('div', { + style: 'margin-top:24px;padding:16px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);' + + 'border-radius:8px;color:#fff;' + }, [ + E('h4', { style: 'margin:0 0 8px 0;' }, '\u{1F9E0} AI-Powered Detection'), + E('p', { style: 'margin:0;opacity:0.9;font-size:0.9em;' }, [ + 'DNS Guard uses LocalAI to detect DGA domains, DNS tunneling, and other anomalies. ', + 'Detections are automatically imported into Vortex Firewall for DNS-level blocking.' + ]) + ])); + + return container; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/mesh.js b/package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/mesh.js new file mode 100644 index 00000000..b2868d44 --- /dev/null +++ b/package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/mesh.js @@ -0,0 +1,306 @@ +'use strict'; +'require view'; +'require rpc'; +'require poll'; +'require ui'; + +var callMeshStatus = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'mesh_status', + expect: {} +}); + +var callMeshReceived = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'mesh_received', + params: ['limit'], + expect: { iocs: [] } +}); + +var callMeshPeers = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'mesh_peers', + expect: { peers: [] } +}); + +var callMeshPublish = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'mesh_publish', + expect: {} +}); + +var callMeshSync = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'mesh_sync', + expect: {} +}); + +function severityBadge(severity) { + var colors = { + 'critical': '#dc3545', + 'high': '#fd7e14', + 'medium': '#ffc107', + 'low': '#28a745' + }; + var color = colors[severity] || '#6c757d'; + return E('span', { + style: 'display:inline-block;padding:2px 8px;border-radius:4px;' + + 'background:' + color + ';color:#fff;font-size:0.85em;font-weight:500;' + }, severity || 'unknown'); +} + +function trustBadge(trust) { + var colors = { + 'direct': '#28a745', + 'transitive': '#17a2b8', + 'unknown': '#6c757d' + }; + var icons = { + 'direct': '\u{2705}', + 'transitive': '\u{1F517}', + 'unknown': '\u{2753}' + }; + var color = colors[trust] || '#6c757d'; + var icon = icons[trust] || ''; + return E('span', { + style: 'display:inline-block;padding:2px 8px;border-radius:4px;' + + 'background:' + color + ';color:#fff;font-size:0.85em;font-weight:500;' + }, icon + ' ' + (trust || 'unknown')); +} + +return view.extend({ + load: function() { + return Promise.all([ + callMeshStatus(), + callMeshReceived(50), + callMeshPeers() + ]); + }, + + render: function(data) { + var status = data[0] || {}; + var received = data[1] || []; + var peers = data[2] || []; + + var container = E('div', { class: 'cbi-map' }); + + // Header + container.appendChild(E('h2', { class: 'cbi-section-title' }, [ + '\u{1F310} Mesh Threat Sharing' + ])); + + // Check if available + if (!status.available) { + container.appendChild(E('div', { + style: 'padding:24px;background:#f8f9fa;border-radius:8px;text-align:center;' + }, [ + E('p', { style: 'font-size:1.2em;color:#666;' }, + '\u{26A0} Mesh threat sharing requires secubox-p2p package'), + E('p', { style: 'color:#999;' }, + 'Install secubox-p2p to enable distributed threat intelligence') + ])); + return container; + } + + // Status Cards Row + var cardsRow = E('div', { + style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:24px;' + }); + + // Status Card + var statusColor = status.enabled ? '#28a745' : '#dc3545'; + cardsRow.appendChild(E('div', { + style: 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 2px 4px rgba(0,0,0,0.1);' + }, [ + E('div', { style: 'font-size:0.85em;color:#666;margin-bottom:4px;' }, 'Mesh Status'), + E('div', { style: 'font-size:1.5em;font-weight:600;color:' + statusColor + ';' }, + status.enabled ? '\u{2705} Active' : '\u{1F534} Inactive') + ])); + + // Local IOCs Card + cardsRow.appendChild(E('div', { + style: 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 2px 4px rgba(0,0,0,0.1);' + }, [ + E('div', { style: 'font-size:0.85em;color:#666;margin-bottom:4px;' }, 'Local IOCs'), + E('div', { style: 'font-size:2em;font-weight:600;color:#17a2b8;' }, + String(status.local_iocs || 0)) + ])); + + // Received IOCs Card + cardsRow.appendChild(E('div', { + style: 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 2px 4px rgba(0,0,0,0.1);' + }, [ + E('div', { style: 'font-size:0.85em;color:#666;margin-bottom:4px;' }, 'Received'), + E('div', { style: 'font-size:2em;font-weight:600;color:#fd7e14;' }, + String(status.received_iocs || 0)) + ])); + + // Applied IOCs Card + cardsRow.appendChild(E('div', { + style: 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 2px 4px rgba(0,0,0,0.1);' + }, [ + E('div', { style: 'font-size:0.85em;color:#666;margin-bottom:4px;' }, 'Applied'), + E('div', { style: 'font-size:2em;font-weight:600;color:#28a745;' }, + String(status.applied_iocs || 0)) + ])); + + // Vortex Shared Card + cardsRow.appendChild(E('div', { + style: 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 2px 4px rgba(0,0,0,0.1);' + }, [ + E('div', { style: 'font-size:0.85em;color:#666;margin-bottom:4px;' }, 'Domains Shared'), + E('div', { style: 'font-size:2em;font-weight:600;color:#6f42c1;' }, + String(status.vortex_shared || 0)) + ])); + + // Peers Card + cardsRow.appendChild(E('div', { + style: 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 2px 4px rgba(0,0,0,0.1);' + }, [ + E('div', { style: 'font-size:0.85em;color:#666;margin-bottom:4px;' }, 'Peer Contributors'), + E('div', { style: 'font-size:2em;font-weight:600;color:#007bff;' }, + String(status.peer_contributors || 0)) + ])); + + container.appendChild(cardsRow); + + // Actions Bar + var actionsBar = E('div', { + style: 'display:flex;gap:12px;margin-bottom:24px;' + }); + + var publishBtn = E('button', { + class: 'cbi-button cbi-button-action', + click: ui.createHandlerFn(this, function() { + return callMeshPublish().then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', '\u{2705} Published ' + result.published + ' IOCs to mesh'), 'info'); + window.location.reload(); + } else { + ui.addNotification(null, E('p', '\u{274C} ' + result.message), 'error'); + } + }); + }) + }, '\u{1F4E4} Publish to Mesh'); + + var syncBtn = E('button', { + class: 'cbi-button cbi-button-apply', + click: ui.createHandlerFn(this, function() { + return callMeshSync().then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', '\u{2705} Applied ' + result.applied + ' IOCs from mesh'), 'info'); + window.location.reload(); + } else { + ui.addNotification(null, E('p', '\u{274C} ' + result.message), 'error'); + } + }); + }) + }, '\u{1F504} Sync from Mesh'); + + actionsBar.appendChild(publishBtn); + actionsBar.appendChild(syncBtn); + container.appendChild(actionsBar); + + // Peer Contributors Section + if (peers.length > 0) { + var peersSection = E('div', { + style: 'background:#fff;border-radius:8px;padding:16px;margin-bottom:24px;box-shadow:0 2px 4px rgba(0,0,0,0.1);' + }); + + peersSection.appendChild(E('h3', { style: 'margin:0 0 16px 0;font-size:1.1em;' }, + '\u{1F465} Peer Contributors')); + + var peersGrid = E('div', { + style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:12px;' + }); + + peers.forEach(function(peer) { + peersGrid.appendChild(E('div', { + style: 'display:flex;justify-content:space-between;align-items:center;' + + 'padding:12px;background:#f8f9fa;border-radius:6px;' + }, [ + E('div', {}, [ + E('div', { style: 'font-weight:500;font-family:monospace;font-size:0.9em;' }, + (peer.node || 'unknown').substring(0, 16) + '...'), + E('div', { style: 'margin-top:4px;' }, trustBadge(peer.trust)) + ]), + E('div', { style: 'text-align:right;' }, [ + E('div', { style: 'font-size:1.5em;font-weight:600;color:#17a2b8;' }, + String(peer.ioc_count || 0)), + E('div', { style: 'font-size:0.85em;color:#666;' }, + String(peer.applied_count || 0) + ' applied') + ]) + ])); + }); + + peersSection.appendChild(peersGrid); + container.appendChild(peersSection); + } + + // Received IOCs Table + var receivedSection = E('div', { + style: 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 2px 4px rgba(0,0,0,0.1);' + }); + + receivedSection.appendChild(E('h3', { style: 'margin:0 0 16px 0;font-size:1.1em;' }, + '\u{1F4E5} Received Threats')); + + if (received.length === 0) { + receivedSection.appendChild(E('p', { style: 'color:#666;font-style:italic;' }, + 'No threats received from mesh yet')); + } else { + var table = E('table', { + class: 'table', + style: 'width:100%;border-collapse:collapse;' + }); + + // Header + table.appendChild(E('tr', { + style: 'background:#f8f9fa;' + }, [ + E('th', { style: 'padding:10px;text-align:left;border-bottom:2px solid #dee2e6;' }, 'Target'), + E('th', { style: 'padding:10px;text-align:left;border-bottom:2px solid #dee2e6;' }, 'Severity'), + E('th', { style: 'padding:10px;text-align:left;border-bottom:2px solid #dee2e6;' }, 'Trust'), + E('th', { style: 'padding:10px;text-align:left;border-bottom:2px solid #dee2e6;' }, 'Scenario'), + E('th', { style: 'padding:10px;text-align:left;border-bottom:2px solid #dee2e6;' }, 'Status') + ])); + + // Rows + received.forEach(function(ioc) { + var target = ioc.domain || ioc.ip || '-'; + var statusIcon = ioc.applied ? '\u{2705}' : '\u{23F3}'; + var statusText = ioc.applied ? 'Applied' : 'Pending'; + + table.appendChild(E('tr', { style: 'border-bottom:1px solid #e0e0e0;' }, [ + E('td', { style: 'padding:10px;font-family:monospace;font-size:0.9em;' }, target), + E('td', { style: 'padding:10px;' }, severityBadge(ioc.severity)), + E('td', { style: 'padding:10px;' }, trustBadge(ioc.trust)), + E('td', { style: 'padding:10px;font-size:0.9em;color:#666;' }, ioc.scenario || '-'), + E('td', { style: 'padding:10px;' }, statusIcon + ' ' + statusText) + ])); + }); + + receivedSection.appendChild(table); + } + + container.appendChild(receivedSection); + + // Info box + container.appendChild(E('div', { + style: 'margin-top:24px;padding:16px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);' + + 'border-radius:8px;color:#fff;' + }, [ + E('h4', { style: 'margin:0 0 8px 0;' }, '\u{1F310} Decentralized Threat Intelligence'), + E('p', { style: 'margin:0;opacity:0.9;font-size:0.9em;' }, [ + 'Vortex domains with high confidence and hit counts are shared across the SecuBox mesh. ', + 'Threats from trusted peers are automatically applied to your local blocklist.' + ]) + ])); + + return container; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/sinkhole.js b/package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/sinkhole.js new file mode 100644 index 00000000..10f3a7e3 --- /dev/null +++ b/package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/sinkhole.js @@ -0,0 +1,368 @@ +'use strict'; +'require view'; +'require rpc'; +'require ui'; +'require poll'; +'require secubox/kiss-theme'; + +var callSinkholeStatus = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'sinkhole_status', + expect: {} +}); + +var callSinkholeEvents = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'sinkhole_events', + params: ['limit'], + expect: {} +}); + +var callSinkholeStats = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'sinkhole_stats', + expect: {} +}); + +var callSinkholeToggle = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'sinkhole_toggle', + params: ['enabled'], + expect: {} +}); + +var callSinkholeClear = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'sinkhole_clear', + expect: {} +}); + +function formatTime(ts) { + if (!ts) return '-'; + var d = new Date(ts); + return d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); +} + +function formatDate(ts) { + if (!ts) return '-'; + return ts.split('T')[0] || ts.split(' ')[0] || ts; +} + +return view.extend({ + load: function() { + return Promise.all([ + callSinkholeStatus(), + callSinkholeEvents(100), + callSinkholeStats() + ]); + }, + + renderStatusCard: function(status) { + var enabled = status.enabled; + var httpRunning = status.http_running; + var httpsRunning = status.https_running; + var sinkholeIP = status.sinkhole_ip || '192.168.255.253'; + + var statusColor = enabled ? (httpRunning ? '#27ae60' : '#f39c12') : '#e74c3c'; + var statusText = enabled ? (httpRunning ? 'Active' : 'Starting...') : 'Disabled'; + + return E('div', { 'class': 'sink-status-card' }, [ + E('div', { 'class': 'sink-status-header' }, [ + E('div', { 'class': 'sink-status-icon', 'style': 'background:' + statusColor }, + enabled ? '\u25CF' : '\u25CB'), + E('div', { 'class': 'sink-status-info' }, [ + E('div', { 'class': 'sink-status-title' }, 'Sinkhole Server'), + E('div', { 'class': 'sink-status-text' }, statusText) + ]), + E('label', { 'class': 'sink-toggle' }, [ + E('input', { + 'type': 'checkbox', + 'checked': enabled ? 'checked' : null, + 'click': this.handleToggle.bind(this) + }), + E('span', { 'class': 'sink-toggle-slider' }) + ]) + ]), + E('div', { 'class': 'sink-status-details' }, [ + E('div', { 'class': 'sink-detail' }, [ + E('span', { 'class': 'sink-detail-label' }, 'Sinkhole IP'), + E('span', { 'class': 'sink-detail-value' }, sinkholeIP) + ]), + E('div', { 'class': 'sink-detail' }, [ + E('span', { 'class': 'sink-detail-label' }, 'HTTP Server'), + E('span', { 'class': 'sink-detail-value', 'style': 'color:' + (httpRunning ? '#27ae60' : '#e74c3c') }, + httpRunning ? '\u2713 Running' : '\u2717 Stopped') + ]), + E('div', { 'class': 'sink-detail' }, [ + E('span', { 'class': 'sink-detail-label' }, 'HTTPS Server'), + E('span', { 'class': 'sink-detail-value', 'style': 'color:' + (httpsRunning ? '#27ae60' : (status.https_limited ? '#f39c12' : '#e74c3c')) }, + httpsRunning ? '\u2713 Running' : (status.https_limited ? '\u26A0 Limited' : '\u2717 Stopped')) + ]) + ]) + ]); + }, + + renderStatsCards: function(status, stats) { + var totalEvents = status.total_events || stats.total_events || 0; + var todayEvents = status.today_events || 0; + var uniqueClients = status.unique_clients || stats.unique_clients || 0; + var uniqueDomains = stats.unique_domains || 0; + + return E('div', { 'class': 'sink-stats' }, [ + E('div', { 'class': 'sink-stat-card' }, [ + E('div', { 'class': 'sink-stat-value', 'data-stat': 'total' }, String(totalEvents)), + E('div', { 'class': 'sink-stat-label' }, 'Total Events') + ]), + E('div', { 'class': 'sink-stat-card' }, [ + E('div', { 'class': 'sink-stat-value', 'data-stat': 'today' }, String(todayEvents)), + E('div', { 'class': 'sink-stat-label' }, 'Today') + ]), + E('div', { 'class': 'sink-stat-card sink-stat-alert' }, [ + E('div', { 'class': 'sink-stat-value', 'data-stat': 'clients' }, String(uniqueClients)), + E('div', { 'class': 'sink-stat-label' }, 'Infected Clients') + ]), + E('div', { 'class': 'sink-stat-card' }, [ + E('div', { 'class': 'sink-stat-value', 'data-stat': 'domains' }, String(uniqueDomains)), + E('div', { 'class': 'sink-stat-label' }, 'Unique Domains') + ]) + ]); + }, + + renderTopClients: function(stats) { + var clients = stats.top_clients || []; + + if (clients.length === 0) { + return E('div', { 'class': 'sink-section' }, [ + E('h3', {}, '\u26A0 Potentially Infected Clients'), + E('p', { 'style': 'color:#999;text-align:center;padding:20px' }, 'No infected clients detected') + ]); + } + + var rows = clients.map(function(c) { + return E('tr', {}, [ + E('td', { 'style': 'font-family:monospace' }, c.ip || '-'), + E('td', { 'style': 'text-align:right;font-weight:600;color:#e74c3c' }, String(c.events || 0)), + E('td', {}, [ + E('div', { 'class': 'sink-bar' }, [ + E('div', { 'class': 'sink-bar-fill', 'style': 'width:' + Math.min(100, (c.events / (clients[0].events || 1)) * 100) + '%' }) + ]) + ]) + ]); + }); + + return E('div', { 'class': 'sink-section sink-section-alert' }, [ + E('h3', {}, '\u26A0 Potentially Infected Clients'), + E('p', { 'class': 'sink-section-desc' }, 'These clients attempted to connect to blocked malicious domains.'), + E('table', { 'class': 'table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Client IP'), + E('th', { 'style': 'text-align:right' }, 'Events'), + E('th', { 'style': 'width:40%' }, 'Activity') + ])), + E('tbody', {}, rows) + ]) + ]); + }, + + renderTopDomains: function(stats) { + var domains = stats.top_domains || []; + + if (domains.length === 0) { + return E('div', { 'class': 'sink-section' }, [ + E('h3', {}, '\uD83D\uDEE1 Top Blocked Domains'), + E('p', { 'style': 'color:#999;text-align:center;padding:20px' }, 'No blocked connections yet') + ]); + } + + var rows = domains.map(function(d) { + return E('tr', {}, [ + E('td', { 'style': 'font-family:monospace;font-size:12px;word-break:break-all' }, d.domain || '-'), + E('td', { 'style': 'text-align:right;font-weight:600' }, String(d.events || 0)) + ]); + }); + + return E('div', { 'class': 'sink-section' }, [ + E('h3', {}, '\uD83D\uDEE1 Top Blocked Domains'), + E('table', { 'class': 'table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Domain'), + E('th', { 'style': 'text-align:right' }, 'Hits') + ])), + E('tbody', {}, rows) + ]) + ]); + }, + + renderEventLog: function(events) { + var eventList = events.events || []; + + if (eventList.length === 0) { + return E('div', { 'class': 'sink-section' }, [ + E('h3', {}, '\uD83D\uDCCB Event Log'), + E('p', { 'style': 'color:#999;text-align:center;padding:20px' }, + 'No events recorded. When clients try to reach blocked domains, their connections will appear here.') + ]); + } + + var rows = eventList.slice(0, 50).map(function(e) { + return E('tr', {}, [ + E('td', { 'style': 'white-space:nowrap;font-size:11px;color:#999' }, [ + E('div', {}, formatDate(e.timestamp)), + E('div', {}, formatTime(e.timestamp)) + ]), + E('td', { 'style': 'font-family:monospace;font-size:12px' }, e.client_ip || '-'), + E('td', { 'style': 'font-family:monospace;font-size:11px;word-break:break-all' }, e.domain || '-'), + E('td', {}, [ + E('span', { + 'class': 'sink-type-badge', + 'style': 'background:' + (e.event_type === 'https' ? '#9b59b6' : '#3498db') + }, (e.event_type || 'http').toUpperCase()) + ]), + E('td', { 'style': 'font-size:11px;color:#666' }, e.details || '-') + ]); + }); + + var self = this; + + return E('div', { 'class': 'sink-section' }, [ + E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;margin-bottom:16px' }, [ + E('h3', { 'style': 'margin:0' }, '\uD83D\uDCCB Event Log'), + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'style': 'padding:4px 12px;font-size:12px', + 'click': function() { self.handleClearEvents(); } + }, '\uD83D\uDDD1 Clear Log') + ]), + E('div', { 'class': 'sink-events-scroll' }, [ + E('table', { 'class': 'table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', { 'style': 'width:90px' }, 'Time'), + E('th', { 'style': 'width:120px' }, 'Client'), + E('th', {}, 'Domain'), + E('th', { 'style': 'width:60px' }, 'Type'), + E('th', {}, 'Details') + ])), + E('tbody', { 'id': 'events-tbody' }, rows) + ]) + ]) + ]); + }, + + handleToggle: function(ev) { + var enabled = ev.target.checked ? 1 : 0; + var self = this; + + callSinkholeToggle(enabled).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', {}, result.message), 'success'); + } else { + ui.addNotification(null, E('p', {}, result.message || 'Failed to toggle sinkhole'), 'error'); + ev.target.checked = !ev.target.checked; + } + }).catch(function(err) { + ui.addNotification(null, E('p', {}, 'Error: ' + err.message), 'error'); + ev.target.checked = !ev.target.checked; + }); + }, + + handleClearEvents: function() { + var self = this; + + ui.showModal('Clear Event Log', [ + E('p', {}, 'Are you sure you want to clear all sinkhole events? This action cannot be undone.'), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'click': function() { + ui.hideModal(); + callSinkholeClear().then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', {}, result.message), 'success'); + window.location.reload(); + } else { + ui.addNotification(null, E('p', {}, result.message || 'Failed to clear events'), 'error'); + } + }); + } + }, 'Clear All') + ]) + ]); + }, + + render: function(data) { + var status = data[0] || {}; + var events = data[1] || {}; + var stats = data[2] || {}; + + var self = this; + + // Start polling + poll.add(function() { + return callSinkholeStatus().then(function(s) { + var totalEl = document.querySelector('[data-stat="total"]'); + var todayEl = document.querySelector('[data-stat="today"]'); + var clientsEl = document.querySelector('[data-stat="clients"]'); + if (totalEl) totalEl.textContent = String(s.total_events || 0); + if (todayEl) todayEl.textContent = String(s.today_events || 0); + if (clientsEl) clientsEl.textContent = String(s.unique_clients || 0); + }); + }, 15); + + var dashboard = E('div', { 'class': 'sink-dashboard' }, [ + E('style', {}, [ + '.sink-dashboard { max-width: 1200px; }', + '.sink-status-card { background: var(--kiss-card, #fff); border-radius: 12px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border: 1px solid var(--kiss-line, #eee); }', + '.sink-status-header { display: flex; align-items: center; gap: 16px; margin-bottom: 16px; }', + '.sink-status-icon { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 24px; }', + '.sink-status-title { font-size: 18px; font-weight: 600; }', + '.sink-status-text { font-size: 14px; color: var(--kiss-muted, #666); }', + '.sink-status-info { flex: 1; }', + '.sink-status-details { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; padding-top: 16px; border-top: 1px solid var(--kiss-line, #eee); }', + '.sink-detail { text-align: center; }', + '.sink-detail-label { font-size: 11px; color: var(--kiss-muted, #666); text-transform: uppercase; }', + '.sink-detail-value { font-size: 14px; font-weight: 500; margin-top: 4px; }', + '.sink-toggle { position: relative; width: 50px; height: 26px; }', + '.sink-toggle input { opacity: 0; width: 0; height: 0; }', + '.sink-toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #ccc; border-radius: 26px; transition: 0.3s; }', + '.sink-toggle-slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 3px; bottom: 3px; background: #fff; border-radius: 50%; transition: 0.3s; }', + '.sink-toggle input:checked + .sink-toggle-slider { background: #27ae60; }', + '.sink-toggle input:checked + .sink-toggle-slider:before { transform: translateX(24px); }', + '.sink-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 20px; }', + '.sink-stat-card { background: var(--kiss-card, #fff); border-radius: 8px; padding: 20px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border: 1px solid var(--kiss-line, #eee); }', + '.sink-stat-alert { background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: #fff; }', + '.sink-stat-alert .sink-stat-label { color: rgba(255,255,255,0.8); }', + '.sink-stat-value { font-size: 32px; font-weight: 700; }', + '.sink-stat-label { font-size: 12px; color: var(--kiss-muted, #666); text-transform: uppercase; margin-top: 4px; }', + '.sink-section { background: var(--kiss-card, #fff); border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border: 1px solid var(--kiss-line, #eee); }', + '.sink-section-alert { border-left: 4px solid #e74c3c; }', + '.sink-section h3 { margin: 0 0 16px 0; font-size: 16px; font-weight: 600; }', + '.sink-section-desc { color: var(--kiss-muted, #666); font-size: 13px; margin-bottom: 16px; }', + '.sink-bar { height: 8px; background: var(--kiss-line, #eee); border-radius: 4px; overflow: hidden; }', + '.sink-bar-fill { height: 100%; background: linear-gradient(90deg, #e74c3c, #f39c12); }', + '.sink-events-scroll { max-height: 400px; overflow-y: auto; }', + '.sink-type-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; color: #fff; font-size: 10px; font-weight: 600; }', + '.table { width: 100%; border-collapse: collapse; }', + '.table th, .table td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--kiss-line, #eee); }', + '.table th { background: var(--kiss-bg2, #f8f9fa); font-weight: 600; font-size: 11px; text-transform: uppercase; color: var(--kiss-muted, #666); }', + '@media (max-width: 768px) { .sink-stats { grid-template-columns: repeat(2, 1fr); } .sink-status-details { grid-template-columns: 1fr; } }' + ].join('\n')), + E('h2', { 'style': 'margin-bottom: 8px' }, '\uD83D\uDD73 Sinkhole Server'), + E('p', { 'style': 'color: var(--kiss-muted, #666); margin-bottom: 24px' }, + 'Capture and analyze connections to blocked malicious domains. Identify infected clients and investigate malware behavior.'), + this.renderStatusCard(status), + this.renderStatsCards(status, stats), + E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 20px;' }, [ + this.renderTopClients(stats), + this.renderTopDomains(stats) + ]), + this.renderEventLog(events) + ]); + + return KissTheme.wrap([dashboard], 'admin/secubox/security/vortex-firewall/sinkhole'); + }, + + handleSave: null, + handleSaveApply: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-vortex-firewall/root/usr/share/luci/menu.d/luci-app-vortex-firewall.json b/package/secubox/luci-app-vortex-firewall/root/usr/share/luci/menu.d/luci-app-vortex-firewall.json index 4b09b6aa..29148962 100644 --- a/package/secubox/luci-app-vortex-firewall/root/usr/share/luci/menu.d/luci-app-vortex-firewall.json +++ b/package/secubox/luci-app-vortex-firewall/root/usr/share/luci/menu.d/luci-app-vortex-firewall.json @@ -3,11 +3,42 @@ "title": "Vortex Firewall", "order": 40, "action": { - "type": "view", - "path": "vortex-firewall/overview" + "type": "firstchild" }, "depends": { "acl": ["luci-app-vortex-firewall"] } + }, + "admin/secubox/security/vortex-firewall/overview": { + "title": "Overview", + "order": 1, + "action": { + "type": "view", + "path": "vortex-firewall/overview" + } + }, + "admin/secubox/security/vortex-firewall/sinkhole": { + "title": "Sinkhole", + "order": 2, + "action": { + "type": "view", + "path": "vortex-firewall/sinkhole" + } + }, + "admin/secubox/security/vortex-firewall/dnsguard": { + "title": "DNS Guard", + "order": 3, + "action": { + "type": "view", + "path": "vortex-firewall/dnsguard" + } + }, + "admin/secubox/security/vortex-firewall/mesh": { + "title": "Mesh", + "order": 4, + "action": { + "type": "view", + "path": "vortex-firewall/mesh" + } } } diff --git a/package/secubox/luci-app-vortex-firewall/root/usr/share/rpcd/acl.d/luci-app-vortex-firewall.json b/package/secubox/luci-app-vortex-firewall/root/usr/share/rpcd/acl.d/luci-app-vortex-firewall.json index bc785585..ed6fba78 100644 --- a/package/secubox/luci-app-vortex-firewall/root/usr/share/rpcd/acl.d/luci-app-vortex-firewall.json +++ b/package/secubox/luci-app-vortex-firewall/root/usr/share/rpcd/acl.d/luci-app-vortex-firewall.json @@ -3,12 +3,27 @@ "description": "Grant access to Vortex DNS Firewall LuCI app", "read": { "ubus": { - "luci.vortex-firewall": ["status", "get_stats", "get_feeds", "get_blocked", "search"] + "luci.vortex-firewall": [ + "status", + "get_stats", + "get_feeds", + "get_blocked", + "search", + "sinkhole_status", + "sinkhole_events", + "sinkhole_stats" + ] } }, "write": { "ubus": { - "luci.vortex-firewall": ["update_feeds", "block_domain", "unblock_domain"] + "luci.vortex-firewall": [ + "update_feeds", + "block_domain", + "unblock_domain", + "sinkhole_toggle", + "sinkhole_clear" + ] } } } diff --git a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/uplinks.js b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/uplinks.js new file mode 100644 index 00000000..5ec405c6 --- /dev/null +++ b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/uplinks.js @@ -0,0 +1,613 @@ +'use strict'; +'require view'; +'require poll'; +'require dom'; +'require ui'; +'require wireguard-dashboard/api as API'; +'require secubox/kiss-theme'; + +return view.extend({ + title: _('WireGuard Uplinks'), + pollInterval: 10, + pollActive: true, + + load: function() { + return Promise.all([ + API.getUplinkStatus(), + API.getUplinks(), + API.getPeers() + ]); + }, + + // Handle offer uplink + handleOfferUplink: function(ev) { + var self = this; + + ui.showModal(_('Offer Uplink'), [ + E('p', {}, _('Offer your internet connection to mesh peers as a backup uplink.')), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Bandwidth (Mbps)')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'number', + 'id': 'offer-bandwidth', + 'class': 'cbi-input-text', + 'value': '100', + 'min': '1', + 'max': '10000' + }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Latency (ms)')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'number', + 'id': 'offer-latency', + 'class': 'cbi-input-text', + 'value': '10', + 'min': '1', + 'max': '1000' + }) + ]) + ]), + E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'class': 'btn cbi-button-action', + 'click': function() { + var bandwidth = document.getElementById('offer-bandwidth').value; + var latency = document.getElementById('offer-latency').value; + + ui.hideModal(); + ui.showModal(_('Offering Uplink'), [ + E('p', { 'class': 'spinning' }, _('Advertising uplink to mesh...')) + ]); + + API.offerUplink(bandwidth, latency).then(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', result.message || _('Uplink offered successfully')), 'info'); + window.location.reload(); + } else { + ui.addNotification(null, E('p', result.error || _('Failed to offer uplink')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error'); + }); + } + }, _('Offer Uplink')) + ]) + ]); + }, + + // Handle withdraw uplink + handleWithdrawUplink: function(ev) { + ui.showModal(_('Withdrawing Uplink'), [ + E('p', { 'class': 'spinning' }, _('Withdrawing uplink offer from mesh...')) + ]); + + API.withdrawUplink().then(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', result.message || _('Uplink withdrawn')), 'info'); + window.location.reload(); + } else { + ui.addNotification(null, E('p', result.error || _('Failed to withdraw uplink')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error'); + }); + }, + + // Handle add uplink from peer offer + handleAddUplink: function(offer, ev) { + var self = this; + + ui.showModal(_('Add Uplink'), [ + E('p', {}, _('Use this mesh peer as a backup internet uplink.')), + E('div', { 'style': 'background: #f8f9fa; padding: 1em; border-radius: 4px; margin: 1em 0;' }, [ + E('div', {}, [ + E('strong', {}, _('Node: ')), + E('span', {}, offer.node_id || 'Unknown') + ]), + E('div', {}, [ + E('strong', {}, _('Bandwidth: ')), + E('span', {}, (offer.bandwidth || '?') + ' Mbps') + ]), + E('div', {}, [ + E('strong', {}, _('Latency: ')), + E('span', {}, (offer.latency || '?') + ' ms') + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Priority')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'number', + 'id': 'uplink-priority', + 'class': 'cbi-input-text', + 'value': '10', + 'min': '1', + 'max': '100' + }), + E('div', { 'class': 'cbi-value-description' }, _('Lower = higher priority for failover')) + ]) + ]), + E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'class': 'btn cbi-button-action', + 'click': function() { + var priority = document.getElementById('uplink-priority').value; + + ui.hideModal(); + ui.showModal(_('Adding Uplink'), [ + E('p', { 'class': 'spinning' }, _('Creating uplink interface...')) + ]); + + API.addUplink( + offer.public_key, + offer.endpoint, + '', // local_pubkey (auto-generated) + priority, + '1', // weight + offer.node_id + ).then(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', result.message || _('Uplink added successfully')), 'info'); + window.location.reload(); + } else { + ui.addNotification(null, E('p', result.error || _('Failed to add uplink')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error'); + }); + } + }, _('Add Uplink')) + ]) + ]); + }, + + // Handle remove uplink + handleRemoveUplink: function(uplink, ev) { + var self = this; + + ui.showModal(_('Remove Uplink'), [ + E('p', {}, _('Are you sure you want to remove this uplink?')), + E('div', { 'style': 'background: #f8f9fa; padding: 1em; border-radius: 4px; margin: 1em 0;' }, [ + E('strong', {}, _('Interface: ')), + E('code', {}, uplink.interface) + ]), + E('p', { 'style': 'color: #dc3545;' }, _('This will disconnect the backup uplink.')), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'class': 'btn cbi-button-negative', + 'click': function() { + ui.hideModal(); + ui.showModal(_('Removing Uplink'), [ + E('p', { 'class': 'spinning' }, _('Removing uplink interface...')) + ]); + + API.removeUplink(uplink.interface).then(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', result.message || _('Uplink removed')), 'info'); + window.location.reload(); + } else { + ui.addNotification(null, E('p', result.error || _('Failed to remove uplink')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error'); + }); + } + }, _('Remove')) + ]) + ]); + }, + + // Handle test uplink + handleTestUplink: function(uplink, ev) { + ui.showModal(_('Testing Uplink'), [ + E('p', { 'class': 'spinning' }, _('Testing connectivity via %s...').format(uplink.interface)) + ]); + + API.testUplink(uplink.interface, '8.8.8.8').then(function(result) { + ui.hideModal(); + if (result.reachable) { + ui.showModal(_('Uplink Test Result'), [ + E('div', { 'style': 'text-align: center; padding: 1em;' }, [ + E('div', { 'style': 'font-size: 4em; color: #28a745;' }, '✓'), + E('h3', { 'style': 'color: #28a745;' }, _('Uplink Working')), + E('p', {}, _('Latency: %s ms').format(result.latency_ms || '?')) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close')) + ]) + ]); + } else { + ui.showModal(_('Uplink Test Result'), [ + E('div', { 'style': 'text-align: center; padding: 1em;' }, [ + E('div', { 'style': 'font-size: 4em; color: #dc3545;' }, '✗'), + E('h3', { 'style': 'color: #dc3545;' }, _('Uplink Unreachable')), + E('p', {}, result.error || _('Target not reachable through this uplink')) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close')) + ]) + ]); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error'); + }); + }, + + // Handle set priority + handleSetPriority: function(uplink, ev) { + var self = this; + + ui.showModal(_('Set Priority'), [ + E('p', {}, _('Set failover priority for this uplink.')), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Priority')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'number', + 'id': 'set-priority', + 'class': 'cbi-input-text', + 'value': uplink.priority || '10', + 'min': '1', + 'max': '100' + }), + E('div', { 'class': 'cbi-value-description' }, _('Lower = higher priority')) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Weight')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'number', + 'id': 'set-weight', + 'class': 'cbi-input-text', + 'value': uplink.weight || '1', + 'min': '1', + 'max': '100' + }), + E('div', { 'class': 'cbi-value-description' }, _('Load balancing weight')) + ]) + ]), + E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'class': 'btn cbi-button-action', + 'click': function() { + var priority = document.getElementById('set-priority').value; + var weight = document.getElementById('set-weight').value; + + ui.hideModal(); + API.setUplinkPriority(uplink.interface, priority, weight).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', _('Priority updated')), 'info'); + window.location.reload(); + } else { + ui.addNotification(null, E('p', result.error || _('Failed to update priority')), 'error'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error'); + }); + } + }, _('Save')) + ]) + ]); + }, + + // Toggle failover + handleToggleFailover: function(enabled, ev) { + API.setUplinkFailover(enabled ? '1' : '0').then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', enabled ? _('Auto-failover enabled') : _('Auto-failover disabled')), 'info'); + window.location.reload(); + } else { + ui.addNotification(null, E('p', result.error || _('Failed to update failover setting')), 'error'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error'); + }); + }, + + startPolling: function() { + var self = this; + this.pollActive = true; + + poll.add(L.bind(function() { + if (!this.pollActive) return Promise.resolve(); + + return Promise.all([ + API.getUplinkStatus(), + API.getUplinks() + ]).then(L.bind(function(results) { + var status = results[0] || {}; + var uplinksData = results[1] || []; + var uplinks = Array.isArray(uplinksData) ? uplinksData : (uplinksData.uplinks || []); + + // Update status badges + var enabledBadge = document.querySelector('.uplink-enabled-badge'); + if (enabledBadge) { + var enabled = status.enabled === '1' || status.enabled === 1; + enabledBadge.className = 'badge uplink-enabled-badge ' + (enabled ? 'badge-success' : 'badge-secondary'); + enabledBadge.textContent = enabled ? 'Enabled' : 'Disabled'; + } + + var countBadge = document.querySelector('.uplink-count'); + if (countBadge) { + countBadge.textContent = status.uplink_count || 0; + } + + var offerBadge = document.querySelector('.offer-count'); + if (offerBadge) { + var offers = status.peer_offers || []; + offerBadge.textContent = offers.length; + } + + }, this)); + }, this), this.pollInterval); + }, + + render: function(data) { + var self = this; + var status = data[0] || {}; + var uplinksData = data[1] || []; + var peersData = data[2] || []; + var uplinks = Array.isArray(uplinksData) ? uplinksData : (uplinksData.uplinks || []); + var peers = Array.isArray(peersData) ? peersData : (peersData.peers || []); + + var enabled = status.enabled === '1' || status.enabled === 1; + var offering = status.offering === '1' || status.offering === 1; + var autoFailover = status.auto_failover === '1' || status.auto_failover === 1; + var peerOffers = status.peer_offers || []; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), + E('h2', {}, _('WireGuard Mesh Uplinks')), + E('div', { 'class': 'cbi-map-descr' }, + _('Use WireGuard mesh peers as backup internet uplinks with automatic failover via MWAN3.')), + + // Status Cards + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1em; margin-bottom: 1em;' }, [ + // Uplink Status + E('div', { 'style': 'background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 1.5em; border-radius: 12px;' }, [ + E('div', { 'style': 'font-size: 0.9em; opacity: 0.9;' }, _('Uplink Status')), + E('div', { 'style': 'font-size: 2em; font-weight: bold;' }, [ + E('span', { 'class': 'badge uplink-enabled-badge ' + (enabled ? 'badge-success' : 'badge-secondary'), 'style': 'font-size: 0.5em;' }, + enabled ? 'Enabled' : 'Disabled') + ]), + E('div', { 'style': 'font-size: 0.85em; margin-top: 0.5em;' }, + autoFailover ? '✓ Auto-failover active' : '○ Manual mode') + ]), + + // Active Uplinks + E('div', { 'style': 'background: linear-gradient(135deg, #11998e, #38ef7d); color: white; padding: 1.5em; border-radius: 12px;' }, [ + E('div', { 'style': 'font-size: 0.9em; opacity: 0.9;' }, _('Active Uplinks')), + E('div', { 'style': 'font-size: 2.5em; font-weight: bold;' }, [ + E('span', { 'class': 'uplink-count' }, status.uplink_count || 0) + ]), + E('div', { 'style': 'font-size: 0.85em; margin-top: 0.5em;' }, + _('Configured backup routes')) + ]), + + // Peer Offers + E('div', { 'style': 'background: linear-gradient(135deg, #f093fb, #f5576c); color: white; padding: 1.5em; border-radius: 12px;' }, [ + E('div', { 'style': 'font-size: 0.9em; opacity: 0.9;' }, _('Mesh Offers')), + E('div', { 'style': 'font-size: 2.5em; font-weight: bold;' }, [ + E('span', { 'class': 'offer-count' }, peerOffers.length) + ]), + E('div', { 'style': 'font-size: 0.85em; margin-top: 0.5em;' }, + _('Available from peers')) + ]), + + // Provider Status + E('div', { 'style': 'background: linear-gradient(135deg, #4facfe, #00f2fe); color: white; padding: 1.5em; border-radius: 12px;' }, [ + E('div', { 'style': 'font-size: 0.9em; opacity: 0.9;' }, _('Provider Mode')), + E('div', { 'style': 'font-size: 2em; font-weight: bold;' }, [ + E('span', {}, offering ? '📡 Offering' : '📴 Not Offering') + ]), + E('div', { 'style': 'font-size: 0.85em; margin-top: 0.5em;' }, + offering ? _('Sharing uplink with mesh') : _('Not sharing uplink')) + ]) + ]) + ]), + + // Quick Actions + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 0.5em; margin-bottom: 1em;' }, [ + offering ? + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'click': L.bind(this.handleWithdrawUplink, this) + }, '📴 ' + _('Stop Offering')) : + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': L.bind(this.handleOfferUplink, this) + }, '📡 ' + _('Offer My Uplink')), + + autoFailover ? + E('button', { + 'class': 'cbi-button', + 'click': L.bind(this.handleToggleFailover, this, false) + }, '⏹ ' + _('Disable Auto-Failover')) : + E('button', { + 'class': 'cbi-button cbi-button-apply', + 'click': L.bind(this.handleToggleFailover, this, true) + }, '▶ ' + _('Enable Auto-Failover')) + ]) + ]), + + // Active Uplinks Table + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Configured Uplinks')), + uplinks.length > 0 ? + E('div', { 'class': 'table-wrapper' }, [ + E('table', { 'class': 'table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, _('Interface')), + E('th', {}, _('Peer')), + E('th', {}, _('Endpoint')), + E('th', {}, _('Priority')), + E('th', {}, _('Status')), + E('th', {}, _('Actions')) + ]) + ]), + E('tbody', {}, + uplinks.map(function(uplink) { + var statusColor = uplink.status === 'active' ? '#28a745' : + uplink.status === 'testing' ? '#ffc107' : '#6c757d'; + var statusIcon = uplink.status === 'active' ? '✓' : + uplink.status === 'testing' ? '~' : '?'; + + return E('tr', {}, [ + E('td', {}, [ + E('code', {}, uplink.interface || 'wgup?') + ]), + E('td', {}, [ + E('code', { 'style': 'font-size: 0.85em;' }, + API.shortenKey(uplink.peer_pubkey, 12)) + ]), + E('td', {}, uplink.endpoint || '-'), + E('td', {}, [ + E('span', { 'class': 'badge', 'style': 'background: #6c757d; color: white;' }, + 'P' + (uplink.priority || 10) + ' W' + (uplink.weight || 1)) + ]), + E('td', {}, [ + E('span', { + 'class': 'badge', + 'style': 'background: ' + statusColor + '; color: white;' + }, statusIcon + ' ' + (uplink.status || 'unknown')) + ]), + E('td', {}, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'margin: 2px; padding: 4px 8px;', + 'click': L.bind(self.handleTestUplink, self, uplink) + }, '🔍 ' + _('Test')), + E('button', { + 'class': 'cbi-button', + 'style': 'margin: 2px; padding: 4px 8px;', + 'click': L.bind(self.handleSetPriority, self, uplink) + }, '⚙ ' + _('Priority')), + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'style': 'margin: 2px; padding: 4px 8px;', + 'click': L.bind(self.handleRemoveUplink, self, uplink) + }, '✗ ' + _('Remove')) + ]) + ]); + }) + ) + ]) + ]) : + E('div', { 'style': 'text-align: center; padding: 2em; background: #f8f9fa; border-radius: 8px;' }, [ + E('div', { 'style': 'font-size: 3em; margin-bottom: 0.5em;' }, '🔗'), + E('h4', {}, _('No Uplinks Configured')), + E('p', { 'style': 'color: #666;' }, + _('Add uplinks from mesh peer offers below, or wait for peers to advertise their uplinks.')) + ]) + ]), + + // Available Peer Offers + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Available Peer Offers')), + peerOffers.length > 0 ? + E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1em;' }, + peerOffers.map(function(offer) { + return E('div', { + 'style': 'background: white; border: 1px solid #ddd; border-radius: 12px; padding: 1.5em; ' + + 'box-shadow: 0 2px 4px rgba(0,0,0,0.05);' + }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1em;' }, [ + E('div', { 'style': 'font-weight: bold; font-size: 1.1em;' }, [ + '🌐 ', + offer.node_id || 'Mesh Peer' + ]), + E('span', { + 'class': 'badge', + 'style': 'background: #28a745; color: white;' + }, 'Available') + ]), + E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 0.5em; margin-bottom: 1em;' }, [ + E('div', {}, [ + E('div', { 'style': 'color: #666; font-size: 0.85em;' }, _('Bandwidth')), + E('div', { 'style': 'font-weight: bold;' }, (offer.bandwidth || '?') + ' Mbps') + ]), + E('div', {}, [ + E('div', { 'style': 'color: #666; font-size: 0.85em;' }, _('Latency')), + E('div', { 'style': 'font-weight: bold;' }, (offer.latency || '?') + ' ms') + ]) + ]), + E('div', { 'style': 'font-size: 0.85em; color: #666; margin-bottom: 1em;' }, [ + E('code', {}, API.shortenKey(offer.public_key, 16) || 'N/A') + ]), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'width: 100%;', + 'click': L.bind(self.handleAddUplink, self, offer) + }, '+ ' + _('Use as Uplink')) + ]); + }) + ) : + E('div', { 'style': 'text-align: center; padding: 2em; background: #f8f9fa; border-radius: 8px;' }, [ + E('div', { 'style': 'font-size: 3em; margin-bottom: 0.5em;' }, '📡'), + E('h4', {}, _('No Peer Offers Available')), + E('p', { 'style': 'color: #666;' }, + _('Mesh peers can offer their internet connection as backup uplinks. ' + + 'Offers will appear here when peers advertise via gossip protocol.')) + ]) + ]), + + // Help Section + E('div', { 'class': 'cbi-section', 'style': 'background: #e7f3ff; padding: 1.5em; border-radius: 8px; margin-top: 1em;' }, [ + E('h4', { 'style': 'margin-top: 0;' }, '💡 ' + _('How Mesh Uplinks Work')), + E('ul', { 'style': 'margin: 0; padding-left: 1.5em;' }, [ + E('li', {}, _('Mesh peers can share their internet connection as backup uplinks')), + E('li', {}, _('Traffic is routed through WireGuard tunnels to the offering peer')), + E('li', {}, _('MWAN3 handles automatic failover when primary WAN fails')), + E('li', {}, _('Offers are advertised via the P2P gossip protocol')), + E('li', {}, _('Use priority settings to control failover order')) + ]) + ]) + ]); + + // Start polling + this.startPolling(); + + return KissTheme.wrap([view], 'admin/services/wireguard/uplinks'); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/api.js b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/api.js index 3a6312f1..e2ad945a 100644 --- a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/api.js +++ b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/api.js @@ -134,6 +134,67 @@ var callDeleteEndpoint = rpc.declare({ expect: { } }); +// Uplink API calls (Reverse MWAN WireGuard) +var callUplinkStatus = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'uplink_status', + expect: { } +}); + +var callGetUplinks = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'uplinks', + expect: { uplinks: [] } +}); + +var callAddUplink = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'add_uplink', + params: ['peer_pubkey', 'endpoint', 'local_pubkey', 'priority', 'weight', 'node_id'], + expect: { } +}); + +var callRemoveUplink = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'remove_uplink', + params: ['interface'], + expect: { } +}); + +var callTestUplink = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'test_uplink', + params: ['interface', 'target'], + expect: { } +}); + +var callOfferUplink = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'offer_uplink', + params: ['bandwidth', 'latency'], + expect: { } +}); + +var callWithdrawUplink = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'withdraw_uplink', + expect: { } +}); + +var callSetUplinkPriority = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'set_uplink_priority', + params: ['interface', 'priority', 'weight'], + expect: { } +}); + +var callSetUplinkFailover = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'set_uplink_failover', + params: ['enabled'], + expect: { } +}); + function buildEndpointSelector(endpointData, inputId) { var endpoints = (endpointData || {}).endpoints || []; var defaultId = (endpointData || {})['default'] || ''; @@ -274,6 +335,16 @@ return baseclass.extend({ setEndpoint: callSetEndpoint, setDefaultEndpoint: callSetDefaultEndpoint, deleteEndpoint: callDeleteEndpoint, + // Uplink API (Reverse MWAN) + getUplinkStatus: callUplinkStatus, + getUplinks: callGetUplinks, + addUplink: callAddUplink, + removeUplink: callRemoveUplink, + testUplink: callTestUplink, + offerUplink: callOfferUplink, + withdrawUplink: callWithdrawUplink, + setUplinkPriority: callSetUplinkPriority, + setUplinkFailover: callSetUplinkFailover, buildEndpointSelector: buildEndpointSelector, getEndpointValue: getEndpointValue, formatBytes: formatBytes, diff --git a/package/secubox/luci-app-wireguard-dashboard/root/usr/share/luci/menu.d/luci-app-wireguard-dashboard.json b/package/secubox/luci-app-wireguard-dashboard/root/usr/share/luci/menu.d/luci-app-wireguard-dashboard.json index f5f805e0..bcac2b55 100644 --- a/package/secubox/luci-app-wireguard-dashboard/root/usr/share/luci/menu.d/luci-app-wireguard-dashboard.json +++ b/package/secubox/luci-app-wireguard-dashboard/root/usr/share/luci/menu.d/luci-app-wireguard-dashboard.json @@ -49,6 +49,14 @@ "path": "wireguard-dashboard/traffic" } }, + "admin/services/wireguard/uplinks": { + "title": "Mesh Uplinks", + "order": 45, + "action": { + "type": "view", + "path": "wireguard-dashboard/uplinks" + } + }, "admin/services/wireguard/config": { "title": "Configuration", "order": 50, diff --git a/package/secubox/secubox-p2p/root/usr/lib/secubox/threat-intel.sh b/package/secubox/secubox-p2p/root/usr/lib/secubox/threat-intel.sh index a8b7e0aa..476ce541 100755 --- a/package/secubox/secubox-p2p/root/usr/lib/secubox/threat-intel.sh +++ b/package/secubox/secubox-p2p/root/usr/lib/secubox/threat-intel.sh @@ -199,6 +199,47 @@ ti_collect_mitmproxy() { echo "$current_size" > "$last_collect_pos" } +# ============================================================================ +# Collection - Vortex Firewall DNS blocklist +# ============================================================================ +ti_collect_vortex() { + local blocklist_db="/var/lib/vortex-firewall/blocklist.db" + [ -f "$blocklist_db" ] || return 0 + + command -v sqlite3 >/dev/null 2>&1 || return 0 + + local now=$(date +%s) + local node_id=$(cat "$NODE_ID_FILE" 2>/dev/null || echo "unknown") + + # Query high-confidence domains with recent hits (locally-verified threats) + sqlite3 "$blocklist_db" \ + "SELECT domain, threat_type, confidence FROM domains + WHERE blocked=1 AND confidence >= 85 AND hit_count > 0 + ORDER BY hit_count DESC + LIMIT 50;" 2>/dev/null | while IFS='|' read -r domain threat_type confidence; do + + [ -z "$domain" ] && continue + + # Skip private/local domains + case "$domain" in + *.local|*.lan|localhost*|*internal*|*.home|*.localdomain) continue ;; + esac + + # Skip whitelisted + grep -q "^${domain}$" "$TI_WHITELIST" 2>/dev/null && continue + + # Map Vortex threat types to IOC severity + local severity="high" + case "$threat_type" in + malware|c2|botnet|dga|dns_tunnel) severity="critical" ;; + phishing|scam|known_bad) severity="high" ;; + adware|pup|suspicious_tld|tld_anomaly) severity="medium" ;; + esac + + echo "{\"domain\":\"$domain\",\"type\":\"block\",\"severity\":\"$severity\",\"source\":\"vortex\",\"scenario\":\"$threat_type\",\"confidence\":$confidence,\"duration\":\"${TI_IOC_TTL}s\",\"ts\":$now,\"node\":\"$node_id\",\"ttl\":$TI_IOC_TTL}" + done +} + # ============================================================================ # Collection - Aggregate and deduplicate # ============================================================================ @@ -207,36 +248,50 @@ ti_collect_all() { local tmp_file="$TI_DIR/tmp-collect-$$.json" local existing_ips="" + local existing_domains="" - # Gather existing local IOC IPs for dedup + # Gather existing local IOC IPs and domains for dedup if [ -f "$IOC_LOCAL" ] && [ "$(cat "$IOC_LOCAL")" != "[]" ]; then existing_ips=$(jsonfilter -i "$IOC_LOCAL" -e '@[*].ip' 2>/dev/null | sort -u) + existing_domains=$(jsonfilter -i "$IOC_LOCAL" -e '@[*].domain' 2>/dev/null | sort -u) fi # Collect from all sources { ti_collect_crowdsec ti_collect_mitmproxy + ti_collect_vortex } > "$tmp_file" - # Deduplicate by IP against existing and within new results + # Deduplicate by IP/domain against existing and within new results local new_iocs="[" local first=1 local seen_ips="" + local seen_domains="" while read -r ioc_line; do [ -z "$ioc_line" ] && continue + # Check for IP-based IOC local ip=$(echo "$ioc_line" | jsonfilter -e '@.ip' 2>/dev/null) - [ -z "$ip" ] && continue - - # Skip if already in local IOCs - echo "$existing_ips" | grep -q "^${ip}$" && continue - - # Skip if already seen in this batch - echo "$seen_ips" | grep -q "^${ip}$" && continue - seen_ips="$seen_ips + if [ -n "$ip" ]; then + # Skip if already in local IOCs + echo "$existing_ips" | grep -q "^${ip}$" && continue + # Skip if already seen in this batch + echo "$seen_ips" | grep -q "^${ip}$" && continue + seen_ips="$seen_ips $ip" + else + # Check for domain-based IOC (Vortex) + local domain=$(echo "$ioc_line" | jsonfilter -e '@.domain' 2>/dev/null) + [ -z "$domain" ] && continue + # Skip if already in local IOCs + echo "$existing_domains" | grep -q "^${domain}$" && continue + # Skip if already seen in this batch + echo "$seen_domains" | grep -q "^${domain}$" && continue + seen_domains="$seen_domains +$domain" + fi [ $first -eq 0 ] && new_iocs="$new_iocs," first=0 @@ -502,6 +557,7 @@ ti_trust_score() { ti_apply_ioc() { local ioc_json="$1" local ip=$(echo "$ioc_json" | jsonfilter -e '@.ip' 2>/dev/null) + local domain=$(echo "$ioc_json" | jsonfilter -e '@.domain' 2>/dev/null) local severity=$(echo "$ioc_json" | jsonfilter -e '@.severity' 2>/dev/null) local source_node=$(echo "$ioc_json" | jsonfilter -e '@.node' 2>/dev/null) local scenario=$(echo "$ioc_json" | jsonfilter -e '@.scenario' 2>/dev/null || echo "mesh-shared") @@ -509,11 +565,14 @@ ti_apply_ioc() { local ttl=$(echo "$ioc_json" | jsonfilter -e '@.ttl' 2>/dev/null || echo "$TI_IOC_TTL") local ts=$(echo "$ioc_json" | jsonfilter -e '@.ts' 2>/dev/null || echo "0") - [ -z "$ip" ] && return 1 + # Need either IP or domain + [ -z "$ip" ] && [ -z "$domain" ] && return 1 + + local target="${ip:-$domain}" # Check whitelist - grep -q "^${ip}$" "$TI_WHITELIST" 2>/dev/null && { - logger -t threat-intel "Skipping whitelisted IP: $ip" + grep -q "^${target}$" "$TI_WHITELIST" 2>/dev/null && { + logger -t threat-intel "Skipping whitelisted: $target" return 1 } @@ -540,7 +599,7 @@ ti_apply_ioc() { ;; unknown|*) # Never auto-apply unknown sources - logger -t threat-intel "Skipping IOC from unknown node: $source_node ($ip)" + logger -t threat-intel "Skipping IOC from unknown node: $source_node ($target)" return 1 ;; esac @@ -548,13 +607,25 @@ ti_apply_ioc() { # Check minimum severity _severity_meets_min "$severity" "$TI_MIN_SEVERITY" || return 1 - # Apply via CrowdSec - if command -v cscli >/dev/null 2>&1; then - cscli decisions add --ip "$ip" --duration "$duration" \ - --reason "mesh-p2p:$scenario" --type ban 2>/dev/null - if [ $? -eq 0 ]; then - logger -t threat-intel "Applied IOC: $ip (trust=$trust, severity=$severity, source=$source_node)" - return 0 + # Apply based on IOC type + if [ -n "$domain" ]; then + # Domain IOC - apply via Vortex Firewall + if [ -x /usr/sbin/vortex-firewall ]; then + /usr/sbin/vortex-firewall intel add "$domain" "mesh:$scenario" >/dev/null 2>&1 + if [ $? -eq 0 ]; then + logger -t threat-intel "Applied domain IOC: $domain (trust=$trust, severity=$severity, source=$source_node)" + return 0 + fi + fi + else + # IP IOC - apply via CrowdSec + if command -v cscli >/dev/null 2>&1; then + cscli decisions add --ip "$ip" --duration "$duration" \ + --reason "mesh-p2p:$scenario" --type ban 2>/dev/null + if [ $? -eq 0 ]; then + logger -t threat-intel "Applied IP IOC: $ip (trust=$trust, severity=$severity, source=$source_node)" + return 0 + fi fi fi diff --git a/package/secubox/secubox-vortex-firewall/Makefile b/package/secubox/secubox-vortex-firewall/Makefile index 939e451f..47dc68fa 100644 --- a/package/secubox/secubox-vortex-firewall/Makefile +++ b/package/secubox/secubox-vortex-firewall/Makefile @@ -12,7 +12,7 @@ define Package/secubox-vortex-firewall SECTION:=secubox CATEGORY:=SecuBox TITLE:=Vortex DNS Firewall - DEPENDS:=+dnsmasq-full +curl +sqlite3-cli +ca-certificates + DEPENDS:=+dnsmasq-full +curl +sqlite3-cli +ca-certificates +socat +openssl-util PKGARCH:=all endef @@ -21,6 +21,9 @@ define Package/secubox-vortex-firewall/description Blocks malware, phishing, and C2 at DNS resolution before any connection is established. Integrates threat feeds from abuse.ch, OpenPhish, and local DNS Guard detections. + + Phase 2: Sinkhole server captures blocked connections to + analyze malware behavior and identify infected clients. endef define Package/secubox-vortex-firewall/conffiles @@ -45,6 +48,13 @@ define Package/secubox-vortex-firewall/install $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-vortex-firewall.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/lib/vortex-firewall + $(INSTALL_BIN) ./root/usr/lib/vortex-firewall/sinkhole-http.sh $(1)/usr/lib/vortex-firewall/ + $(INSTALL_BIN) ./root/usr/lib/vortex-firewall/sinkhole-http-handler.sh $(1)/usr/lib/vortex-firewall/ + $(INSTALL_BIN) ./root/usr/lib/vortex-firewall/sinkhole-https.sh $(1)/usr/lib/vortex-firewall/ + + $(INSTALL_DIR) $(1)/etc/vortex-firewall endef define Package/secubox-vortex-firewall/postinst diff --git a/package/secubox/secubox-vortex-firewall/root/etc/init.d/vortex-firewall b/package/secubox/secubox-vortex-firewall/root/etc/init.d/vortex-firewall index 50eb1431..94dc4be5 100755 --- a/package/secubox/secubox-vortex-firewall/root/etc/init.d/vortex-firewall +++ b/package/secubox/secubox-vortex-firewall/root/etc/init.d/vortex-firewall @@ -1,5 +1,5 @@ #!/bin/sh /etc/rc.common -# Vortex DNS Firewall - DNS-level threat blocking +# Vortex DNS Firewall - DNS-level threat blocking with sinkhole server START=95 STOP=10 @@ -26,3 +26,15 @@ reload_service() { status() { $PROG status } + +sinkhole_start() { + $PROG sinkhole start +} + +sinkhole_stop() { + $PROG sinkhole stop +} + +sinkhole_status() { + $PROG sinkhole status +} diff --git a/package/secubox/secubox-vortex-firewall/root/usr/lib/vortex-firewall/sinkhole-http-handler.sh b/package/secubox/secubox-vortex-firewall/root/usr/lib/vortex-firewall/sinkhole-http-handler.sh new file mode 100644 index 00000000..6c1f1b41 --- /dev/null +++ b/package/secubox/secubox-vortex-firewall/root/usr/lib/vortex-firewall/sinkhole-http-handler.sh @@ -0,0 +1,145 @@ +#!/bin/sh +# +# Vortex Sinkhole HTTP Handler +# Called by socat for each connection +# + +SINKHOLE_HTML="/usr/share/vortex-firewall/sinkhole.html" +BLOCKLIST_DB="/var/lib/vortex-firewall/blocklist.db" + +# Get client IP from SOCAT environment +CLIENT_IP="${SOCAT_PEERADDR:-unknown}" + +# Get threat type for domain +get_threat_type() { + local domain="$1" + if [ -f "$BLOCKLIST_DB" ]; then + sqlite3 "$BLOCKLIST_DB" "SELECT threat_type FROM domains WHERE domain='$domain' LIMIT 1;" 2>/dev/null || echo "malicious" + else + echo "malicious" + fi +} + +# Read HTTP request +request="" +host="" +method="" +path="" +user_agent="" + +# Read first line +read -r line +method=$(echo "$line" | awk '{print $1}') +path=$(echo "$line" | awk '{print $2}') + +# Read headers until empty line +while read -r line; do + line=$(echo "$line" | tr -d '\r') + [ -z "$line" ] && break + + case "$line" in + Host:*|host:*) + host=$(echo "$line" | cut -d':' -f2- | tr -d ' ' | cut -d':' -f1) + ;; + User-Agent:*|user-agent:*) + user_agent=$(echo "$line" | cut -d':' -f2-) + ;; + esac +done + +# Default host if not found +[ -z "$host" ] && host="unknown" + +# Get threat type +threat_type=$(get_threat_type "$host") +timestamp=$(date "+%Y-%m-%d %H:%M:%S") + +# Record event (background to not delay response) +/usr/sbin/vortex-firewall sinkhole record "$CLIENT_IP" "$host" "http" "$method $path" >/dev/null 2>&1 & + +# Log +logger -t vortex-sinkhole "HTTP: $CLIENT_IP -> $host ($method $path) [$threat_type]" + +# Generate warning page +body=$(cat < + + + Connection Blocked - Vortex DNS Firewall + + + + + +
+
🛡
+

Connection Blocked

+

Vortex DNS Firewall has intercepted a potentially dangerous connection

+
$host
+ $threat_type +

+ This domain has been identified as malicious by our threat intelligence feeds. + The connection was blocked to protect your device from potential harm. +

+
+
Block Details
+
+ Domain + $host +
+
+ Category + $threat_type +
+
+ Your IP + $CLIENT_IP +
+
+ Timestamp + $timestamp +
+
+ +
+ + +EOF +) + +body_len=${#body} + +# Send HTTP response +printf "HTTP/1.1 403 Forbidden\r\n" +printf "Content-Type: text/html; charset=utf-8\r\n" +printf "Content-Length: %d\r\n" "$body_len" +printf "Connection: close\r\n" +printf "X-Vortex-Blocked: %s\r\n" "$host" +printf "X-Threat-Type: %s\r\n" "$threat_type" +printf "Cache-Control: no-store, no-cache, must-revalidate\r\n" +printf "\r\n" +printf "%s" "$body" diff --git a/package/secubox/secubox-vortex-firewall/root/usr/lib/vortex-firewall/sinkhole-http.sh b/package/secubox/secubox-vortex-firewall/root/usr/lib/vortex-firewall/sinkhole-http.sh new file mode 100644 index 00000000..e89841ab --- /dev/null +++ b/package/secubox/secubox-vortex-firewall/root/usr/lib/vortex-firewall/sinkhole-http.sh @@ -0,0 +1,151 @@ +#!/bin/sh +# +# Vortex Sinkhole HTTP Server +# Captures malware/phishing connections for analysis +# +# Usage: sinkhole-http.sh +# + +BIND_IP="${1:-192.168.255.253}" +PORT="${2:-80}" +SINKHOLE_HTML="/usr/share/vortex-firewall/sinkhole.html" +BLOCKLIST_DB="/var/lib/vortex-firewall/blocklist.db" + +# Log function +log() { + logger -t vortex-sinkhole-http "$1" +} + +# Get warning page HTML +get_warning_page() { + local domain="$1" + local client_ip="$2" + local threat_type="$3" + local timestamp=$(date "+%Y-%m-%d %H:%M:%S") + + if [ -f "$SINKHOLE_HTML" ]; then + # Substitute placeholders + sed -e "s|{{DOMAIN}}|$domain|g" \ + -e "s|{{CLIENT_IP}}|$client_ip|g" \ + -e "s|{{THREAT_TYPE}}|$threat_type|g" \ + -e "s|{{TIMESTAMP}}|$timestamp|g" \ + "$SINKHOLE_HTML" + else + # Inline fallback + cat < + + + Connection Blocked - Vortex DNS Firewall + + + + + +
+
🛡
+

Connection Blocked

+
$domain
+
$threat_type
+

+ This connection was blocked by Vortex DNS Firewall because the domain + has been identified as malicious or potentially harmful. +

+
+

Block Details:

+

Domain: $domain

+

Category: $threat_type

+

Client IP: $client_ip

+

Timestamp: $timestamp

+
+ +
+ + +EOF + fi +} + +# Get threat type for domain +get_threat_type() { + local domain="$1" + if [ -f "$BLOCKLIST_DB" ]; then + sqlite3 "$BLOCKLIST_DB" "SELECT threat_type FROM domains WHERE domain='$domain' LIMIT 1;" 2>/dev/null || echo "malicious" + else + echo "malicious" + fi +} + +# Handle HTTP request +handle_request() { + local client_ip="$1" + + # Read HTTP request + local request="" + local host="" + local method="" + local path="" + local headers="" + + # Read first line (GET /path HTTP/1.1) + read -r line + method=$(echo "$line" | awk '{print $1}') + path=$(echo "$line" | awk '{print $2}') + request="$line" + + # Read headers until empty line + while read -r line; do + line=$(echo "$line" | tr -d '\r') + [ -z "$line" ] && break + headers="$headers$line\n" + + # Extract Host header + case "$line" in + Host:*|host:*) + host=$(echo "$line" | cut -d':' -f2 | tr -d ' ') + ;; + esac + done + + # Default host if not found + [ -z "$host" ] && host="unknown" + + # Get threat type + local threat_type=$(get_threat_type "$host") + + # Record event + /usr/sbin/vortex-firewall sinkhole record "$client_ip" "$host" "http" "$method $path" >/dev/null 2>&1 & + + # Log + log "Captured: $client_ip -> $host ($method $path) [$threat_type]" + + # Generate response + local body=$(get_warning_page "$host" "$client_ip" "$threat_type") + local body_len=${#body} + + # Send HTTP response + printf "HTTP/1.1 403 Forbidden\r\n" + printf "Content-Type: text/html; charset=utf-8\r\n" + printf "Content-Length: %d\r\n" "$body_len" + printf "Connection: close\r\n" + printf "X-Vortex-Blocked: %s\r\n" "$host" + printf "Cache-Control: no-store, no-cache\r\n" + printf "\r\n" + printf "%s" "$body" +} + +# Main loop using socat +log "Starting HTTP sinkhole on $BIND_IP:$PORT" + +exec socat -T 30 TCP-LISTEN:$PORT,bind=$BIND_IP,reuseaddr,fork EXEC:"/usr/lib/vortex-firewall/sinkhole-http-handler.sh",sigint,sigterm diff --git a/package/secubox/secubox-vortex-firewall/root/usr/lib/vortex-firewall/sinkhole-https.sh b/package/secubox/secubox-vortex-firewall/root/usr/lib/vortex-firewall/sinkhole-https.sh new file mode 100644 index 00000000..96d7f0c9 --- /dev/null +++ b/package/secubox/secubox-vortex-firewall/root/usr/lib/vortex-firewall/sinkhole-https.sh @@ -0,0 +1,75 @@ +#!/bin/sh +# +# Vortex Sinkhole HTTPS Server +# Captures TLS connections to blocked domains +# +# Note: Requires socat with SSL support OR stunnel package +# Without SSL support, HTTPS connections to blocked domains will +# show certificate warnings instead of the sinkhole page. +# +# Usage: sinkhole-https.sh +# + +BIND_IP="${1:-192.168.255.253}" +PORT="${2:-443}" +CERT_DIR="/etc/vortex-firewall" +CERT_FILE="$CERT_DIR/sinkhole.crt" +KEY_FILE="$CERT_DIR/sinkhole.key" +PID_FILE="/var/run/vortex-sinkhole-https.pid" + +log() { + logger -t vortex-sinkhole-https "$1" +} + +# Check certificates exist +if [ ! -f "$CERT_FILE" ] || [ ! -f "$KEY_FILE" ]; then + log "ERROR: Certificates not found. Run: vortex-firewall sinkhole gencert" + exit 1 +fi + +log "Starting HTTPS sinkhole on $BIND_IP:$PORT" +echo $$ > "$PID_FILE" + +# Cleanup on exit +cleanup() { + log "Stopping HTTPS sinkhole" + rm -f "$PID_FILE" + exit 0 +} +trap cleanup INT TERM + +# Check if socat has SSL support +if socat -h 2>&1 | grep -q "openssl"; then + log "Using socat with SSL support" + exec socat -T 30 \ + OPENSSL-LISTEN:$PORT,bind=$BIND_IP,reuseaddr,fork,cert=$CERT_FILE,key=$KEY_FILE,verify=0 \ + EXEC:"/usr/lib/vortex-firewall/sinkhole-http-handler.sh",sigint,sigterm +fi + +# Check if stunnel is available +if command -v stunnel >/dev/null 2>&1; then + log "Using stunnel for HTTPS" + # Create stunnel config + STUNNEL_CONF="/tmp/vortex-stunnel.conf" + cat > "$STUNNEL_CONF" </dev/null 2>&1 && http_running=1 + + # Check HTTPS by PID file + local https_pid_file="/var/run/vortex-sinkhole-https.pid" + if [ -f "$https_pid_file" ] && kill -0 "$(cat "$https_pid_file")" 2>/dev/null; then + if pgrep -f "OPENSSL-LISTEN\|stunnel\|vortex-stunnel" >/dev/null 2>&1; then + https_running=1 + else + https_limited=1 + fi + fi + + json_add_boolean "http_running" "$http_running" + json_add_boolean "https_running" "$https_running" + json_add_boolean "https_limited" "$https_limited" + + # Event statistics + if [ -f "$BLOCKLIST_DB" ]; then + local today=$(date +%Y-%m-%d) + local total_events=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM events;" 2>/dev/null || echo 0) + local today_events=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM events WHERE timestamp LIKE '$today%';" 2>/dev/null || echo 0) + local unique_clients=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(DISTINCT client_ip) FROM events;" 2>/dev/null || echo 0) + + json_add_int "total_events" "$total_events" + json_add_int "today_events" "$today_events" + json_add_int "unique_clients" "$unique_clients" + else + json_add_int "total_events" 0 + json_add_int "today_events" 0 + json_add_int "unique_clients" 0 + fi + + json_dump +} + +do_sinkhole_events() { + local input limit + read input + limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null) + [ -z "$limit" ] && limit=100 + + json_init + json_add_array "events" + + if [ -f "$BLOCKLIST_DB" ]; then + sqlite3 "$BLOCKLIST_DB" "SELECT id, timestamp, client_ip, domain, event_type, details FROM events ORDER BY id DESC LIMIT $limit;" 2>/dev/null > /tmp/vf_events.tmp + while IFS='|' read -r id ts ip domain type details; do + [ -n "$id" ] || continue + json_add_object "" + json_add_int "id" "$id" + json_add_string "timestamp" "$ts" + json_add_string "client_ip" "$ip" + json_add_string "domain" "$domain" + json_add_string "event_type" "$type" + json_add_string "details" "$details" + json_close_object + done < /tmp/vf_events.tmp + rm -f /tmp/vf_events.tmp + fi + + json_close_array + json_dump +} + +do_sinkhole_stats() { + json_init + + if [ ! -f "$BLOCKLIST_DB" ]; then + json_add_int "total_events" 0 + json_add_int "unique_clients" 0 + json_add_int "unique_domains" 0 + json_dump + return + fi + + local total_events=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM events;" 2>/dev/null || echo 0) + local unique_clients=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(DISTINCT client_ip) FROM events;" 2>/dev/null || echo 0) + local unique_domains=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(DISTINCT domain) FROM events;" 2>/dev/null || echo 0) + + json_add_int "total_events" "$total_events" + json_add_int "unique_clients" "$unique_clients" + json_add_int "unique_domains" "$unique_domains" + + # Top clients (infected hosts) + json_add_array "top_clients" + sqlite3 "$BLOCKLIST_DB" "SELECT client_ip, COUNT(*) as cnt FROM events GROUP BY client_ip ORDER BY cnt DESC LIMIT 10;" 2>/dev/null > /tmp/vf_clients.tmp + while IFS='|' read -r ip cnt; do + [ -n "$ip" ] || continue + json_add_object "" + json_add_string "ip" "$ip" + json_add_int "events" "$cnt" + json_close_object + done < /tmp/vf_clients.tmp + rm -f /tmp/vf_clients.tmp + json_close_array + + # Top blocked domains + json_add_array "top_domains" + sqlite3 "$BLOCKLIST_DB" "SELECT domain, COUNT(*) as cnt FROM events GROUP BY domain ORDER BY cnt DESC LIMIT 10;" 2>/dev/null > /tmp/vf_domains.tmp + while IFS='|' read -r domain cnt; do + [ -n "$domain" ] || continue + json_add_object "" + json_add_string "domain" "$domain" + json_add_int "events" "$cnt" + json_close_object + done < /tmp/vf_domains.tmp + rm -f /tmp/vf_domains.tmp + json_close_array + + # Events by type + json_add_object "by_type" + sqlite3 "$BLOCKLIST_DB" "SELECT event_type, COUNT(*) FROM events GROUP BY event_type;" 2>/dev/null | while IFS='|' read -r type cnt; do + [ -n "$type" ] && json_add_int "$type" "$cnt" + done + json_close_object + + json_dump +} + +do_sinkhole_toggle() { + local input enabled + read input + enabled=$(echo "$input" | jsonfilter -e '@.enabled' 2>/dev/null) + + json_init + + if [ -z "$enabled" ]; then + json_add_boolean "success" 0 + json_add_string "message" "Missing enabled parameter" + json_dump + return + fi + + uci set vortex-firewall.server.enabled="$enabled" + uci commit vortex-firewall + + if [ "$enabled" = "1" ]; then + /usr/sbin/vortex-firewall sinkhole start >/dev/null 2>&1 & + json_add_string "message" "Sinkhole server enabled and starting" + else + /usr/sbin/vortex-firewall sinkhole stop >/dev/null 2>&1 & + json_add_string "message" "Sinkhole server disabled and stopping" + fi + + json_add_boolean "success" 1 + json_dump +} + +do_sinkhole_clear() { + json_init + + if [ -f "$BLOCKLIST_DB" ]; then + sqlite3 "$BLOCKLIST_DB" "DELETE FROM events;" 2>/dev/null + json_add_boolean "success" 1 + json_add_string "message" "Event log cleared" + else + json_add_boolean "success" 0 + json_add_string "message" "Database not found" + fi + + json_dump +} + +# ============================================================================ +# DNS Guard Integration Methods (Phase 3) +# ============================================================================ + +DNSGUARD_DIR="/var/lib/dns-guard" + +do_dnsguard_status() { + json_init + + # Service running check + local running=0 + pgrep -f "dns-guard" >/dev/null 2>&1 && running=1 + json_add_boolean "running" "$running" + + # Service enabled check + local enabled=0 + [ -x /etc/init.d/dns-guard ] && /etc/init.d/dns-guard enabled 2>/dev/null && enabled=1 + json_add_boolean "enabled" "$enabled" + + # Service installed check + local installed=0 + [ -x /usr/bin/dns-guard ] && installed=1 + json_add_boolean "installed" "$installed" + + # Alert counts + local alert_count=0 + local pending_count=0 + + if [ -f "$DNSGUARD_DIR/alerts.json" ]; then + alert_count=$(jsonfilter -i "$DNSGUARD_DIR/alerts.json" -e '@[*]' 2>/dev/null | wc -l) + fi + json_add_int "alert_count" "$alert_count" + + if [ -f "$DNSGUARD_DIR/pending_blocks.json" ]; then + pending_count=$(jsonfilter -i "$DNSGUARD_DIR/pending_blocks.json" -e '@[*]' 2>/dev/null | wc -l) + fi + json_add_int "pending_count" "$pending_count" + + # Domain count + local domain_count=0 + if [ -f "$DNSGUARD_DIR/threat_domains.txt" ]; then + domain_count=$(wc -l < "$DNSGUARD_DIR/threat_domains.txt" 2>/dev/null || echo 0) + fi + json_add_int "domain_count" "$domain_count" + + # Vortex integration stats + local vortex_imported=0 + local vortex_last_sync="" + if [ -f "$BLOCKLIST_DB" ]; then + vortex_imported=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE source='dnsguard';" 2>/dev/null || echo 0) + vortex_last_sync=$(sqlite3 "$BLOCKLIST_DB" "SELECT last_update FROM feeds WHERE name='dnsguard';" 2>/dev/null || echo "") + fi + json_add_int "vortex_imported" "$vortex_imported" + json_add_string "vortex_last_sync" "$vortex_last_sync" + + # Detection type breakdown + json_add_object "detection_types" + if [ -f "$BLOCKLIST_DB" ]; then + sqlite3 "$BLOCKLIST_DB" "SELECT threat_type, COUNT(*) FROM domains WHERE source='dnsguard' GROUP BY threat_type;" 2>/dev/null | while IFS='|' read -r type cnt; do + [ -n "$type" ] && json_add_int "$type" "$cnt" + done + fi + json_close_object + + json_dump +} + +do_dnsguard_alerts() { + local input limit + read input + limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null) + [ -z "$limit" ] && limit=50 + + json_init + json_add_array "alerts" + + if [ -f "$DNSGUARD_DIR/alerts.json" ]; then + # Get recent alerts + local count=0 + jsonfilter -i "$DNSGUARD_DIR/alerts.json" -e '@[*]' 2>/dev/null | tail -n "$limit" | while read -r alert; do + local domain=$(echo "$alert" | jsonfilter -e '@.domain' 2>/dev/null) + [ -z "$domain" ] && continue + [ "$domain" = "*" ] && continue # Skip rate anomaly wildcards + + json_add_object "" + json_add_string "domain" "$domain" + + local client=$(echo "$alert" | jsonfilter -e '@.client' 2>/dev/null) + json_add_string "client" "${client:-unknown}" + + local type=$(echo "$alert" | jsonfilter -e '@.type' 2>/dev/null) + json_add_string "type" "${type:-unknown}" + + local confidence=$(echo "$alert" | jsonfilter -e '@.confidence' 2>/dev/null) + json_add_int "confidence" "${confidence:-0}" + + local reason=$(echo "$alert" | jsonfilter -e '@.reason' 2>/dev/null) + json_add_string "reason" "${reason:-}" + + json_close_object + done + fi + + json_close_array + json_dump +} + +do_dnsguard_sync() { + json_init + + if [ -x /usr/sbin/vortex-firewall ]; then + /usr/sbin/vortex-firewall dnsguard sync >/dev/null 2>&1 & + json_add_boolean "success" 1 + json_add_string "message" "DNS Guard sync started" + else + json_add_boolean "success" 0 + json_add_string "message" "vortex-firewall not installed" + fi + + json_dump +} + +# ============================================================================ +# Mesh Threat Sharing Methods (Phase 4) +# ============================================================================ + +THREAT_INTEL_SCRIPT="/usr/lib/secubox/threat-intel.sh" + +do_mesh_status() { + json_init + + # Check if P2P threat intel is available + if [ ! -x "$THREAT_INTEL_SCRIPT" ]; then + json_add_boolean "available" 0 + json_add_string "message" "secubox-p2p not installed" + json_dump + return + fi + + json_add_boolean "available" 1 + + # Get threat intel status + local status=$("$THREAT_INTEL_SCRIPT" status 2>/dev/null) + + if [ -z "$status" ]; then + json_add_boolean "enabled" 0 + json_dump + return + fi + + local enabled=$(echo "$status" | jsonfilter -e '@.enabled' 2>/dev/null) + json_add_boolean "enabled" "$( [ "$enabled" = "true" ] && echo 1 || echo 0)" + + local local_iocs=$(echo "$status" | jsonfilter -e '@.local_iocs' 2>/dev/null || echo 0) + local received=$(echo "$status" | jsonfilter -e '@.received_iocs' 2>/dev/null || echo 0) + local applied=$(echo "$status" | jsonfilter -e '@.applied_iocs' 2>/dev/null || echo 0) + local peers=$(echo "$status" | jsonfilter -e '@.peer_contributors' 2>/dev/null || echo 0) + local chain_blocks=$(echo "$status" | jsonfilter -e '@.chain_threat_blocks' 2>/dev/null || echo 0) + + json_add_int "local_iocs" "$local_iocs" + json_add_int "received_iocs" "$received" + json_add_int "applied_iocs" "$applied" + json_add_int "peer_contributors" "$peers" + json_add_int "chain_blocks" "$chain_blocks" + + # Count Vortex-sourced IOCs + local vortex_count=0 + local ti_local="/var/lib/secubox/threat-intel/iocs-local.json" + if [ -f "$ti_local" ]; then + vortex_count=$(jsonfilter -i "$ti_local" -e '@[*].source' 2>/dev/null | grep -c "^vortex$" || echo 0) + fi + json_add_int "vortex_shared" "$vortex_count" + + json_dump +} + +do_mesh_received() { + local input limit + read input + limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null) + [ -z "$limit" ] && limit=50 + + json_init + json_add_array "iocs" + + if [ -x "$THREAT_INTEL_SCRIPT" ]; then + local received=$("$THREAT_INTEL_SCRIPT" list received 2>/dev/null) + + echo "$received" | jsonfilter -e '@[*]' 2>/dev/null | tail -n "$limit" | while read -r ioc; do + [ -z "$ioc" ] && continue + + json_add_object "" + + local domain=$(echo "$ioc" | jsonfilter -e '@.domain' 2>/dev/null) + local ip=$(echo "$ioc" | jsonfilter -e '@.ip' 2>/dev/null) + local severity=$(echo "$ioc" | jsonfilter -e '@.severity' 2>/dev/null) + local trust=$(echo "$ioc" | jsonfilter -e '@.trust' 2>/dev/null) + local applied=$(echo "$ioc" | jsonfilter -e '@.applied' 2>/dev/null) + local scenario=$(echo "$ioc" | jsonfilter -e '@.scenario' 2>/dev/null) + local node=$(echo "$ioc" | jsonfilter -e '@.node' 2>/dev/null) + + [ -n "$domain" ] && json_add_string "domain" "$domain" + [ -n "$ip" ] && json_add_string "ip" "$ip" + json_add_string "severity" "${severity:-unknown}" + json_add_string "trust" "${trust:-unknown}" + json_add_boolean "applied" "$( [ "$applied" = "true" ] && echo 1 || echo 0)" + json_add_string "scenario" "${scenario:-unknown}" + json_add_string "node" "${node:-unknown}" + + json_close_object + done + fi + + json_close_array + json_dump +} + +do_mesh_publish() { + json_init + + if [ -x "$THREAT_INTEL_SCRIPT" ]; then + "$THREAT_INTEL_SCRIPT" collect >/dev/null 2>&1 + local result=$("$THREAT_INTEL_SCRIPT" publish 2>/dev/null) + local published=$(echo "$result" | jsonfilter -e '@.published' 2>/dev/null || echo 0) + + json_add_boolean "success" 1 + json_add_int "published" "$published" + json_add_string "message" "Published $published IOCs to mesh" + else + json_add_boolean "success" 0 + json_add_string "message" "secubox-p2p not installed" + fi + + json_dump +} + +do_mesh_sync() { + json_init + + if [ -x "$THREAT_INTEL_SCRIPT" ]; then + local result=$("$THREAT_INTEL_SCRIPT" apply-pending 2>/dev/null) + local applied=$(echo "$result" | jsonfilter -e '@.applied' 2>/dev/null || echo 0) + local skipped=$(echo "$result" | jsonfilter -e '@.skipped' 2>/dev/null || echo 0) + + # Regenerate blocklist if any applied + if [ "$applied" -gt 0 ] && [ -x /usr/sbin/vortex-firewall ]; then + /usr/sbin/vortex-firewall intel update >/dev/null 2>&1 & + fi + + json_add_boolean "success" 1 + json_add_int "applied" "$applied" + json_add_int "skipped" "$skipped" + json_add_string "message" "Applied $applied, skipped $skipped" + else + json_add_boolean "success" 0 + json_add_string "message" "secubox-p2p not installed" + fi + + json_dump +} + +do_mesh_peers() { + json_init + json_add_array "peers" + + if [ -x "$THREAT_INTEL_SCRIPT" ]; then + local peers=$("$THREAT_INTEL_SCRIPT" peers 2>/dev/null) + + echo "$peers" | jsonfilter -e '@[*]' 2>/dev/null | while read -r peer; do + [ -z "$peer" ] && continue + + json_add_object "" + + local node=$(echo "$peer" | jsonfilter -e '@.node' 2>/dev/null) + local trust=$(echo "$peer" | jsonfilter -e '@.trust' 2>/dev/null) + local ioc_count=$(echo "$peer" | jsonfilter -e '@.ioc_count' 2>/dev/null) + local applied_count=$(echo "$peer" | jsonfilter -e '@.applied_count' 2>/dev/null) + + json_add_string "node" "${node:-unknown}" + json_add_string "trust" "${trust:-unknown}" + json_add_int "ioc_count" "${ioc_count:-0}" + json_add_int "applied_count" "${applied_count:-0}" + + json_close_object + done + fi + + json_close_array + json_dump +} + case "$1" in list) echo '{' @@ -247,7 +721,20 @@ case "$1" in echo '"search":{"domain":"String"},' echo '"update_feeds":{},' echo '"block_domain":{"domain":"String","reason":"String"},' - echo '"unblock_domain":{"domain":"String"}' + echo '"unblock_domain":{"domain":"String"},' + echo '"sinkhole_status":{},' + echo '"sinkhole_events":{"limit":"Integer"},' + echo '"sinkhole_stats":{},' + echo '"sinkhole_toggle":{"enabled":"Integer"},' + echo '"sinkhole_clear":{},' + echo '"dnsguard_status":{},' + echo '"dnsguard_alerts":{"limit":"Integer"},' + echo '"dnsguard_sync":{},' + echo '"mesh_status":{},' + echo '"mesh_received":{"limit":"Integer"},' + echo '"mesh_publish":{},' + echo '"mesh_sync":{},' + echo '"mesh_peers":{}' echo '}' ;; call) @@ -260,6 +747,19 @@ case "$1" in update_feeds) do_update_feeds ;; block_domain) do_block_domain ;; unblock_domain) do_unblock_domain ;; + sinkhole_status) do_sinkhole_status ;; + sinkhole_events) do_sinkhole_events ;; + sinkhole_stats) do_sinkhole_stats ;; + sinkhole_toggle) do_sinkhole_toggle ;; + sinkhole_clear) do_sinkhole_clear ;; + dnsguard_status) do_dnsguard_status ;; + dnsguard_alerts) do_dnsguard_alerts ;; + dnsguard_sync) do_dnsguard_sync ;; + mesh_status) do_mesh_status ;; + mesh_received) do_mesh_received ;; + mesh_publish) do_mesh_publish ;; + mesh_sync) do_mesh_sync ;; + mesh_peers) do_mesh_peers ;; esac ;; esac diff --git a/package/secubox/secubox-vortex-firewall/root/usr/sbin/vortex-firewall b/package/secubox/secubox-vortex-firewall/root/usr/sbin/vortex-firewall index 9a2c4395..4f906654 100755 --- a/package/secubox/secubox-vortex-firewall/root/usr/sbin/vortex-firewall +++ b/package/secubox/secubox-vortex-firewall/root/usr/sbin/vortex-firewall @@ -160,20 +160,81 @@ feed_update_threatfox() { } feed_import_dnsguard() { - local dnsguard_list="/var/lib/dns-guard/threat_domains.txt" + local dnsguard_dir="/var/lib/dns-guard" + local dnsguard_list="$dnsguard_dir/threat_domains.txt" + local dnsguard_alerts="$dnsguard_dir/alerts.json" local feed_file="$FEEDS_DIR/dnsguard.txt" - if [ -f "$dnsguard_list" ]; then - log "Importing DNS Guard detections..." + log "Importing DNS Guard detections..." + + # Phase 3: Enhanced import with metadata from alerts.json + if [ -f "$dnsguard_alerts" ] && [ -s "$dnsguard_alerts" ]; then + log "Reading DNS Guard alerts with metadata..." + + # Parse alerts.json and import with proper threat types and confidence + local imported=0 + local now=$(date -Iseconds) + + # Build SQL import from alerts + local sql_file="/tmp/vortex-dnsguard-import.sql" + echo "BEGIN TRANSACTION;" > "$sql_file" + + # Read each alert and extract domain, type, confidence + jsonfilter -i "$dnsguard_alerts" -e '@[*]' 2>/dev/null | while read -r alert; do + local domain=$(echo "$alert" | jsonfilter -e '@.domain' 2>/dev/null | tr -d '\n\r') + local threat_type=$(echo "$alert" | jsonfilter -e '@.type' 2>/dev/null | tr -d '\n\r') + local confidence=$(echo "$alert" | jsonfilter -e '@.confidence' 2>/dev/null | tr -d '\n\r') + + # Skip rate anomalies with wildcard domains + [ "$domain" = "*" ] && continue + [ -z "$domain" ] && continue + + # Default values + [ -z "$threat_type" ] && threat_type="ai_detected" + [ -z "$confidence" ] && confidence=80 + + # Map DNS Guard types to Vortex threat types + case "$threat_type" in + dga) threat_type="dga" ;; + tunneling) threat_type="dns_tunnel" ;; + known_bad) threat_type="malware" ;; + tld_anomaly) threat_type="suspicious_tld" ;; + rate_anomaly) threat_type="rate_anomaly" ;; + esac + + # Escape for SQL + domain=$(echo "$domain" | sed "s/'/''/g") + + echo "INSERT OR REPLACE INTO domains (domain, threat_type, confidence, source, first_seen, last_seen, blocked) VALUES ('$domain', '$threat_type', $confidence, 'dnsguard', '$now', '$now', 1);" >> "$sql_file" + imported=$((imported + 1)) + done + + echo "COMMIT;" >> "$sql_file" + + # Execute import + sqlite3 "$BLOCKLIST_DB" < "$sql_file" 2>/dev/null + rm -f "$sql_file" + + # Also copy plaintext list for dnsmasq + [ -f "$dnsguard_list" ] && cp "$dnsguard_list" "$feed_file" + + local count=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE source='dnsguard';" 2>/dev/null || echo 0) + sqlite3 "$BLOCKLIST_DB" "INSERT OR REPLACE INTO feeds VALUES ('dnsguard', 'local', datetime('now'), $count, 1);" + log "DNS Guard: $count domains (with AI metadata)" + return 0 + fi + + # Fallback: basic import from threat_domains.txt + if [ -f "$dnsguard_list" ] && [ -s "$dnsguard_list" ]; then cp "$dnsguard_list" "$feed_file" local count=$(wc -l < "$feed_file") sqlite3 "$BLOCKLIST_DB" "INSERT OR REPLACE INTO feeds VALUES ('dnsguard', 'local', datetime('now'), $count, 1);" - log "DNS Guard: $count domains" - return 0 - else - info "No DNS Guard detections found" + log "DNS Guard: $count domains (basic)" return 0 fi + + info "No DNS Guard detections found" + return 0 } intel_update() { @@ -556,6 +617,552 @@ show_x47() { echo "" } +# ============================================================================ +# Sinkhole Server - HTTP/HTTPS Trap for Blocked Domains +# ============================================================================ + +SINKHOLE_PID_HTTP="/var/run/vortex-sinkhole-http.pid" +SINKHOLE_PID_HTTPS="/var/run/vortex-sinkhole-https.pid" +SINKHOLE_LOG="/var/log/vortex-sinkhole.log" +SINKHOLE_HTML="/usr/share/vortex-firewall/sinkhole.html" + +sinkhole_start() { + log "Starting Vortex Sinkhole Server..." + + init_dirs + init_db + + # Check if sinkhole is enabled in config + local enabled=$(uci -q get vortex-firewall.server.enabled) + if [ "$enabled" != "1" ]; then + warn "Sinkhole server not enabled in config" + info "Enable with: uci set vortex-firewall.server.enabled=1 && uci commit" + return 1 + fi + + local http_port=$(uci -q get vortex-firewall.server.http_port || echo 80) + local https_port=$(uci -q get vortex-firewall.server.https_port || echo 443) + + # Create sinkhole IP alias if not exists + if ! ip addr show dev br-lan 2>/dev/null | grep -q "$SINKHOLE_IP"; then + log "Adding sinkhole IP $SINKHOLE_IP to br-lan..." + ip addr add "$SINKHOLE_IP/32" dev br-lan 2>/dev/null || true + fi + + # Start HTTP sinkhole + if ! pgrep -f "sinkhole-http-handler" >/dev/null 2>&1; then + log "Starting HTTP sinkhole on $SINKHOLE_IP:$http_port..." + /usr/lib/vortex-firewall/sinkhole-http.sh "$SINKHOLE_IP" "$http_port" & + echo $! > "$SINKHOLE_PID_HTTP" + fi + + # Start HTTPS sinkhole (if certificates available) + if [ -f "/etc/vortex-firewall/sinkhole.key" ] && [ -f "/etc/vortex-firewall/sinkhole.crt" ]; then + if ! pgrep -f "OPENSSL-LISTEN" >/dev/null 2>&1; then + log "Starting HTTPS sinkhole on $SINKHOLE_IP:$https_port..." + /usr/lib/vortex-firewall/sinkhole-https.sh "$SINKHOLE_IP" "$https_port" & + echo $! > "$SINKHOLE_PID_HTTPS" + fi + else + info "HTTPS sinkhole skipped (no certificates)" + info "Generate with: vortex-firewall sinkhole gencert" + fi + + log "Sinkhole server started" +} + +sinkhole_stop() { + log "Stopping Vortex Sinkhole Server..." + + # Stop HTTP sinkhole + if [ -f "$SINKHOLE_PID_HTTP" ]; then + kill $(cat "$SINKHOLE_PID_HTTP") 2>/dev/null + rm -f "$SINKHOLE_PID_HTTP" + fi + pkill -f "sinkhole-http-handler" 2>/dev/null || true + + # Stop HTTPS sinkhole + if [ -f "$SINKHOLE_PID_HTTPS" ]; then + kill $(cat "$SINKHOLE_PID_HTTPS") 2>/dev/null + rm -f "$SINKHOLE_PID_HTTPS" + fi + pkill -f "OPENSSL-LISTEN" 2>/dev/null || true + + log "Sinkhole server stopped" +} + +sinkhole_status() { + echo "" + echo -e "${BOLD}Vortex Sinkhole Server${NC}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + local enabled=$(uci -q get vortex-firewall.server.enabled || echo 0) + local http_port=$(uci -q get vortex-firewall.server.http_port || echo 80) + local https_port=$(uci -q get vortex-firewall.server.https_port || echo 443) + + if [ "$enabled" = "1" ]; then + echo -e "Config: ${GREEN}Enabled${NC}" + else + echo -e "Config: ${YELLOW}Disabled${NC}" + fi + + echo "Sinkhole IP: $SINKHOLE_IP" + echo "HTTP Port: $http_port" + echo "HTTPS Port: $https_port" + echo "" + + # Check running processes + if pgrep -f "sinkhole-http-handler" >/dev/null 2>&1; then + echo -e "HTTP Server: ${GREEN}Running${NC}" + else + echo -e "HTTP Server: ${RED}Stopped${NC}" + fi + + # Check HTTPS by PID file (supports multiple backends) + if [ -f "$SINKHOLE_PID_HTTPS" ] && kill -0 "$(cat "$SINKHOLE_PID_HTTPS")" 2>/dev/null; then + if pgrep -f "OPENSSL-LISTEN\|stunnel\|vortex-stunnel" >/dev/null 2>&1; then + echo -e "HTTPS Server: ${GREEN}Running${NC}" + else + echo -e "HTTPS Server: ${YELLOW}Limited (no SSL)${NC}" + fi + else + echo -e "HTTPS Server: ${RED}Stopped${NC}" + fi + + # Event stats + if [ -f "$BLOCKLIST_DB" ]; then + local today=$(date +%Y-%m-%d) + local total_events=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM events;" 2>/dev/null || echo 0) + local today_events=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM events WHERE timestamp LIKE '$today%';" 2>/dev/null || echo 0) + local unique_clients=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(DISTINCT client_ip) FROM events;" 2>/dev/null || echo 0) + + echo "" + echo -e "${BOLD}Capture Statistics:${NC}" + echo " Total Events: $total_events" + echo " Today's Events: $today_events" + echo " Unique Clients: $unique_clients" + fi + echo "" +} + +sinkhole_logs() { + local lines="${1:-50}" + + echo "" + echo -e "${BOLD}Sinkhole Event Log (last $lines)${NC}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + if [ -f "$BLOCKLIST_DB" ]; then + sqlite3 -column -header "$BLOCKLIST_DB" \ + "SELECT timestamp, client_ip, domain, event_type FROM events ORDER BY id DESC LIMIT $lines;" 2>/dev/null + else + warn "No database found" + fi + echo "" +} + +sinkhole_export() { + local output="${1:-/tmp/vortex-sinkhole-events.json}" + + log "Exporting sinkhole events to $output..." + + if [ ! -f "$BLOCKLIST_DB" ]; then + error "No database found" + return 1 + fi + + echo "[" > "$output" + local first=1 + sqlite3 "$BLOCKLIST_DB" "SELECT id, timestamp, client_ip, domain, event_type, details FROM events ORDER BY id;" 2>/dev/null | \ + while IFS='|' read -r id ts ip domain type details; do + [ -z "$id" ] && continue + [ "$first" = "1" ] && first=0 || echo "," >> "$output" + printf '{"id":%d,"timestamp":"%s","client_ip":"%s","domain":"%s","event_type":"%s","details":"%s"}' \ + "$id" "$ts" "$ip" "$domain" "$type" "$details" >> "$output" + done + echo "]" >> "$output" + + log "Exported to: $output" + log "Events: $(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM events;" 2>/dev/null || echo 0)" +} + +sinkhole_gencert() { + local cert_dir="/etc/vortex-firewall" + mkdir -p "$cert_dir" + + log "Generating self-signed certificate for HTTPS sinkhole..." + + # Generate private key + openssl genrsa -out "$cert_dir/sinkhole.key" 2048 2>/dev/null + + # Generate self-signed certificate + openssl req -new -x509 -key "$cert_dir/sinkhole.key" \ + -out "$cert_dir/sinkhole.crt" \ + -days 3650 \ + -subj "/CN=Vortex Sinkhole/O=SecuBox/C=FR" 2>/dev/null + + chmod 600 "$cert_dir/sinkhole.key" + chmod 644 "$cert_dir/sinkhole.crt" + + log "Certificate generated:" + log " Key: $cert_dir/sinkhole.key" + log " Cert: $cert_dir/sinkhole.crt" +} + +sinkhole_clear() { + log "Clearing sinkhole event log..." + + if [ -f "$BLOCKLIST_DB" ]; then + sqlite3 "$BLOCKLIST_DB" "DELETE FROM events;" 2>/dev/null + log "Events cleared" + else + warn "No database found" + fi +} + +# Record a sinkhole hit (called by sinkhole HTTP servers) +sinkhole_record_event() { + local client_ip="$1" + local domain="$2" + local event_type="${3:-http}" + local details="${4:-}" + + [ -z "$client_ip" ] || [ -z "$domain" ] && return 1 + + init_db + + local timestamp=$(date -Iseconds) + + # Record event + sqlite3 "$BLOCKLIST_DB" \ + "INSERT INTO events (timestamp, client_ip, domain, event_type, details) + VALUES ('$timestamp', '$client_ip', '$domain', '$event_type', '$details');" 2>/dev/null + + # Update hit count on domain + sqlite3 "$BLOCKLIST_DB" \ + "UPDATE domains SET hit_count = hit_count + 1, last_seen = '$timestamp' + WHERE domain = '$domain';" 2>/dev/null + + # Log to syslog + logger -t vortex-sinkhole "Blocked: $client_ip -> $domain ($event_type)" + + echo "$timestamp" +} + +# ============================================================================ +# Mesh Threat Sharing (Phase 4) +# ============================================================================ + +THREAT_INTEL_SCRIPT="/usr/lib/secubox/threat-intel.sh" + +mesh_status() { + echo "" + echo -e "${BOLD}Vortex Mesh Threat Sharing${NC}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + if [ ! -x "$THREAT_INTEL_SCRIPT" ]; then + echo -e "Status: ${RED}Not Available${NC}" + echo "Install secubox-p2p for mesh threat sharing" + return 1 + fi + + # Get threat intel status + local status=$("$THREAT_INTEL_SCRIPT" status 2>/dev/null) + + if [ -z "$status" ]; then + echo -e "Status: ${YELLOW}Initializing${NC}" + return 0 + fi + + local enabled=$(echo "$status" | jsonfilter -e '@.enabled' 2>/dev/null) + local local_iocs=$(echo "$status" | jsonfilter -e '@.local_iocs' 2>/dev/null || echo 0) + local received=$(echo "$status" | jsonfilter -e '@.received_iocs' 2>/dev/null || echo 0) + local applied=$(echo "$status" | jsonfilter -e '@.applied_iocs' 2>/dev/null || echo 0) + local peers=$(echo "$status" | jsonfilter -e '@.peer_contributors' 2>/dev/null || echo 0) + local chain_blocks=$(echo "$status" | jsonfilter -e '@.chain_threat_blocks' 2>/dev/null || echo 0) + + if [ "$enabled" = "true" ]; then + echo -e "Status: ${GREEN}Enabled${NC}" + else + echo -e "Status: ${YELLOW}Disabled${NC}" + fi + + echo "" + echo -e "${BOLD}Threat Intelligence:${NC}" + echo " Local IOCs: $local_iocs (from this node)" + echo " Received IOCs: $received (from mesh)" + echo " Applied IOCs: $applied" + echo " Peer Contributors: $peers" + echo " Chain Blocks: $chain_blocks" + + # Count Vortex-sourced IOCs in local + local vortex_local=0 + local ti_local="/var/lib/secubox/threat-intel/iocs-local.json" + if [ -f "$ti_local" ]; then + vortex_local=$(jsonfilter -i "$ti_local" -e '@[*].source' 2>/dev/null | grep -c "^vortex$" || echo 0) + fi + echo "" + echo -e "${BOLD}Vortex Contributions:${NC}" + echo " Domains Shared: $vortex_local" + echo "" +} + +mesh_publish() { + log "Publishing Vortex domains to mesh..." + + if [ ! -x "$THREAT_INTEL_SCRIPT" ]; then + error "secubox-p2p not installed" + return 1 + fi + + # Collect and publish + "$THREAT_INTEL_SCRIPT" collect 2>/dev/null + local result=$("$THREAT_INTEL_SCRIPT" publish 2>/dev/null) + + local published=$(echo "$result" | jsonfilter -e '@.published' 2>/dev/null || echo 0) + log "Published $published IOCs to mesh" +} + +mesh_sync() { + log "Syncing threats from mesh..." + + if [ ! -x "$THREAT_INTEL_SCRIPT" ]; then + error "secubox-p2p not installed" + return 1 + fi + + # Process pending blocks and apply + local result=$("$THREAT_INTEL_SCRIPT" apply-pending 2>/dev/null) + + local applied=$(echo "$result" | jsonfilter -e '@.applied' 2>/dev/null || echo 0) + local skipped=$(echo "$result" | jsonfilter -e '@.skipped' 2>/dev/null || echo 0) + + log "Applied: $applied, Skipped: $skipped" + + # Regenerate blocklist with new domains + if [ "$applied" -gt 0 ]; then + generate_blocklist + fi +} + +mesh_received() { + local lines="${1:-20}" + + echo "" + echo -e "${BOLD}Received Threats from Mesh${NC}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + if [ ! -x "$THREAT_INTEL_SCRIPT" ]; then + warn "secubox-p2p not installed" + return 1 + fi + + local received=$("$THREAT_INTEL_SCRIPT" list received 2>/dev/null) + local count=$(echo "$received" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) + + if [ "$count" -eq 0 ]; then + info "No threats received from mesh yet" + return 0 + fi + + echo "Total: $count received IOCs" + echo "" + + # Show recent domain IOCs + echo "$received" | jsonfilter -e '@[*]' 2>/dev/null | tail -n "$lines" | while read -r ioc; do + local domain=$(echo "$ioc" | jsonfilter -e '@.domain' 2>/dev/null) + local ip=$(echo "$ioc" | jsonfilter -e '@.ip' 2>/dev/null) + local severity=$(echo "$ioc" | jsonfilter -e '@.severity' 2>/dev/null) + local trust=$(echo "$ioc" | jsonfilter -e '@.trust' 2>/dev/null) + local applied=$(echo "$ioc" | jsonfilter -e '@.applied' 2>/dev/null) + local scenario=$(echo "$ioc" | jsonfilter -e '@.scenario' 2>/dev/null) + + local target="${domain:-$ip}" + [ -z "$target" ] && continue + + local status_icon="\u2705" + [ "$applied" = "false" ] && status_icon="\u23F3" + + printf "%-35s " "$target" + printf "%-10s " "$severity" + printf "%-12s " "$trust" + printf "%-20s " "$scenario" + echo -e "$status_icon" + done + + echo "" +} + +mesh_peers() { + echo "" + echo -e "${BOLD}Mesh Peer Contributions${NC}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + if [ ! -x "$THREAT_INTEL_SCRIPT" ]; then + warn "secubox-p2p not installed" + return 1 + fi + + local peers=$("$THREAT_INTEL_SCRIPT" peers 2>/dev/null) + local count=$(echo "$peers" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) + + if [ "$count" -eq 0 ]; then + info "No peer contributions yet" + return 0 + fi + + echo "$peers" | jsonfilter -e '@[*]' 2>/dev/null | while read -r peer; do + local node=$(echo "$peer" | jsonfilter -e '@.node' 2>/dev/null) + local trust=$(echo "$peer" | jsonfilter -e '@.trust' 2>/dev/null) + local ioc_count=$(echo "$peer" | jsonfilter -e '@.ioc_count' 2>/dev/null) + local applied_count=$(echo "$peer" | jsonfilter -e '@.applied_count' 2>/dev/null) + + printf "%-20s " "${node:0:20}" + printf "%-12s " "$trust" + printf "IOCs: %-5s " "$ioc_count" + printf "Applied: %-5s\n" "$applied_count" + done + + echo "" +} + +# ============================================================================ +# DNS Guard Integration (Phase 3) +# ============================================================================ + +DNSGUARD_DIR="/var/lib/dns-guard" +DNSGUARD_BLOCKLIST_DIR="/etc/dns-guard/blocklists" + +dnsguard_status() { + echo "" + echo -e "${BOLD}DNS Guard Integration${NC}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + # Check DNS Guard service + if pgrep -f "dns-guard" >/dev/null 2>&1; then + echo -e "Service: ${GREEN}Running${NC}" + elif [ -x /etc/init.d/dns-guard ]; then + local enabled=$(/etc/init.d/dns-guard enabled && echo yes || echo no) + if [ "$enabled" = "yes" ]; then + echo -e "Service: ${YELLOW}Enabled (not running)${NC}" + else + echo -e "Service: ${RED}Disabled${NC}" + fi + else + echo -e "Service: ${RED}Not installed${NC}" + return 1 + fi + + # Data files + echo "" + echo -e "${BOLD}Data Files:${NC}" + + if [ -f "$DNSGUARD_DIR/alerts.json" ]; then + local alert_count=$(jsonfilter -i "$DNSGUARD_DIR/alerts.json" -e '@[*]' 2>/dev/null | wc -l) + echo " Alerts: $alert_count entries" + else + echo " Alerts: (no file)" + fi + + if [ -f "$DNSGUARD_DIR/threat_domains.txt" ]; then + local domain_count=$(wc -l < "$DNSGUARD_DIR/threat_domains.txt" 2>/dev/null || echo 0) + echo " Threats: $domain_count domains" + else + echo " Threats: (no file)" + fi + + if [ -f "$DNSGUARD_DIR/pending_blocks.json" ]; then + local pending_count=$(jsonfilter -i "$DNSGUARD_DIR/pending_blocks.json" -e '@[*]' 2>/dev/null | wc -l) + echo " Pending: $pending_count approvals" + else + echo " Pending: (no file)" + fi + + # Vortex import stats + echo "" + echo -e "${BOLD}Vortex Integration:${NC}" + + if [ -f "$BLOCKLIST_DB" ]; then + local imported=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE source='dnsguard';" 2>/dev/null || echo 0) + local last_update=$(sqlite3 "$BLOCKLIST_DB" "SELECT last_update FROM feeds WHERE name='dnsguard';" 2>/dev/null || echo "never") + echo " Imported: $imported domains" + echo " Last Sync: $last_update" + + # Threat type breakdown + echo "" + echo -e "${BOLD}Detection Types from DNS Guard:${NC}" + sqlite3 "$BLOCKLIST_DB" \ + "SELECT ' ' || threat_type || ': ' || COUNT(*) FROM domains WHERE source='dnsguard' GROUP BY threat_type;" 2>/dev/null + fi + + echo "" +} + +dnsguard_sync() { + log "Syncing with DNS Guard..." + + feed_import_dnsguard + + # Regenerate blocklist with new entries + generate_blocklist + + log "DNS Guard sync complete" +} + +dnsguard_export() { + # Export Vortex threat intel back to DNS Guard blocklists (bidirectional) + log "Exporting Vortex intel to DNS Guard blocklists..." + + mkdir -p "$DNSGUARD_BLOCKLIST_DIR" + + local export_file="$DNSGUARD_BLOCKLIST_DIR/vortex-firewall.txt" + + # Export domains from external feeds (not DNS Guard's own detections) + sqlite3 "$BLOCKLIST_DB" \ + "SELECT domain FROM domains WHERE blocked=1 AND source != 'dnsguard';" 2>/dev/null > "$export_file" + + local count=$(wc -l < "$export_file" 2>/dev/null || echo 0) + log "Exported $count domains to: $export_file" + + # Signal DNS Guard to reload if running + if pgrep -f "dns-guard" >/dev/null 2>&1; then + killall -HUP dns-guard 2>/dev/null || true + log "Signaled DNS Guard to reload" + fi +} + +dnsguard_alerts() { + local lines="${1:-20}" + + echo "" + echo -e "${BOLD}Recent DNS Guard Alerts${NC}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + if [ ! -f "$DNSGUARD_DIR/alerts.json" ]; then + warn "No alerts file found" + return 1 + fi + + # Parse and display recent alerts + jsonfilter -i "$DNSGUARD_DIR/alerts.json" -e '@[*]' 2>/dev/null | tail -n "$lines" | while read -r alert; do + local domain=$(echo "$alert" | jsonfilter -e '@.domain' 2>/dev/null) + local client=$(echo "$alert" | jsonfilter -e '@.client' 2>/dev/null) + local type=$(echo "$alert" | jsonfilter -e '@.type' 2>/dev/null) + local confidence=$(echo "$alert" | jsonfilter -e '@.confidence' 2>/dev/null) + local reason=$(echo "$alert" | jsonfilter -e '@.reason' 2>/dev/null) + + [ -z "$domain" ] && continue + + printf "${YELLOW}%-30s${NC} " "$domain" + printf "%-12s " "$type" + printf "${CYAN}%3s%%${NC} " "$confidence" + printf "client=%s" "$client" + echo "" + done + + echo "" +} + # ============================================================================ # Service Control # ============================================================================ @@ -571,6 +1178,10 @@ service_start() { intel_update fi + # Start sinkhole if enabled + local sinkhole_enabled=$(uci -q get vortex-firewall.server.enabled) + [ "$sinkhole_enabled" = "1" ] && sinkhole_start + log "Vortex DNS Firewall active" log "Sinkhole IP: $SINKHOLE_IP" log "Blocked domains: $(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE blocked=1;")" @@ -579,6 +1190,9 @@ service_start() { service_stop() { log "Stopping Vortex DNS Firewall..." + # Stop sinkhole server + sinkhole_stop + # Remove dnsmasq config rm -f "$DNSMASQ_CONF" @@ -633,13 +1247,35 @@ Intel Commands: intel add Manually block a domain intel remove Unblock a domain +DNS Guard Integration (Phase 3): + dnsguard status Show DNS Guard integration status + dnsguard sync Force sync detections from DNS Guard + dnsguard export Export Vortex intel to DNS Guard blocklists + dnsguard alerts [N] Show recent DNS Guard alerts (default: 20) + +Mesh Threat Sharing (Phase 4): + mesh status Show mesh threat sharing status + mesh publish Publish local domains to mesh + mesh sync Sync and apply threats from mesh + mesh received [N] Show threats received from mesh (default: 20) + mesh peers Show peer contribution statistics + +Sinkhole Server: + sinkhole start Start HTTP/HTTPS sinkhole server + sinkhole stop Stop sinkhole server + sinkhole status Show sinkhole status and stats + sinkhole logs [N] Show last N sinkhole events (default: 50) + sinkhole export [file] Export events to JSON file + sinkhole gencert Generate self-signed HTTPS certificate + sinkhole clear Clear event log + Statistics: stats Show blocking statistics stats --x47 Show ×47 impact score stats --top-blocked Top blocked domains Service: - start Start firewall + start Start firewall (includes sinkhole if enabled) stop Stop firewall status Show service status @@ -650,6 +1286,10 @@ Examples: vortex-firewall intel update vortex-firewall intel search evil.com vortex-firewall intel add malware.example.com c2 + vortex-firewall dnsguard status + vortex-firewall dnsguard sync + vortex-firewall sinkhole start + vortex-firewall sinkhole logs 100 vortex-firewall stats --x47 EOF } @@ -671,6 +1311,21 @@ case "${1:-}" in esac ;; + sinkhole) + shift + case "${1:-}" in + start) sinkhole_start ;; + stop) sinkhole_stop ;; + status) sinkhole_status ;; + logs) shift; sinkhole_logs "$@" ;; + export) shift; sinkhole_export "$@" ;; + gencert) sinkhole_gencert ;; + clear) sinkhole_clear ;; + record) shift; sinkhole_record_event "$@" ;; + *) error "Unknown sinkhole command. Use: start, stop, status, logs, export, gencert, clear" ;; + esac + ;; + stats) shift case "${1:-}" in @@ -680,6 +1335,29 @@ case "${1:-}" in esac ;; + dnsguard) + shift + case "${1:-}" in + status) dnsguard_status ;; + sync) dnsguard_sync ;; + export) dnsguard_export ;; + alerts) shift; dnsguard_alerts "$@" ;; + *) error "Unknown dnsguard command. Use: status, sync, export, alerts" ;; + esac + ;; + + mesh) + shift + case "${1:-}" in + status) mesh_status ;; + publish) mesh_publish ;; + sync) mesh_sync ;; + received) shift; mesh_received "$@" ;; + peers) mesh_peers ;; + *) error "Unknown mesh command. Use: status, publish, sync, received, peers" ;; + esac + ;; + start) service_start ;; diff --git a/package/secubox/secubox-vortex-firewall/root/usr/share/rpcd/acl.d/luci-vortex-firewall.json b/package/secubox/secubox-vortex-firewall/root/usr/share/rpcd/acl.d/luci-vortex-firewall.json index 8ea2f070..a85b1471 100644 --- a/package/secubox/secubox-vortex-firewall/root/usr/share/rpcd/acl.d/luci-vortex-firewall.json +++ b/package/secubox/secubox-vortex-firewall/root/usr/share/rpcd/acl.d/luci-vortex-firewall.json @@ -3,12 +3,35 @@ "description": "Grant access to Vortex DNS Firewall", "read": { "ubus": { - "luci.vortex-firewall": ["status", "get_stats", "get_feeds", "get_blocked", "search"] + "luci.vortex-firewall": [ + "status", + "get_stats", + "get_feeds", + "get_blocked", + "search", + "sinkhole_status", + "sinkhole_events", + "sinkhole_stats", + "dnsguard_status", + "dnsguard_alerts", + "mesh_status", + "mesh_received", + "mesh_peers" + ] } }, "write": { "ubus": { - "luci.vortex-firewall": ["update_feeds", "block_domain", "unblock_domain"] + "luci.vortex-firewall": [ + "update_feeds", + "block_domain", + "unblock_domain", + "sinkhole_toggle", + "sinkhole_clear", + "dnsguard_sync", + "mesh_publish", + "mesh_sync" + ] } } } diff --git a/secubox-tools/secubox-image.sh b/secubox-tools/secubox-image.sh index 1d3daabf..b902f561 100755 --- a/secubox-tools/secubox-image.sh +++ b/secubox-tools/secubox-image.sh @@ -265,7 +265,7 @@ api_build() { log_step "Submitting build request to ASU..." local response - response=$(curl -s -w "\n%{http_code}" \ + response=$(curl -sL -w "\n%{http_code}" \ -H "Content-Type: application/json" \ -d "@$json_file" \ "$ASU_URL/api/v1/build") @@ -318,7 +318,7 @@ poll_build() { while [ $elapsed -lt $max_wait ]; do local response - response=$(curl -s -w "\n%{http_code}" "$ASU_URL/api/v1/build/$request_hash") + response=$(curl -sL -w "\n%{http_code}" "$ASU_URL/api/v1/build/$request_hash") local http_code http_code=$(echo "$response" | tail -1) local body @@ -442,7 +442,7 @@ print(rh, sha) log_step "Downloading: $filename" log_info "URL: $download_url" - curl -# -o "$output_file" "$download_url" || { + curl -#L -o "$output_file" "$download_url" || { log_error "Download failed" return 1 } @@ -603,7 +603,7 @@ cmd_status() { [[ -z "$hash" ]] && { log_error "Usage: $0 status "; return 1; } local response - response=$(curl -s "$ASU_URL/api/v1/build/$hash") + response=$(curl -sL "$ASU_URL/api/v1/build/$hash") echo "$response" | python3 -m json.tool 2>/dev/null || echo "$response" } @@ -612,7 +612,7 @@ cmd_download() { [[ -z "$hash" ]] && { log_error "Usage: $0 download "; return 1; } local response - response=$(curl -s "$ASU_URL/api/v1/build/$hash") + response=$(curl -sL "$ASU_URL/api/v1/build/$hash") download_image "$response" } diff --git a/secubox-tools/secubox-sysupgrade.sh b/secubox-tools/secubox-sysupgrade.sh index 556189ae..8ca73e10 100755 --- a/secubox-tools/secubox-sysupgrade.sh +++ b/secubox-tools/secubox-sysupgrade.sh @@ -132,7 +132,7 @@ asu_build() { echo "$json" > "$tmpfile" local response - response=$(curl -s -w "\n%{http_code}" \ + response=$(curl -sL -w "\n%{http_code}" \ -H "Content-Type: application/json" \ -d "@$tmpfile" \ "$ASU_URL/api/v1/build" 2>>"$LOG") @@ -189,7 +189,7 @@ poll_build() { while [ $elapsed -lt $max_wait ]; do local response - response=$(curl -s "$ASU_URL/api/v1/build/$hash" 2>/dev/null) + response=$(curl -sL "$ASU_URL/api/v1/build/$hash" 2>/dev/null) # Check if response has images (= complete) local image_count @@ -278,7 +278,7 @@ download_image() { fi log "Downloading: $image_name" - curl -# -o "$IMAGE_FILE" "$download_url" 2>>"$LOG" || { + curl -#L -o "$IMAGE_FILE" "$download_url" 2>>"$LOG" || { log_error "Download failed" return 1 } @@ -334,7 +334,7 @@ cmd_check() { # Check ASU for available versions log_info "Checking ASU server for available versions..." local overview - overview=$(curl -s "$ASU_URL/api/v1/overview" 2>/dev/null) + overview=$(curl -sL "$ASU_URL/api/v1/overview" 2>/dev/null) if [ -n "$overview" ]; then # Try to extract version info