diff --git a/docs/DOCKER_INSTALLATION.md b/docs/DOCKER_INSTALLATION.md
index 1c03e9985..70183b109 100644
--- a/docs/DOCKER_INSTALLATION.md
+++ b/docs/DOCKER_INSTALLATION.md
@@ -36,7 +36,7 @@ docker run -d --rm --network=host \
> Runtime UID/GID: The image defaults to a service user `netalertx` (UID/GID 20211). A separate readonly lock owner also uses UID/GID 20211 for 004/005 immutability. You can override the runtime UID/GID at build (ARG) or run (`--user` / compose `user:`) but must align writable mounts (`/data`, `/tmp*`) and tmpfs `uid/gid` to that choice.
-See alternative [docked-compose examples](https://docs.netalertx.com/DOCKER_COMPOSE).
+See alternative [docker-compose examples](https://docs.netalertx.com/DOCKER_COMPOSE).
### Default ports
diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md
index d108aeea3..e8224fd2c 100755
--- a/docs/PLUGINS.md
+++ b/docs/PLUGINS.md
@@ -62,6 +62,7 @@ Device-detecting plugins insert values into the `CurrentScan` database table. T
| `INTRNT` | [internet_ip](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/internet_ip/) | 🔍 | Internet IP scanner | | |
| `INTRSPD` | [internet_speedtest](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/internet_speedtest/) | ♻ | Internet speed test | | |
| `IPNEIGH` | [ipneigh](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/ipneigh/) | 🔍 | Scan ARP (IPv4) and NDP (IPv6) tables | | |
+| `KEALSS` | [kea_api](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/kea_api/) | 🔍/🆎 | Pull lease data from the Kea DHCP API | | |
| `LUCIRPC` | [luci_import](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/luci_import/) | 🔍 | Import connected devices from OpenWRT | | |
| `MAINT` | [maintenance](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/maintenance/) | ⚙ | Maintenance of logs, etc. | | |
| `MQTT` | [_publisher_mqtt](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/_publisher_mqtt/) | ▶️ | MQTT for synching to Home Assistant | | |
diff --git a/front/php/templates/language/ca_ca.json b/front/php/templates/language/ca_ca.json
index 807ab146e..4e28136cd 100644
--- a/front/php/templates/language/ca_ca.json
+++ b/front/php/templates/language/ca_ca.json
@@ -66,8 +66,8 @@
"CustProps_cant_remove": "No es pot eliminar, es necessita una propietat mínim.",
"DAYS_TO_KEEP_EVENTS_description": "Això és una configuració de manteniment. Especifica el nombre de dies que es conservaran els esdeveniments. Els esdeveniments antics s'esborraran periòdicament. També aplica als esdeveniments dels Connectors (Plugins).",
"DAYS_TO_KEEP_EVENTS_name": "Esborrar esdeveniments més vells de",
- "DEEP_SLEEP_description": "",
- "DEEP_SLEEP_name": "",
+ "DEEP_SLEEP_description": "Redueix l'ús de la CPU ampliant els temps d'espera inactiu entre els cicles de processament. Quan està activat, les exploracions es poden retardar fins a 1 minut i la interfície d'usuari pot ser menys sensible.",
+ "DEEP_SLEEP_name": "Son profund",
"DISCOVER_PLUGINS_description": "Desactiva aquesta opció per accelerar la inicialització i l'estalvi de configuració. Quan està desactivat, els connectors no es descobreixen, i no podeu afegir nous connectors a la configuració LOADED_PLUGINS.",
"DISCOVER_PLUGINS_name": "Descobreix els plugins",
"DevDetail_Children_Title": "Relacions filles",
@@ -141,7 +141,7 @@
"DevDetail_SessionTable_Duration": "Durada",
"DevDetail_SessionTable_IP": "IP",
"DevDetail_SessionTable_Order": "Ordre",
- "DevDetail_Shortcut_CurrentStatus": "Estat actual",
+ "DevDetail_Shortcut_CurrentStatus": "Estat",
"DevDetail_Shortcut_DownAlerts": "Aturar alertes",
"DevDetail_Shortcut_Presence": "Presència",
"DevDetail_Shortcut_Sessions": "Sessions",
@@ -250,7 +250,7 @@
"Device_TableHead_NetworkSite": "Network Site",
"Device_TableHead_Owner": "Propietari",
"Device_TableHead_ParentRelType": "Tipus de relació",
- "Device_TableHead_Parent_MAC": "Node pare de xarxa",
+ "Device_TableHead_Parent_MAC": "Node pare",
"Device_TableHead_Port": "Port",
"Device_TableHead_PresentLastScan": "Presència",
"Device_TableHead_ReqNicsOnline": "Requereix NICs En línia",
@@ -346,7 +346,7 @@
"Gen_LockedDB": "ERROR - DB podria estar bloquejada - Fes servir F12 Eines desenvolupament -> Consola o provar-ho més tard.",
"Gen_NetworkMask": "Màscara de xarxa",
"Gen_New": "Nou",
- "Gen_No_Data": "",
+ "Gen_No_Data": "Sense dades",
"Gen_Offline": "Fora de línia",
"Gen_Okay": "Ok",
"Gen_Online": "En línia",
@@ -808,4 +808,4 @@
"settings_system_label": "Sistema",
"settings_update_item_warning": "Actualitza el valor sota. Sigues curós de seguir el format anterior. No hi ha validació.",
"test_event_tooltip": "Deseu els canvis primer abans de comprovar la configuració."
-}
\ No newline at end of file
+}
diff --git a/front/plugins/kea_api/README.md b/front/plugins/kea_api/README.md
new file mode 100755
index 000000000..9ddc174c0
--- /dev/null
+++ b/front/plugins/kea_api/README.md
@@ -0,0 +1,99 @@
+## Overview
+
+A plugin allowing for importing devices from the Kea DHCP API.
+https://www.isc.org/kea/
+
+And specifically:
+https://kea.readthedocs.io/en/kea-2.6.3/api.html#lease4-get-all
+
+
+### Usage
+
+To enable the API, first you want to add something like this to your main kea configuration (this is for debian 13):
+
+```json
+ "control-socket": {
+ "socket-type": "unix",
+ "socket-name": "/run/kea/kea4-ctrl-socket"
+ },
+
+ "hooks-libraries": [
+ {
+ "library": "/usr/lib/x86_64-linux-gnu/kea/hooks/libdhcp_lease_cmds.so"
+ }
+ ],
+```
+
+
+And you need to install kea-ctrl-agent, with a config that looks something like this:
+
+```json
+{
+"Control-agent": {
+ "http-host": "127.0.0.1",
+ "http-port": 8000,
+
+ "authentication": {
+ "type": "basic",
+ "realm": "Kea Control Agent",
+ "directory": "/etc/kea",
+ "clients": [
+ {
+ "user": "kea-api",
+ "password-file": "kea-api-password"
+ }
+ ]
+ },
+ "control-sockets": {
+ "dhcp4": {
+ "socket-type": "unix",
+ "socket-name": "/run/kea/kea4-ctrl-socket"
+ }
+ },
+ "loggers": [
+ {
+ "name": "kea-ctrl-agent",
+ "output-options": [
+ {
+ "output": "stdout",
+ "pattern": "%-5p %m\n"
+ }
+ ],
+ "severity": "INFO",
+ "debuglevel": 0
+ }
+ ]
+}
+}
+```
+
+You will need to configure the plugin with the URL to the API, and the username and password configured above (from kea-api-password file in the example)
+
+
+#### Required Settings
+
+These settings are required, besides the common device scanner settings:
+
+- **Kea Control Agent URL** (`KEALSS_URL`): The full URL, including port number, to the Kea API.
+ - Default: `http://127.0.0.1:8000`
+ - This mirrors what you set up in the kea-ctrl-agent configuration.
+
+- **Basic Auth Username** (`KEALSS_USER`): The user to use for authenticating with the Kea API.
+ - Default: `kea-api`
+ - This mirrors what you set up in the kea-ctrl-agent configuration.
+
+- **Basic Auth Password** (`KEALSS_PASS`): The password to use for authenticating with the Kea API.
+ - This mirrors what you set up in the kea-ctrl-agent configuration.
+ - When using a password file, it should be the content of the password file.
+
+
+### Notes
+
+- This was tested on a basic Debian 13 install.
+- When you install kea-ctrl-agent, it should ask you about creating a password.
+- It's possible to run kea-ctrl-agent without password, but it's not recommended and at the moment we don't support that.
+- I may provide some minimal support, if you ask nicely :)
+
+- Version: 1.0.0
+- Author: `void-spark`
+- Release Date: `11/05/2026`
diff --git a/front/plugins/kea_api/config.json b/front/plugins/kea_api/config.json
new file mode 100644
index 000000000..6bc00f3d8
--- /dev/null
+++ b/front/plugins/kea_api/config.json
@@ -0,0 +1,455 @@
+{
+ "code_name": "kea_api",
+ "unique_prefix": "KEALSS",
+ "plugin_type": "device_scanner",
+ "execution_order" : "Layer_3",
+ "enabled": true,
+ "data_source": "script",
+ "data_filters": [
+ {
+ "compare_column": "objectPrimaryId",
+ "compare_operator": "==",
+ "compare_field_id": "txtMacFilter",
+ "compare_js_template": "'{value}'.toString()",
+ "compare_use_quotes": true
+ }
+ ],
+ "show_ui": true,
+ "localized": ["display_name", "description", "icon"],
+ "mapped_to_table": "CurrentScan",
+ "display_name": [{"language_code": "en_us", "string": "Kea DHCP API"}],
+ "icon": [{"language_code": "en_us", "string": ""}],
+ "description": [{"language_code": "en_us", "string": "Imports leases via Kea Control Agent REST API"}],
+ "database_column_definitions": [
+ {
+ "column": "index",
+ "css_classes": "col-sm-2",
+ "show": true,
+ "type": "none",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [{"language_code": "en_us", "string": "Index"}]
+ },
+ {
+ "column": "objectPrimaryId",
+ "mapped_to_column": "scanMac",
+ "css_classes": "col-sm-2",
+ "show": true,
+ "type": "device_mac",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [{"language_code": "en_us", "string": "MAC address"}]
+ },
+ {
+ "column": "objectSecondaryId",
+ "mapped_to_column": "scanLastIP",
+ "css_classes": "col-sm-2",
+ "show": true,
+ "type": "device_ip",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [{"language_code": "en_us", "string": "IP" }]
+ },
+ {
+ "column": "dateTimeCreated",
+ "css_classes": "col-sm-2",
+ "show": true,
+ "type": "label",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [{"language_code": "en_us", "string": "Created"}]
+ },
+ {
+ "column": "dateTimeChanged",
+ "mapped_to_column": "scanLastConnection",
+ "css_classes": "col-sm-2",
+ "show": true,
+ "type": "label",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [{"language_code": "en_us", "string": "Changed"}]
+ },
+ {
+ "column": "watchedValue1",
+ "css_classes": "col-sm-2",
+ "show": true,
+ "type": "label",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [{"language_code": "en_us", "string": "Is active"}]
+ },
+ {
+ "column": "watchedValue2",
+ "mapped_to_column": "scanName",
+ "css_classes": "col-sm-2",
+ "show": true,
+ "type": "label",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [{"language_code": "en_us", "string": "Hostname"}]
+ },
+ {
+ "column": "watchedValue4",
+ "css_classes": "col-sm-2",
+ "show": true,
+ "type": "label",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [{"language_code": "en_us", "string": "State"}]
+ },
+ {
+ "column": "userData",
+ "css_classes": "col-sm-2",
+ "show": false,
+ "type": "textbox_save",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [{"language_code": "en_us", "string": "Comments"}]
+ },
+ {
+ "column": "Dummy",
+ "mapped_to_column": "scanSourcePlugin",
+ "mapped_to_column_data": {
+ "value": "KEALSS"
+ },
+ "css_classes": "col-sm-2",
+ "show": false,
+ "type": "label",
+ "default_value": "",
+ "options": [],
+ "localized": ["name"],
+ "name": [{"language_code": "en_us", "string": "Scan method"}]
+ },
+ {
+ "column": "status",
+ "css_classes": "col-sm-1",
+ "show": true,
+ "type": "replace",
+ "default_value": "",
+ "options": [
+ {
+ "equals": "watched-not-changed",
+ "replacement": "
Kea API. If you select schedule the scheduling settings from below are applied. If you select once the scan is run only once on start of the application (container) or after you update your settings. ⚠ Use the same schedule if you have multiple Device scanners enabled."
+ }
+ ]
+ },
+ {
+ "function": "CMD",
+ "type": {
+ "dataType": "string",
+ "elements": [
+ { "elementType": "input", "elementOptions": [], "transformers": [] }
+ ]
+ },
+ "default_value": "python3 /app/front/plugins/kea_api/script.py",
+ "options": [],
+ "localized": ["name", "description"],
+ "name": [{"language_code": "en_us", "string": "Command"}],
+ "description": [{"language_code": "en_us", "string": "Command to run"}]
+ },
+ {
+ "function": "URL",
+ "localized": ["name", "description"],
+ "name": [{"language_code": "en_us", "string": "API URL"}],
+ "description": [{"language_code": "en_us", "string": "Kea Control Agent URL"}],
+ "type": {
+ "dataType": "string",
+ "elements": [
+ {
+ "elementType": "input",
+ "elementOptions": [],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": "http://127.0.0.1:8000"
+ },
+ {
+ "function": "USER",
+ "localized": ["name", "description"],
+ "name": [{"language_code": "en_us", "string": "API User"}],
+ "description": [{"language_code": "en_us", "string": "Basic Auth Username"}],
+ "type": {
+ "dataType": "string",
+ "elements": [
+ {
+ "elementType": "input",
+ "elementOptions": [],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": "kea-api"
+ },
+ {
+ "function": "PASS",
+ "localized": ["name", "description"],
+ "name": [{"language_code": "en_us", "string": "API Password"}],
+ "description": [{"language_code": "en_us", "string": "Basic Auth Password"}],
+ "type": {
+ "dataType": "string",
+ "elements": [
+ {
+ "elementType": "input",
+ "elementOptions": [{"type": "password"}],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": ""
+ },
+ {
+ "function": "RUN_SCHD",
+ "type": {
+ "dataType": "string",
+ "elements": [
+ {
+ "elementType": "span",
+ "elementOptions": [
+ {
+ "cssClasses": "input-group-addon validityCheck"
+ },
+ {
+ "getStringKey": "Gen_ValidIcon"
+ }
+ ],
+ "transformers": []
+ },
+ {
+ "elementType": "input",
+ "elementOptions": [
+ {
+ "focusout": "validateRegex(this)"
+ },
+ {
+ "base64Regex": "Xig/OlwqfCg/OlswLTldfFsxLTVdWzAtOV18WzAtOV0rLVswLTldK3xcKi9bMC05XSspKVxzKyg/OlwqfCg/OlswLTldfDFbMC05XXwyWzAtM118WzAtOV0rLVswLTldK3xcKi9bMC05XSspKVxzKyg/OlwqfCg/OlsxLTldfFsxMl1bMC05XXwzWzAxXXxbMC05XSstWzAtOV0rfFwqL1swLTldKykpXHMrKD86XCp8KD86WzEtOV18MVswLTJdfFswLTldKy1bMC05XSt8XCovWzAtOV0rKSlccysoPzpcKnwoPzpbMC02XXxbMC02XS1bMC02XXxcKi9bMC05XSspKSQ="
+ }
+ ],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": "0 2 * * *",
+ "options": [],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Schedule"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "Only enabled if you select schedule in the KEALSS_RUN setting. Make sure you enter the schedule in the correct cron-like format (e.g. validate at crontab.guru). For example entering 0 4 * * * will run the scan after 4 am in the TIMEZONE you set above. Will be run NEXT time the time passes. Source = USER or Source = LOCKED)."
+ }
+ ]
+ },
+ {
+ "function": "SET_EMPTY",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": [],
+ "options": [
+ "devMac",
+ "devLastIP",
+ "devName",
+ "devSourcePlugin"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set empty columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are only overwritten if they are empty (NULL / empty string) or if their Source is set to NEWDEV"
+ }
+ ]
+ },
+ {
+ "function": "WATCH",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "orderable": "true"}],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": ["watchedValue1", "watchedValue4"],
+ "options": [
+ "watchedValue1",
+ "watchedValue2",
+ "watchedValue4"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Watched"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "Send a notification if selected values change. Use CTRL + Click to select/deselect. watchedValue1 is Active watchedValue2 is Hostname watchedValue4 is Statenew means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. watched-changed means that selected watchedValueN columns changed."
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/front/plugins/kea_api/script.py b/front/plugins/kea_api/script.py
new file mode 100644
index 000000000..9dc72d8ce
--- /dev/null
+++ b/front/plugins/kea_api/script.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+import os
+import sys
+import requests
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../server'))
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../plugins'))
+
+from plugin_helper import Plugin_Objects, mylog, handleEmpty, is_mac
+from helper import get_setting_value
+from const import logPath
+
+pluginName = 'KEALSS'
+LOG_PATH = logPath + '/plugins'
+LOG_FILE = os.path.join(LOG_PATH, f'script.{pluginName}.log')
+RESULT_FILE = os.path.join(LOG_PATH, f'last_result.{pluginName}.log')
+
+plugin_objects = Plugin_Objects(RESULT_FILE)
+
+
+def main():
+ try:
+ url = get_setting_value(f'{pluginName}_URL')
+ user = get_setting_value(f'{pluginName}_USER')
+ password = get_setting_value(f'{pluginName}_PASS')
+ timeout = get_setting_value(f'{pluginName}_RUN_TIMEOUT')
+
+ mylog('verbose', [f'[{pluginName}] Querying Kea API at {url}'])
+
+ payload = {'command': 'lease4-get-all', 'service': ['dhcp4']}
+
+ response = requests.post(url, json=payload, auth=(user, password), timeout=max(1, timeout - 1))
+ response.raise_for_status()
+ data = response.json()
+
+ count = 0
+ for entry in data:
+ text = entry.get('text', '[API provided no text]')
+ # Result: 0 (success), 1 (error), or 3 (empty).
+ if entry['result'] == 0:
+ leases = entry['arguments']['leases']
+ for lease in leases:
+ mac = lease['hw-address']
+ state = lease['state']
+ if is_mac(mac):
+ plugin_objects.add_object(
+ primaryId = mac,
+ secondaryId = lease['ip-address'],
+ # Active or not, similar to watched1 of DHCPLSS plugin
+ watched1 = state == 0,
+ watched2 = lease['hostname'],
+ watched3 = None,
+ # Default (or assigned) (0), declined (1), expired-reclaimed (2), released (3), and registered (4)).
+ watched4 = state,
+ extra = None,
+ foreignKey = mac
+ )
+ count += 1
+ plugin_objects.write_result_file()
+
+ mylog('verbose', [f'[{pluginName}] Kea API response: {text}'])
+ mylog('verbose', [f'[{pluginName}] Successfully imported {count} devices reported by Kea API'])
+ elif entry['result'] == 1:
+ mylog('none', [f'[{pluginName}] ⚠ ERROR: Kea API indicated error: {text}'])
+ elif entry['result'] == 3:
+ mylog('verbose', [f'[{pluginName}] Kea API indicates no entries found: {text}'])
+
+
+ except Exception as e:
+ mylog('none', [f'[{pluginName}] ⚠ ERROR: {str(e)}'])
+
+
+
+if __name__ == '__main__':
+ main()