diff --git a/server/plugins/dockerdisc/README.md b/server/plugins/dockerdisc/README.md new file mode 100644 index 00000000..de76b23c --- /dev/null +++ b/server/plugins/dockerdisc/README.md @@ -0,0 +1,186 @@ +## Overview + +`DOCKERDISC` enriches Docker **hosts** NetAlertX already knows about with +the list of containers running on them - image, Compose project/service, +network driver, and (for containers on a `macvlan`/`ipvlan` network) their +own MAC/IP. + +It does **not** discover devices. NetAlertX's own ARP/Nmap scanners remain +the only source of device presence. `DOCKERDISC` never creates a device row +- not for a container, and not for the Docker host itself, which must +already exist in NetAlertX before this plugin can attach anything to it. + +Maintainer's mental model for this plugin: **Device = Docker host → List of +containers.** Every container found on a host shows up under that **host's +own** Device Details → Plugins → DOCKERDISC tab, not as a device of its +own. + +> [!TIP] +> Connects via a read-only [Docker Socket +> Proxy](https://github.com/Tecnativa/docker-socket-proxy) (e.g. +> `tecnativa/docker-socket-proxy`) - never mounts `/var/run/docker.sock` +> directly into the NetAlertX container. + +### Why a Socket Proxy, and not `docker.sock` directly? + +Mounting `/var/run/docker.sock` into a container gives that container the +same power as root on the host: anything that can reach the socket can, +for example, start a new `--privileged` container with the host +filesystem bind-mounted in - a standard, well-known way to escalate from +"container access" to "host root." It can't be scoped down to "read-only" +or "just these endpoints" - it's all or nothing. + +That's a much bigger risk to accept for NetAlertX specifically than for a +small single-purpose tool: NetAlertX is a web UI, a GraphQL API, and +dozens of other plugins pulling in data from routers, DHCP leases, and +other external sources - a large attack surface. A vulnerability anywhere +in any of that would inherit full `docker.sock` access too, even though +this plugin itself only ever needs to read three things: the container +list, host info, and the network list. + +The Socket Proxy sits between NetAlertX and the real socket and only +forwards the specific API paths this plugin actually needs +(`CONTAINERS=1`, `INFO=1`, `NETWORKS=1`) - everything else (exec, image +builds, volumes, secrets, any `POST` that creates/kills something) is +rejected by default. If NetAlertX is ever compromised, the blast radius +stops at "can list containers/networks," not "can root the host." + +### Socket Proxy compose service + +Add this as another service in the **same `docker-compose.yml` as +NetAlertX itself** - not a separate stack/file: + +```yaml +services: + netalertx: + container_name: netalertx + image: "ghcr.io/jokob-sk/netalertx" + ... # same as you already have + ... + + docker-socket-proxy: + image: tecnativa/docker-socket-proxy:latest + container_name: docker-socket-proxy + environment: + CONTAINERS: 1 + INFO: 1 + NETWORKS: 1 + POST: 0 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + restart: unless-stopped + # Only if netalertx uses network_mode: host - see note below + # ports: + # - "127.0.0.1:2375:2375" +``` + +> [!NOTE] +> If your `netalertx` service uses `network_mode: host` (common, since ARP +> scanning needs a real host NIC), it won't resolve `docker-socket-proxy` +> by name - host networking means it isn't on the compose network at all. +> Fix: add `ports: ["127.0.0.1:2375:2375"]` to the proxy service above, and +> use `http://127.0.0.1:2375` as the Socket Proxy URL instead. Don't give +> the proxy `network_mode: host` too - that likely exposes it to the whole +> LAN instead of just the NAS. + +Same file, on purpose: + +- Compose puts every service in one file on the same default network + automatically, so NetAlertX can reach it at `http://127.0.0.1:2375` or + `http://docker-socket-proxy:2375` for free - no extra `networks:` config, + no port published to the LAN (nothing else needs to reach it). +- Its lifecycle naturally follows NetAlertX's - one `docker compose up`/ + `down` brings both up or down together, instead of a second stack to + remember to manage separately. +- It's a dependency this plugin needs, not an unrelated service, so it + belongs with NetAlertX conceptually as well as operationally. + +(The only real reason to split it into its own stack is sharing one proxy +across several unrelated projects - e.g. Watchtower and NetAlertX both +reading from the same proxy instead of each running their own. Not needed +here.) + +`POST: 0` is already the image's default; it's listed explicitly since +it's the setting that keeps this read-only - nothing here can +create/start/stop/kill anything. + +### Quick setup guide + +1. Add the Socket Proxy service above to NetAlertX's `docker-compose.yml` + and bring it up (`docker compose up -d docker-socket-proxy`) - one per + Docker host you want tracked, if you're tracking more than one. +2. In NetAlertX, add one entry per Docker host under **Docker hosts** + (`DOCKERDISC_hosts`) with that proxy's URL (`http://127.0.0.1:2375`, + `http://docker-socket-proxy:2375`, or whatever you named the service). +3. Make sure the Docker host itself already exists as a device in + NetAlertX (it normally does, found via ARP/Nmap - Docker hosts have a + real NIC on the LAN). If host-MAC auto-detection doesn't find it (see + below), fill in its MAC manually in the same entry. + +#### Required Settings + +- When to run `DOCKERDISC_RUN` +- Docker hosts `DOCKERDISC_hosts` - at least one entry, each with: + - Docker Socket Proxy URL `DOCKERDISC_SOCKET_PROXY_URL` + - Docker Host MAC Address (Fallback) `DOCKERDISC_HOST_MAC` - optional if + auto-detection works for that host + +### Host MAC auto-detection + +If `DOCKERDISC_HOST_MAC` is filled in, it's used immediately - no Socket +Proxy call at all. Deliberate trade-off: a MAC is stable, so there's +nothing to gain by re-confirming it via `/info` on every scheduled run, +but it also means a future MAC change (e.g. a replaced NIC) won't be +auto-detected while the field stays set. + +Otherwise, the plugin calls the Socket Proxy's `GET /info` (Docker Engine +API) to read the daemon's hostname, then looks for a NetAlertX device +whose `devName` matches it. If either step fails - `/info` isn't reachable +(check the `INFO=1` permission), or no device's name matches - that host's +entry is skipped for the run (logged, not fatal to other hosts). + +### Container listing + +Every container on a host is listed, including `bridge`/overlay ones - not +only `macvlan`/`ipvlan` containers. What changes per container is only +whether it has a real LAN-visible identity to show: + +- **Network Driver** (`watchedValue3`) is always populated. +- **Container MAC** (`watchedValue4`) and **IP** (`extra`) are populated + only when the container has a `macvlan`/`ipvlan` network attached; + otherwise they show `null`. A `bridge`-only container's own IP/MAC isn't + LAN-visible, so there's nothing meaningful to show there - it still gets + a row (image, Compose project/service, driver). +- If a container is attached to **more than one** `macvlan`/`ipvlan` + network at the same time (uncommon, but possible - e.g. a dual-homed + network appliance), the network whose *name* sorts first alphabetically + is the one shown. This is a deliberate, deterministic tie-break, not an + attempt to pick the "right" one - Docker doesn't expose any ordering or + priority between a container's networks, so any rule here is arbitrary; + what matters is that it's stable (the same container always reports the + same MAC/IP) rather than depending on whatever order the Socket Proxy's + JSON happens to return them in. + +### Usage + +- Head to **Settings** → **Docker discovery** to configure Docker hosts. +- Container details appear under each host's own **Device Details** → + **Plugins** → **DOCKERDISC** tab. + +### Notes + +- This plugin never writes to `devMac`, `devLastIP`, `devFirstConnection`, + `devSourcePlugin`, or `devCustomProps` - ARP/Nmap remain authoritative + for device identity and discovery-source attribution on every device, + including the Docker host itself. +- Only Socket Proxy permissions required: `CONTAINERS=1` (list containers, + their networks and labels), `INFO=1` (host-MAC auto-detection), and + `NETWORKS=1` (network driver lookup - one batched `GET /networks` call + per run for every unique network id seen, not one call per container). + No write/exec permissions needed. +- Design history and open implementation questions in [issue #1721] + (https://github.com/netalertx/NetAlertX/issues/1721). + +- Version: 0.1.0 +- Author: [mauricio-camayo](https://github.com/mauricio-camayo/) +- Release Date: `2026-09-14` diff --git a/server/plugins/dockerdisc/config.json b/server/plugins/dockerdisc/config.json new file mode 100644 index 00000000..02a37543 --- /dev/null +++ b/server/plugins/dockerdisc/config.json @@ -0,0 +1,806 @@ +{ + "code_name": "dockerdisc", + "unique_prefix": "DOCKERDISC", + "plugin_type": "other", + "enabled": true, + "data_source": "script", + "show_ui": true, + "localized": [ + "display_name", + "description", + "icon" + ], + "display_name": [ + { + "language_code": "en_us", + "string": "Docker discovery" + }, + { + "language_code": "es_es", + "string": "Descubrimiento de Docker" + }, + { + "language_code": "de_de", + "string": "Docker-Erkennung" + } + ], + "icon": [ + { + "language_code": "en_us", + "string": "" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Enriches known Docker hosts with their running containers - image, Compose project/service, network, and MAC/IP when available. Never creates devices; connects via a read-only Docker Socket Proxy." + }, + { + "language_code": "es_es", + "string": "Enriquece los hosts Docker que NetAlertX ya conoce con la lista de contenedores que corren en ellos - imagen, proyecto/servicio de Compose, driver de red y (cuando el contenedor tiene uno) su propio MAC/IP. Nunca crea devices; se conecta vía un Docker Socket Proxy de solo lectura, nunca directo a `/var/run/docker.sock`." + } + ], + "params": [], + "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": "plugin", + "css_classes": "col-sm-2", + "show": false, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "N/A" + } + ] + }, + { + "column": "objectPrimaryId", + "css_classes": "col-sm-2", + "show": true, + "type": "device_mac", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Container host" + } + ] + }, + { + "column": "objectSecondaryId", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Container" + } + ] + }, + { + "column": "dateTimeCreated", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "First seen" + } + ] + }, + { + "column": "dateTimeChanged", + "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": "Image" + } + ] + }, + { + "column": "watchedValue2", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Compose Project / Service" + } + ] + }, + { + "column": "watchedValue3", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Network Driver" + } + ] + }, + { + "column": "watchedValue4", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Container MAC" + } + ] + }, + { + "column": "extra", + "css_classes": "col-sm-3", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "IP" + } + ] + }, + { + "column": "userData", + "css_classes": "col-sm-2", + "show": false, + "type": "textbox_save", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Comments" + }, + { + "language_code": "es_es", + "string": "Comentarios" + }, + { + "language_code": "de_de", + "string": "Kommentare" + } + ] + }, + { + "column": "status", + "css_classes": "col-sm-1", + "show": false, + "type": "replace", + "default_value": "", + "options": [ + { + "equals": "watched-not-changed", + "replacement": "
schedule the scheduling settings from below are applied. If you select once the scan is run only once on start of the application (container) for the time specified in DOCKERDISC_RUN_TIMEOUT setting."
+ },
+ {
+ "language_code": "es_es",
+ "string": "Habilita una ejecución periódica de descubrimiento de Docker. Si selecciona schedule se aplican las opciones de programación de abajo. Si selecciona once el escaneo se ejecuta solo una vez al iniciar la aplicación (contenedor) durante el tiempo especificado en la configuración DOCKERDISC_RUN_TIMEOUT."
+ }
+ ]
+ },
+ {
+ "function": "CMD",
+ "type": {
+ "dataType": "string",
+ "elements": [
+ {
+ "elementType": "input",
+ "elementOptions": [
+ {
+ "readonly": "true"
+ }
+ ],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": "python3 /app/server/plugins/dockerdisc/script.py",
+ "options": [],
+ "localized": [
+ "name",
+ "description"
+ ],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Command"
+ },
+ {
+ "language_code": "es_es",
+ "string": "Comando"
+ },
+ {
+ "language_code": "de_de",
+ "string": "Befehl"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "Command to run"
+ },
+ {
+ "language_code": "es_es",
+ "string": "Comando a ejecutar"
+ },
+ {
+ "language_code": "de_de",
+ "string": "Auszuführender Befehl"
+ }
+ ]
+ },
+ {
+ "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/OlswLTldfFsxLTVdWzAtOV18WzAtOV0rLVswLTldKyg/Oi9bMC05XSspP3xcKi9bMC05XSspKSg/OiwoPzpbMC05XXxbMS01XVswLTldfFswLTldKy1bMC05XSsoPzovWzAtOV0rKT98XCovWzAtOV0rKSkqXHMrKD86XCp8KD86WzAtOV18MVswLTldfDJbMC0zXXxbMC05XSstWzAtOV0rKD86L1swLTldKyk/fFwqL1swLTldKykpKD86LCg/OlswLTldfDFbMC05XXwyWzAtM118WzAtOV0rLVswLTldKyg/Oi9bMC05XSspP3xcKi9bMC05XSspKSpccysoPzpcKnwoPzpbMS05XXxbMTJdWzAtOV18M1swMV18WzAtOV0rLVswLTldKyg/Oi9bMC05XSspP3xcKi9bMC05XSspKSg/OiwoPzpbMS05XXxbMTJdWzAtOV18M1swMV18WzAtOV0rLVswLTldKyg/Oi9bMC05XSspP3xcKi9bMC05XSspKSpccysoPzpcKnwoPzpbMS05XXwxWzAtMl18WzAtOV0rLVswLTldKyg/Oi9bMC05XSspP3xcKi9bMC05XSspKSg/OiwoPzpbMS05XXwxWzAtMl18WzAtOV0rLVswLTldKyg/Oi9bMC05XSspP3xcKi9bMC05XSspKSpccysoPzpcKnwoPzpbMC02XXxbMC02XS1bMC02XSg/Oi9bMC05XSspP3xcKi9bMC05XSspKSg/OiwoPzpbMC02XXxbMC02XS1bMC02XSg/Oi9bMC05XSspP3xcKi9bMC05XSspKSok"
+ }
+ ],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": "*/5 * * * *",
+ "options": [],
+ "localized": [
+ "name",
+ "description"
+ ],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Schedule"
+ },
+ {
+ "language_code": "es_es",
+ "string": "Schedule"
+ },
+ {
+ "language_code": "de_de",
+ "string": "Zeitplan"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "Only enabled if you select schedule in the DOCKERDISC_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."
+ },
+ {
+ "language_code": "es_es",
+ "string": "Solo habilitado si selecciona schedule en la configuración DOCKERDISC_RUN. Asegúrese de ingresar el schedule en el formato similar a cron correcto (por ejemplo, valide en crontab.guru). Por ejemplo, ingrese 0 4 * * * ejecutará el escaneo después de las 4 am en el TIMEZONE que configuró arriba. Se ejecutará la PRÓXIMA vez que pase el tiempo."
+ }
+ ]
+ },
+ {
+ "function": "RUN_TIMEOUT",
+ "type": {
+ "dataType": "integer",
+ "elements": [
+ {
+ "elementType": "input",
+ "elementOptions": [
+ {
+ "type": "number"
+ }
+ ],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": 60,
+ "options": [],
+ "localized": [
+ "name",
+ "description"
+ ],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Run timeout"
+ },
+ {
+ "language_code": "es_es",
+ "string": "Tiempo de espera de ejecución"
+ },
+ {
+ "language_code": "de_de",
+ "string": "Zeitlimit"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
+ },
+ {
+ "language_code": "es_es",
+ "string": "Tiempo máximo en segundos para esperar a que finalice el script. Si se supera este tiempo, el script se cancela."
+ }
+ ]
+ },
+ {
+ "function": "hosts",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "button",
+ "elementOptions": [
+ {
+ "sourceSuffixes": []
+ },
+ {
+ "separator": ""
+ },
+ {
+ "cssClasses": "col-xs-12"
+ },
+ {
+ "onClick": "addViaPopupForm(this)"
+ },
+ {
+ "getStringKey": "Gen_Add"
+ }
+ ],
+ "transformers": []
+ },
+ {
+ "elementType": "select",
+ "elementHasInputValue": 1,
+ "elementOptions": [
+ {
+ "multiple": "true"
+ },
+ {
+ "readonly": "true"
+ },
+ {
+ "editable": "true"
+ },
+ {
+ "popupForm": [
+ {
+ "function": "DOCKERDISC_SOCKET_PROXY_URL",
+ "type": {
+ "dataType": "string",
+ "elements": [
+ {
+ "elementType": "input",
+ "elementOptions": [
+ {
+ "placeholder": "http://docker-socket-proxy:2375"
+ },
+ {
+ "cssClasses": "col-sm-10"
+ }
+ ],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": "http://docker-socket-proxy:2375",
+ "options": [],
+ "localized": [
+ "name",
+ "description"
+ ],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Docker Socket Proxy URL"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "Base URL for the read-only Docker Socket Proxy endpoint for this Docker host (requires the CONTAINERS, INFO, and NETWORKS permissions)."
+ }
+ ]
+ },
+ {
+ "function": "DOCKERDISC_HOST_MAC",
+ "type": {
+ "dataType": "string",
+ "elements": [
+ {
+ "elementType": "input",
+ "elementOptions": [
+ {
+ "placeholder": "aa:bb:cc:dd:ee:ff"
+ },
+ {
+ "cssClasses": "col-sm-10"
+ }
+ ],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": "",
+ "options": [],
+ "localized": [
+ "name",
+ "description"
+ ],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Docker Host MAC Address (Fallback)"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "Manual fallback physical MAC address of the Docker host, used if auto-detecting it via the Socket Proxy /info endpoint fails. The host must already exist as a device in NetAlertX (found via ARP/Nmap) - this plugin never creates it."
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "transformers": [
+ "name|base64"
+ ]
+ },
+ {
+ "elementType": "button",
+ "elementOptions": [
+ {
+ "sourceSuffixes": []
+ },
+ {
+ "separator": ""
+ },
+ {
+ "cssClasses": "col-xs-6"
+ },
+ {
+ "onClick": "removeFromList(this)"
+ },
+ {
+ "getStringKey": "Gen_Remove_Last"
+ }
+ ],
+ "transformers": []
+ },
+ {
+ "elementType": "button",
+ "elementOptions": [
+ {
+ "sourceSuffixes": []
+ },
+ {
+ "separator": ""
+ },
+ {
+ "cssClasses": "col-xs-6"
+ },
+ {
+ "onClick": "removeAllOptions(this)"
+ },
+ {
+ "getStringKey": "Gen_Remove_All"
+ }
+ ],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": [],
+ "options": [],
+ "localized": [
+ "name",
+ "description"
+ ],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Docker hosts"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "One entry per Docker host to track. Each entry pairs a read-only Docker Socket Proxy URL with that host's device (auto-detected, or entered manually as a fallback). Every container found on a host is listed under that host's own Device Details → Plugins → DOCKERDISC tab - the host device must already exist in NetAlertX (via ARP/Nmap); this plugin never creates devices."
+ }
+ ]
+ },
+ {
+ "function": "WATCH",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [
+ {
+ "multiple": "true",
+ "orderable": "true"
+ }
+ ],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": [],
+ "options": [
+ "watchedValue1",
+ "watchedValue2",
+ "watchedValue3",
+ "watchedValue4"
+ ],
+ "localized": [
+ "name",
+ "description"
+ ],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Watched"
+ },
+ {
+ "language_code": "es_es",
+ "string": "Visto"
+ },
+ {
+ "language_code": "de_de",
+ "string": "Überwacht"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "Send a notification if selected values change. Use CTRL + Click to select/deselect. watchedValue1 is the container imagewatchedValue2 is the Compose project/servicewatchedValue3 is the network driverwatchedValue4 is the container's own MAC, when it has oneCTRL + Clic para seleccionar/deseleccionar. watchedValue1 es la imagen del contenedorwatchedValue2 es el proyecto/servicio de ComposewatchedValue3 es el driver de redwatchedValue4 es el MAC propio del contenedor, cuando tiene unonew means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. watched-changed means that selected watchedValueN columns changed."
+ },
+ {
+ "language_code": "es_es",
+ "string": "Envíe una notificación solo en estos estados. new significa que se descubrió un nuevo objeto único (combinación única de PrimaryId y SecondaryId). watched-changed significa que las columnas watchedValueN seleccionadas cambiaron."
+ }
+ ]
+ }
+ ]
+}
diff --git a/server/plugins/dockerdisc/script.py b/server/plugins/dockerdisc/script.py
new file mode 100644
index 00000000..cf02bdd9
--- /dev/null
+++ b/server/plugins/dockerdisc/script.py
@@ -0,0 +1,338 @@
+#!/usr/bin/env python
+"""NetAlertX plugin: DOCKERDISC - Docker discovery (enrichment, not import)
+
+Does NOT discover devices. NetAlertX's own ARP/Nmap scanners remain the
+sole source of device presence. Instead, for each configured Docker host
+this plugin lists that host's containers under the *host's own* Device
+Details -> Plugins -> DOCKERDISC tab.
+
+Design ("Device = Docker host -> List of containers", per maintainer
+jokob-sk, see ../../../PLUGIN_DOCKERDISC_SPEC.md for the full history):
+
+ - objectPrimaryId / foreignKey is always the Docker HOST's MAC - never a
+ container's own MAC. Every plugin object (one per container) attaches
+ to the host device, which must already exist in NetAlertX (found the
+ normal way, via ARP/Nmap). This plugin never creates a device row, for
+ either a host or a container.
+ - Because matching targets the host (persistent LAN identity), not the
+ container, EVERY container is listed - bridge/overlay ones included -
+ not only macvlan/ipvlan ones. A container only gets its own MAC/IP
+ shown (watched4/extra) when it has a macvlan/ipvlan network; otherwise
+ those fields are "null".
+ - One `hosts` entry = one Docker host: a read-only Docker Socket Proxy
+ URL, plus a manual MAC fallback for when auto-detection (via the
+ proxy's own /info endpoint) doesn't resolve to a known device. Never
+ connects to /var/run/docker.sock directly.
+
+Verified 2026-09-08 against a real Docker Engine + docker-socket-proxy
+(see PLUGIN_DOCKERDISC_SPEC.md §9 for the open questions this closed):
+`GET /containers/json`'s `NetworkSettings.Networks.