secubox-openwrt/package/secubox/secubox-app-auth-logger/files/secubox-auth-hook.js
CyberMind-FR e62919eec7 refactor(packages): Rename and reorganize SecuBox packages
- Rename crowdsec-firewall-bouncer to secubox-app-cs-firewall-bouncer
- Rename secubox-auth-logger to secubox-app-auth-logger
- Delete secubox-crowdsec-setup (merged into other packages)
- Fix circular dependencies in luci-app-secubox-crowdsec
- Fix dependency chain in secubox-app-crowdsec-bouncer
- Add consolidated get_overview API to crowdsec-dashboard
- Improve crowdsec-dashboard overview performance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 10:42:52 +01:00

195 lines
6.6 KiB
JavaScript

/**
* 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');
}
})();