diff --git a/package/secubox/secubox-auth-logger/Makefile b/package/secubox/secubox-auth-logger/Makefile
index 8a01d23c..cce49c31 100644
--- a/package/secubox/secubox-auth-logger/Makefile
+++ b/package/secubox/secubox-auth-logger/Makefile
@@ -4,7 +4,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-auth-logger
-PKG_VERSION:=1.0.0
+PKG_VERSION:=1.1.0
PKG_RELEASE:=1
PKG_ARCH:=all
PKG_LICENSE:=Apache-2.0
@@ -16,30 +16,60 @@ define Package/secubox-auth-logger
SECTION:=secubox
CATEGORY:=SecuBox
TITLE:=Authentication Failure Logger for CrowdSec
- DEPENDS:=+rpcd +uhttpd
+ DEPENDS:=+rpcd +uhttpd +libubox-lua
PKGARCH:=all
endef
define Package/secubox-auth-logger/description
Logs authentication failures from LuCI/rpcd and Dropbear SSH
- for CrowdSec detection. Patches rpcd to emit auth failure logs
- to syslog in a format CrowdSec can parse.
+ for CrowdSec detection. Includes:
+ - SSH failure monitoring (OpenSSH/Dropbear)
+ - LuCI web interface auth failure logging via CGI hook
+ - JavaScript hook to intercept login failures
+ - CrowdSec parser and bruteforce scenario
endef
define Build/Compile
endef
define Package/secubox-auth-logger/install
+ # Auth monitor script
$(INSTALL_DIR) $(1)/usr/lib/secubox
$(INSTALL_BIN) ./files/auth-monitor.sh $(1)/usr/lib/secubox/
+
+ # Init script
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/secubox-auth-logger.init $(1)/etc/init.d/secubox-auth-logger
+
+ # RPCD plugin for auth logging via ubus
+ $(INSTALL_DIR) $(1)/usr/libexec/rpcd
+ $(INSTALL_BIN) ./files/secubox.auth-logger $(1)/usr/libexec/rpcd/
+
+ # ACL for rpcd permissions
+ $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
+ $(INSTALL_DATA) ./files/luci-secubox-auth.acl.json $(1)/usr/share/rpcd/acl.d/
+
+ # CGI hook for getting client IP during auth
+ $(INSTALL_DIR) $(1)/www/cgi-bin
+ $(INSTALL_BIN) ./files/auth-hook.cgi $(1)/www/cgi-bin/secubox-auth-hook
+
+ # JavaScript hook for LuCI login interception
+ $(INSTALL_DIR) $(1)/www/luci-static/resources/secubox
+ $(INSTALL_DATA) ./files/secubox-auth-hook.js $(1)/www/luci-static/resources/secubox/
+
+ # CrowdSec parser
$(INSTALL_DIR) $(1)/etc/crowdsec/parsers/s01-parse
$(INSTALL_DATA) ./files/openwrt-luci-auth.yaml $(1)/etc/crowdsec/parsers/s01-parse/
+
+ # CrowdSec scenario
$(INSTALL_DIR) $(1)/etc/crowdsec/scenarios
$(INSTALL_DATA) ./files/openwrt-luci-bf.yaml $(1)/etc/crowdsec/scenarios/
+
+ # CrowdSec acquisition config
$(INSTALL_DIR) $(1)/etc/crowdsec/acquis.d
$(INSTALL_DATA) ./files/secubox-auth-acquis.yaml $(1)/etc/crowdsec/acquis.d/
+
+ # UCI defaults for first boot setup
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_BIN) ./files/99-secubox-auth-logger $(1)/etc/uci-defaults/
endef
@@ -47,8 +77,28 @@ endef
define Package/secubox-auth-logger/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
+ # Restart rpcd to load new plugin
+ /etc/init.d/rpcd restart 2>/dev/null
+
+ # Enable and start auth monitor
/etc/init.d/secubox-auth-logger enable
/etc/init.d/secubox-auth-logger start
+
+ # Run uci-defaults to inject JS hook
+ /etc/uci-defaults/99-secubox-auth-logger 2>/dev/null || true
+
+ echo "SecuBox Auth Logger installed - LuCI login failures now logged for CrowdSec"
+}
+exit 0
+endef
+
+define Package/secubox-auth-logger/postrm
+#!/bin/sh
+[ -n "$${IPKG_INSTROOT}" ] || {
+ # Remove JS hook from LuCI header
+ if [ -f /usr/lib/lua/luci/view/themes/bootstrap/header.htm ]; then
+ sed -i '/secubox-auth-hook/d' /usr/lib/lua/luci/view/themes/bootstrap/header.htm 2>/dev/null || true
+ fi
}
exit 0
endef
diff --git a/package/secubox/secubox-auth-logger/files/99-secubox-auth-logger b/package/secubox/secubox-auth-logger/files/99-secubox-auth-logger
index c8b8af42..e60087cc 100644
--- a/package/secubox/secubox-auth-logger/files/99-secubox-auth-logger
+++ b/package/secubox/secubox-auth-logger/files/99-secubox-auth-logger
@@ -1,6 +1,7 @@
#!/bin/sh
# SecuBox Auth Logger - Post-install configuration
# Enables verbose logging for uhttpd and configures CrowdSec
+# Copyright (C) 2024 CyberMind.fr
# Note: Dropbear 2024.86 does NOT support -v flag
# Auth monitoring relies on parsing existing syslog messages
@@ -17,6 +18,55 @@ fi
touch /var/log/secubox-auth.log
chmod 644 /var/log/secubox-auth.log
+# Inject JS hook into LuCI login page
+# Try multiple locations for different LuCI versions/themes
+inject_js_hook() {
+ local hook_script=''
+ local hook_marker="secubox-auth-hook"
+
+ # Method 1: Bootstrap theme header (LuCI 19.x+)
+ if [ -f /usr/lib/lua/luci/view/themes/bootstrap/header.htm ]; then
+ if ! grep -q "$hook_marker" /usr/lib/lua/luci/view/themes/bootstrap/header.htm 2>/dev/null; then
+ sed -i "s||$hook_script\n|" /usr/lib/lua/luci/view/themes/bootstrap/header.htm 2>/dev/null
+ fi
+ fi
+
+ # Method 2: Material theme header
+ if [ -f /usr/lib/lua/luci/view/themes/material/header.htm ]; then
+ if ! grep -q "$hook_marker" /usr/lib/lua/luci/view/themes/material/header.htm 2>/dev/null; then
+ sed -i "s||$hook_script\n|" /usr/lib/lua/luci/view/themes/material/header.htm 2>/dev/null
+ fi
+ fi
+
+ # Method 3: OpenWrt theme header
+ if [ -f /usr/lib/lua/luci/view/themes/openwrt/header.htm ]; then
+ if ! grep -q "$hook_marker" /usr/lib/lua/luci/view/themes/openwrt/header.htm 2>/dev/null; then
+ sed -i "s||$hook_script\n|" /usr/lib/lua/luci/view/themes/openwrt/header.htm 2>/dev/null
+ fi
+ fi
+
+ # Method 4: Base sysauth view (fallback for login page)
+ if [ -f /usr/lib/lua/luci/view/sysauth.htm ]; then
+ if ! grep -q "$hook_marker" /usr/lib/lua/luci/view/sysauth.htm 2>/dev/null; then
+ sed -i "s||$hook_script\n|" /usr/lib/lua/luci/view/sysauth.htm 2>/dev/null
+ fi
+ fi
+
+ # Method 5: LuCI2 / luci-mod-admin-full footer
+ if [ -f /www/luci-static/resources/footer.htm ]; then
+ if ! grep -q "$hook_marker" /www/luci-static/resources/footer.htm 2>/dev/null; then
+ echo "$hook_script" >> /www/luci-static/resources/footer.htm 2>/dev/null
+ fi
+ fi
+}
+
+inject_js_hook
+
+# Restart rpcd to load new ubus object
+if [ -x /etc/init.d/rpcd ]; then
+ /etc/init.d/rpcd restart 2>/dev/null
+fi
+
# Restart CrowdSec to pick up new acquisition/parser/scenario
if [ -x /etc/init.d/crowdsec ]; then
/etc/init.d/crowdsec restart 2>/dev/null
diff --git a/package/secubox/secubox-auth-logger/files/auth-hook.cgi b/package/secubox/secubox-auth-logger/files/auth-hook.cgi
new file mode 100644
index 00000000..18f31b04
--- /dev/null
+++ b/package/secubox/secubox-auth-logger/files/auth-hook.cgi
@@ -0,0 +1,150 @@
+#!/bin/sh
+# SecuBox Auth Hook - CGI endpoint for LuCI authentication with logging
+# Copyright (C) 2024 CyberMind.fr
+#
+# This CGI script intercepts login attempts and logs failures with the real client IP
+# Call via: POST /cgi-bin/secubox-auth-hook
+#
+# Request body: {"username":"...", "password":"..."}
+# Special: If password is "__SECUBOX_LOG_FAILURE__", just log the failure (used by JS hook)
+# Response: Same as ubus session.login
+
+. /usr/share/libubox/jshn.sh
+
+LOG_FILE="/var/log/secubox-auth.log"
+LOG_TAG="secubox-auth"
+
+# Get client IP from CGI environment
+CLIENT_IP="${REMOTE_ADDR:-127.0.0.1}"
+
+# Handle X-Forwarded-For if present (reverse proxy)
+if [ -n "$HTTP_X_FORWARDED_FOR" ]; then
+ CLIENT_IP="${HTTP_X_FORWARDED_FOR%%,*}"
+fi
+
+# Sanitize IP (remove IPv6 brackets if present)
+CLIENT_IP=$(echo "$CLIENT_IP" | sed 's/^\[//;s/\]$//')
+
+# Log authentication failure
+log_failure() {
+ local user="$1"
+ local ts=$(date "+%b %d %H:%M:%S")
+ local hostname=$(cat /proc/sys/kernel/hostname 2>/dev/null || echo "OpenWrt")
+
+ # Ensure log file exists
+ [ -f "$LOG_FILE" ] || { touch "$LOG_FILE"; chmod 644 "$LOG_FILE"; }
+
+ # Log to dedicated file for CrowdSec
+ echo "$ts $hostname $LOG_TAG[$$]: authentication failure for $user from $CLIENT_IP via luci" >> "$LOG_FILE"
+
+ # Also log to syslog
+ logger -t "$LOG_TAG" -p auth.warning "authentication failure for $user from $CLIENT_IP via luci"
+}
+
+# Output HTTP headers
+echo "Content-Type: application/json"
+echo ""
+
+# Handle GET request (for IP detection only)
+if [ "$REQUEST_METHOD" = "GET" ]; then
+ json_init
+ json_add_string ip "$CLIENT_IP"
+ json_add_string method "GET"
+ json_dump
+ exit 0
+fi
+
+# Handle POST request (login attempt or log-only)
+if [ "$REQUEST_METHOD" = "POST" ]; then
+ # Read POST body
+ read -r body
+
+ # Parse JSON input
+ json_load "$body" 2>/dev/null
+ if [ $? -ne 0 ]; then
+ json_init
+ json_add_boolean success 0
+ json_add_string error "Invalid JSON"
+ json_dump
+ exit 0
+ fi
+
+ json_get_var username username
+ json_get_var password password
+
+ # Validate input
+ if [ -z "$username" ]; then
+ json_init
+ json_add_boolean success 0
+ json_add_string error "Missing username"
+ json_dump
+ exit 0
+ fi
+
+ # Check if this is a log-only request from our JS hook
+ if [ "$password" = "__SECUBOX_LOG_FAILURE__" ]; then
+ # Just log the failure - don't attempt login
+ log_failure "$username"
+ json_init
+ json_add_boolean success 1
+ json_add_string message "Auth failure logged"
+ json_add_string ip "$CLIENT_IP"
+ json_add_string username "$username"
+ json_dump
+ exit 0
+ fi
+
+ # Normal login flow - validate password
+ if [ -z "$password" ]; then
+ json_init
+ json_add_boolean success 0
+ json_add_string error "Missing password"
+ json_dump
+ exit 0
+ fi
+
+ # Attempt login via ubus
+ result=$(ubus call session login "{\"username\":\"$username\",\"password\":\"$password\"}" 2>&1)
+ rc=$?
+
+ # Check if login failed
+ # ubus returns error code or empty session on failure
+ if [ $rc -ne 0 ]; then
+ # ubus call failed
+ log_failure "$username"
+ json_init
+ json_add_boolean success 0
+ json_add_string error "Authentication failed"
+ json_add_string ip "$CLIENT_IP"
+ json_dump
+ elif echo "$result" | grep -q '"ubus_rpc_session": ""'; then
+ # Empty session token = failed login
+ log_failure "$username"
+ json_init
+ json_add_boolean success 0
+ json_add_string error "Invalid credentials"
+ json_add_string ip "$CLIENT_IP"
+ json_dump
+ else
+ # Check if result contains valid session
+ session=$(echo "$result" | jsonfilter -e '@.ubus_rpc_session' 2>/dev/null)
+ if [ -z "$session" ] || [ "$session" = "null" ]; then
+ log_failure "$username"
+ json_init
+ json_add_boolean success 0
+ json_add_string error "Invalid credentials"
+ json_add_string ip "$CLIENT_IP"
+ json_dump
+ else
+ # Login successful - return the session info
+ echo "$result"
+ fi
+ fi
+ exit 0
+fi
+
+# Unsupported method
+json_init
+json_add_boolean success 0
+json_add_string error "Unsupported method"
+json_dump
diff --git a/package/secubox/secubox-auth-logger/files/luci-secubox-auth.acl.json b/package/secubox/secubox-auth-logger/files/luci-secubox-auth.acl.json
new file mode 100644
index 00000000..68a72ce7
--- /dev/null
+++ b/package/secubox/secubox-auth-logger/files/luci-secubox-auth.acl.json
@@ -0,0 +1,15 @@
+{
+ "secubox-auth-logger": {
+ "description": "SecuBox Authentication Logger",
+ "read": {
+ "ubus": {
+ "secubox.auth-logger": ["log_failure", "get_client_info", "wrapped_login"]
+ }
+ },
+ "write": {
+ "ubus": {
+ "secubox.auth-logger": ["log_failure", "wrapped_login"]
+ }
+ }
+ }
+}
diff --git a/package/secubox/secubox-auth-logger/files/openwrt-luci-auth.yaml b/package/secubox/secubox-auth-logger/files/openwrt-luci-auth.yaml
index 379011e0..3064060f 100644
--- a/package/secubox/secubox-auth-logger/files/openwrt-luci-auth.yaml
+++ b/package/secubox/secubox-auth-logger/files/openwrt-luci-auth.yaml
@@ -1,6 +1,6 @@
# CrowdSec Parser for SecuBox Auth Logger
-# Parses authentication failures from LuCI/uhttpd and Dropbear
-# Format: secubox-auth: Authentication failure for from via
+# Parses authentication failures from LuCI/uhttpd and SSH (OpenSSH/Dropbear)
+# Format: secubox-auth[pid]: authentication failure for from via
name: secubox/openwrt-luci-auth
description: "Parse SecuBox auth failure logs for LuCI and SSH"
@@ -9,7 +9,8 @@ onsuccess: next_stage
nodes:
- grok:
- pattern: "Authentication failure for %{USERNAME:user} from %{IP:source_ip} via %{WORD:service}"
+ # Case-insensitive match for "authentication failure"
+ pattern: "(?i)authentication failure for %{USERNAME:user} from %{IP:source_ip} via %{WORD:service}"
apply_on: message
statics:
- meta: log_type
@@ -18,3 +19,5 @@ nodes:
expression: evt.Parsed.service
- meta: source_ip
expression: evt.Parsed.source_ip
+ - meta: username
+ expression: evt.Parsed.user
diff --git a/package/secubox/secubox-auth-logger/files/secubox-auth-hook.js b/package/secubox/secubox-auth-logger/files/secubox-auth-hook.js
new file mode 100644
index 00000000..169baf87
--- /dev/null
+++ b/package/secubox/secubox-auth-logger/files/secubox-auth-hook.js
@@ -0,0 +1,194 @@
+/**
+ * SecuBox Auth Hook - Intercepts LuCI login failures for CrowdSec
+ * Copyright (C) 2024 CyberMind.fr
+ *
+ * This script hooks into LuCI's authentication system to log
+ * failed login attempts with the real client IP address.
+ *
+ * The hook intercepts XMLHttpRequest calls to session.login and
+ * reports failures to our CGI endpoint which has access to REMOTE_ADDR.
+ */
+
+(function() {
+ 'use strict';
+
+ // Only run once
+ if (window._secuboxAuthHookLoaded) return;
+ window._secuboxAuthHookLoaded = true;
+
+ var AUTH_HOOK_URL = '/cgi-bin/secubox-auth-hook';
+
+ // Debounce to avoid multiple logs for same attempt
+ var lastLogTime = 0;
+ var lastLogUser = '';
+
+ /**
+ * Log auth failure to our CGI endpoint
+ * The CGI endpoint gets REMOTE_ADDR and logs with real client IP
+ */
+ function logAuthFailure(username) {
+ var now = Date.now();
+ // Debounce: don't log same user within 2 seconds
+ if (username === lastLogUser && (now - lastLogTime) < 2000) {
+ return;
+ }
+ lastLogTime = now;
+ lastLogUser = username;
+
+ try {
+ var xhr = new XMLHttpRequest();
+ xhr.open('POST', AUTH_HOOK_URL, true);
+ xhr.setRequestHeader('Content-Type', 'application/json');
+ // Send with dummy password - CGI will detect it's a log-only call
+ // and just log the failure with the real client IP
+ xhr.send(JSON.stringify({
+ username: username || 'root',
+ password: '__SECUBOX_LOG_FAILURE__'
+ }));
+ } catch (e) {
+ // Silently fail - don't break login flow
+ }
+ }
+
+ /**
+ * Check if a ubus response indicates login failure
+ */
+ function isLoginFailure(result) {
+ if (!result) return false;
+
+ // UBUS JSON-RPC response format: { result: [error_code, data] }
+ if (result.result && Array.isArray(result.result)) {
+ var errorCode = result.result[0];
+ var data = result.result[1];
+
+ // Error code != 0 means failure
+ if (errorCode !== 0) return true;
+
+ // Check for empty session (credential failure)
+ if (data && data.ubus_rpc_session === '') return true;
+ if (data && !data.ubus_rpc_session) return true;
+ }
+
+ // Check for error response
+ if (result.error) return true;
+
+ return false;
+ }
+
+ /**
+ * Extract username from login call params
+ */
+ function extractUsername(call) {
+ try {
+ if (call.params && call.params[2] && call.params[2].username) {
+ return call.params[2].username;
+ }
+ } catch (e) {}
+ return 'root';
+ }
+
+ /**
+ * Intercept fetch API calls (modern LuCI)
+ */
+ var originalFetch = window.fetch;
+ if (originalFetch) {
+ window.fetch = function(url, options) {
+ var requestBody = null;
+ var loginCalls = [];
+
+ // Parse request to find login calls
+ if (url && url.indexOf('ubus') !== -1 && options && options.body) {
+ try {
+ requestBody = JSON.parse(options.body);
+ if (Array.isArray(requestBody)) {
+ requestBody.forEach(function(call, idx) {
+ if (call && call.method === 'call' &&
+ call.params && call.params[1] === 'login') {
+ loginCalls.push({ call: call, idx: idx });
+ }
+ });
+ }
+ } catch (e) {}
+ }
+
+ return originalFetch.apply(this, arguments).then(function(response) {
+ // Check login results
+ if (loginCalls.length > 0) {
+ response.clone().json().then(function(data) {
+ if (Array.isArray(data)) {
+ loginCalls.forEach(function(item) {
+ var result = data[item.idx];
+ if (isLoginFailure(result)) {
+ logAuthFailure(extractUsername(item.call));
+ }
+ });
+ }
+ }).catch(function() {});
+ }
+ return response;
+ });
+ };
+ }
+
+ /**
+ * Intercept XMLHttpRequest (older LuCI versions)
+ */
+ var originalXHRSend = XMLHttpRequest.prototype.send;
+ var originalXHROpen = XMLHttpRequest.prototype.open;
+
+ XMLHttpRequest.prototype.open = function(method, url) {
+ this._secuboxUrl = url;
+ this._secuboxMethod = method;
+ return originalXHROpen.apply(this, arguments);
+ };
+
+ XMLHttpRequest.prototype.send = function(body) {
+ var xhr = this;
+ var url = this._secuboxUrl;
+ var loginCalls = [];
+
+ // Only intercept POST to ubus
+ if (this._secuboxMethod === 'POST' && url && url.indexOf('ubus') !== -1 && body) {
+ try {
+ var parsedBody = JSON.parse(body);
+ if (Array.isArray(parsedBody)) {
+ parsedBody.forEach(function(call, idx) {
+ if (call && call.method === 'call' &&
+ call.params && call.params[1] === 'login') {
+ loginCalls.push({ call: call, idx: idx });
+ }
+ });
+ }
+ } catch (e) {}
+ }
+
+ if (loginCalls.length > 0) {
+ var originalOnReadyStateChange = xhr.onreadystatechange;
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState === 4 && xhr.status === 200) {
+ try {
+ var response = JSON.parse(xhr.responseText);
+ if (Array.isArray(response)) {
+ loginCalls.forEach(function(item) {
+ var result = response[item.idx];
+ if (isLoginFailure(result)) {
+ logAuthFailure(extractUsername(item.call));
+ }
+ });
+ }
+ } catch (e) {}
+ }
+ if (originalOnReadyStateChange) {
+ originalOnReadyStateChange.apply(this, arguments);
+ }
+ };
+ }
+
+ return originalXHRSend.apply(this, arguments);
+ };
+
+ // Debug message in console
+ if (window.console && console.log) {
+ console.log('[SecuBox] Auth hook v1.1 loaded - LuCI login failures will be logged for CrowdSec');
+ }
+})();
diff --git a/package/secubox/secubox-auth-logger/files/secubox.auth-logger b/package/secubox/secubox-auth-logger/files/secubox.auth-logger
new file mode 100644
index 00000000..6a043419
--- /dev/null
+++ b/package/secubox/secubox-auth-logger/files/secubox.auth-logger
@@ -0,0 +1,143 @@
+#!/bin/sh
+# SecuBox Auth Logger - RPCD plugin for LuCI authentication logging
+# Copyright (C) 2024 CyberMind.fr
+#
+# This plugin wraps session.login to log authentication failures
+# for CrowdSec detection.
+
+. /lib/functions.sh
+. /usr/share/libubox/jshn.sh
+
+LOG_FILE="/var/log/secubox-auth.log"
+LOG_TAG="secubox-auth"
+
+# Ensure log file exists
+[ -f "$LOG_FILE" ] || { touch "$LOG_FILE"; chmod 644 "$LOG_FILE"; }
+
+# Log authentication failure
+log_auth_failure() {
+ local ip="$1"
+ local user="${2:-root}"
+ local service="${3:-luci}"
+
+ # Get timestamp in syslog format
+ local ts=$(date "+%b %d %H:%M:%S")
+ local hostname=$(cat /proc/sys/kernel/hostname 2>/dev/null || echo "OpenWrt")
+
+ # Log to dedicated file for CrowdSec
+ echo "$ts $hostname $LOG_TAG[$$]: authentication failure for $user from $ip via $service" >> "$LOG_FILE"
+
+ # Also log to syslog for visibility
+ logger -t "$LOG_TAG" -p auth.warning "authentication failure for $user from $ip via $service"
+}
+
+# Get client IP from environment or connection info
+get_client_ip() {
+ # Try various methods to get client IP
+
+ # Method 1: REMOTE_ADDR (if called via CGI)
+ [ -n "$REMOTE_ADDR" ] && echo "$REMOTE_ADDR" && return
+
+ # Method 2: HTTP_X_FORWARDED_FOR (if behind proxy)
+ [ -n "$HTTP_X_FORWARDED_FOR" ] && echo "${HTTP_X_FORWARDED_FOR%%,*}" && return
+
+ # Method 3: Parse from uhttpd connection (not available in rpcd context)
+ # Method 4: Default to local if unknown
+ echo "127.0.0.1"
+}
+
+# Perform login via ubus and return result
+do_login() {
+ local username="$1"
+ local password="$2"
+ local client_ip="$3"
+
+ # Call the real session.login via ubus
+ local result
+ result=$(ubus call session login "{\"username\":\"$username\",\"password\":\"$password\"}" 2>&1)
+ local rc=$?
+
+ if [ $rc -ne 0 ] || echo "$result" | grep -q '"ubus_rpc_session": ""' || echo "$result" | grep -qi "error\|denied"; then
+ # Login failed - log it
+ log_auth_failure "$client_ip" "$username" "luci"
+
+ # Return failure
+ json_init
+ json_add_boolean success 0
+ json_add_string error "Login failed"
+ json_add_string ip "$client_ip"
+ json_dump
+ else
+ # Login succeeded - return the session token
+ echo "$result"
+ fi
+}
+
+case "$1" in
+ list)
+ # List available methods
+ cat <<'EOF'
+{
+ "wrapped_login": {
+ "username": "str",
+ "password": "str",
+ "client_ip": "str"
+ },
+ "log_failure": {
+ "ip": "str",
+ "username": "str",
+ "service": "str"
+ },
+ "get_client_info": {}
+}
+EOF
+ ;;
+ call)
+ case "$2" in
+ wrapped_login)
+ # Parse JSON input
+ read -r input
+ json_load "$input"
+ json_get_var username username
+ json_get_var password password
+ json_get_var client_ip client_ip
+
+ # Use provided IP or try to detect
+ [ -z "$client_ip" ] && client_ip=$(get_client_ip)
+
+ # Perform login with logging
+ do_login "$username" "$password" "$client_ip"
+ ;;
+
+ log_failure)
+ # Direct logging method for JS to call
+ read -r input
+ json_load "$input"
+ json_get_var ip ip
+ json_get_var username username
+ json_get_var service service
+
+ [ -z "$ip" ] && ip="unknown"
+ [ -z "$username" ] && username="root"
+ [ -z "$service" ] && service="luci"
+
+ log_auth_failure "$ip" "$username" "$service"
+
+ json_init
+ json_add_boolean success 1
+ json_add_string message "Auth failure logged for $ip"
+ json_dump
+ ;;
+
+ get_client_info)
+ # Return whatever client info we can detect
+ local ip=$(get_client_ip)
+
+ json_init
+ json_add_string ip "$ip"
+ json_add_string remote_addr "${REMOTE_ADDR:-unknown}"
+ json_dump
+ ;;
+ esac
+ ;;
+esac