Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 42 additions & 12 deletions server/plugins/dockerdisc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@ 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.
It never creates the Docker **host** itself as a device - that must already
exist in NetAlertX before this plugin can attach anything to it. A
**container**, on the other hand, can optionally get its own device too:
opt in with `DOCKERDISC_CREATE_DEV` and a container on a `macvlan`/`ipvlan`
network (its own LAN-visible MAC) can create or confirm a device of its
own, parented to its host. A container without its own MAC
(`bridge`/overlay/etc.) never creates one, regardless of that setting -
there's no LAN-visible identity to create a device from.

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.
own** Device Details → Plugins → DOCKERDISC tab either way - opting a
container into its own device is additive, it doesn't remove it from the
host's list.

> [!TIP]
> Connects via a read-only [Docker Socket
Expand Down Expand Up @@ -128,6 +133,20 @@ create/start/stop/kill anything.
- Docker Host MAC Address (Fallback) `DOCKERDISC_HOST_MAC` - optional if
auto-detection works for that host

#### Optional Settings

- Create/confirm devices for containers with a real MAC
`DOCKERDISC_CREATE_DEV` - off by default. A container without its own
MAC (`bridge`/overlay/etc.) never creates a device either way; this only
affects containers on a `macvlan`/`ipvlan` network.
- Allow updating existing devices from this plugin's data
`DOCKERDISC_IMPORT_ON` - on by default. Turn off to make a run purely
informational: no `CurrentScan` promotion at all, so a container's own
already-existing device (found independently by ARP/Nmap) won't get its
presence/IP/parent fields updated from this plugin either, regardless of
`DOCKERDISC_CREATE_DEV`. The two settings are independent - one doesn't
gate the other.

### Host MAC auto-detection

If `DOCKERDISC_HOST_MAC` is filled in, it's used immediately - no Socket
Expand Down Expand Up @@ -172,10 +191,21 @@ whether it has a real LAN-visible identity to show:

### 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.
- The Docker host's own `devMac`/`devLastIP`/`devFirstConnection`/
`devSourcePlugin`/`devCustomProps` are never touched - ARP/Nmap remain
authoritative for the host's identity and discovery-source attribution.
A container with its own macvlan/ipvlan MAC always maps to `CurrentScan`
(`DOCKERDISC_IMPORT_ON` permitting) - with `DOCKERDISC_CREATE_DEV` on,
it can originate a brand-new device (`devSourcePlugin` set to
`DOCKERDISC` at creation, like any other `CurrentScan`-mapped plugin);
either way, if that MAC is already a device (found independently by
ARP/Nmap, since it's LAN-visible), this plugin's row still confirms its
presence and updates its `devLastIP`/`devParentMAC` on every run - that
existing device's own `devSourcePlugin` isn't touched, since it's only
set once, at creation. Turn `DOCKERDISC_IMPORT_ON` off to skip all of
that and keep this plugin purely informational (its `Plugins_Objects`
listing still updates either way) - see [Plugin Import
Behavior](../../../docs/PLUGINS_IMPORT_BEHAVIOR.md).
- 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
Expand All @@ -184,6 +214,6 @@ whether it has a real LAN-visible identity to show:
- Design history and open implementation questions in [issue #1721]
(https://github.com/netalertx/NetAlertX/issues/1721).

- Version: 0.1.0
- Version: 0.2.0
- Author: [mauricio-camayo](https://github.com/mauricio-camayo/)
- Release Date: `2026-09-14`
- Release Date: `2026-09-18`
132 changes: 131 additions & 1 deletion server/plugins/dockerdisc/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"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."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shorten description - implementation and config details should be in readme - this string is shown in the UI

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shortened to one line in b175a3b - moved the implementation detail to README.

"string": "Enriches known Docker hosts with their running containers."
}
],
"params": [
Expand All @@ -37,6 +37,7 @@
"timeoutMultiplier": true
}
],
"mapped_to_table": "CurrentScan",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"database_column_definitions": [
{
"column": "index",
Expand Down Expand Up @@ -74,6 +75,7 @@
},
{
"column": "objectPrimaryId",
"mapped_to_column": "scanParentMAC",
"css_classes": "col-sm-2",
"show": true,
"type": "device_mac",
Expand Down Expand Up @@ -210,6 +212,7 @@
},
{
"column": "extra",
"mapped_to_column": "scanLastIP",
"css_classes": "col-sm-3",
"show": true,
"type": "label",
Expand All @@ -225,6 +228,63 @@
}
]
},
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also map scanSourcePlugin, unlike every other CurrentScan-mapped plugin, - currently every row it inserts gets scanSourcePlugin = NULL.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added - same Dummy-column static-value pattern arp_scan already uses, in b175a3b.

"column": "helpVal1",
"mapped_to_column": "scanMac",
"css_classes": "col-sm-2",
"show": false,
"type": "none",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "N/A"
}
]
},
{
"column": "helpVal2",
"mapped_to_column": "scanCreatesDevice",
"css_classes": "col-sm-2",
"show": false,
"type": "none",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "N/A"
}
]
},
{
"column": "Dummy",
"mapped_to_column": "scanSourcePlugin",
"mapped_to_column_data": {
"value": "DOCKERDISC"
},
"css_classes": "col-sm-2",
"show": false,
"type": "none",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "N/A"
}
]
},
{
"column": "userData",
"css_classes": "col-sm-2",
Expand Down Expand Up @@ -317,6 +377,41 @@
}
]
},
{
"function": "IMPORT_ON",
"type": {
"dataType": "boolean",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"type": "checkbox"
}
],
"transformers": []
}
]
},
"default_value": true,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Allow updating existing devices from this plugin's data"
}
],
"description": [
{
"language_code": "en_us",
"string": "On by default. Turn off to make this run purely informational - no CurrentScan promotion at all, so a container's own already-existing device (found independently by ARP/Nmap) won't get its presence/IP/parent updated from this plugin either, regardless of <a href=\"#DOCKERDISC_CREATE_DEV\"><code>DOCKERDISC_CREATE_DEV</code></a>."
}
]
},
{
"function": "CMD",
"type": {
Expand Down Expand Up @@ -624,6 +719,41 @@
}
]
},
{
"function": "CREATE_DEV",
"type": {
"dataType": "boolean",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"type": "checkbox"
}
],
"transformers": []
}
]
},
"default_value": false,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Create/confirm devices for containers with a real MAC"
}
],
"description": [
{
"language_code": "en_us",
"string": "When on, a container on a macvlan/ipvlan network (its own LAN-visible MAC) can create or confirm its own device entry, parented to its Docker host. Containers without their own MAC (bridge/overlay/etc.) never create a device either way."
}
]
},
{
"function": "WATCH",
"type": {
Expand Down
52 changes: 42 additions & 10 deletions server/plugins/dockerdisc/script.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
#!/usr/bin/env python
"""NetAlertX plugin: DOCKERDISC - Docker discovery (enrichment, not import)
"""NetAlertX plugin: DOCKERDISC - Docker discovery

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.
For each configured Docker host, lists that host's containers under the
*host's own* Device Details -> Plugins -> DOCKERDISC tab. The host itself
is never created by this plugin - it must already exist in NetAlertX
(found the normal way, via ARP/Nmap).

- 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.
to the host device.
- 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".
- Also maps to CurrentScan (scanMac/scanCreatesDevice/scanParentMAC/
scanLastIP - see docs/PLUGINS_IMPORT_BEHAVIOR.md), gated by
DOCKERDISC_IMPORT_ON (whether this run promotes to CurrentScan at all)
and, independently, DOCKERDISC_CREATE_DEV (whether a container with
its own MAC may originate a brand-new device via scanCreatesDevice -
neither setting gates the other). A container without its own MAC
(bridge/overlay/etc.) always gets a blank scanMac, which blocks device
creation for the whole group regardless of scanCreatesDevice - it can
never be its own device. One with a real MAC is parented to its host
via scanParentMAC on every promoted run, whether or not CREATE_DEV
lets it also originate a device.
- 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
Expand Down Expand Up @@ -263,7 +272,13 @@ def first_network_driver(networks, driver_by_id):
return None


def process_host(host_entry, deadline, plugin_objects):
def process_host(host_entry, deadline, plugin_objects, create_dev):
"""Lists one Docker host's containers as plugin objects under that
host's Device Details tab, and maps each to a CurrentScan row. Skips
the whole host (no containers listed) if its Socket Proxy URL is
missing, its MAC can't be resolved, or that MAC isn't a known device.
Returns the number of containers reported."""

host = DockerHost(
proxy_url=host_entry.get('DOCKERDISC_SOCKET_PROXY_URL'),
manual_mac=host_entry.get('DOCKERDISC_HOST_MAC'),
Expand Down Expand Up @@ -317,6 +332,16 @@ def process_host(host_entry, deadline, plugin_objects):
names = container.get('Names') or []
container_name = names[0].lstrip('/') if names else container.get('Id', '')[:12]

# scanMac/scanCreatesDevice (helpVal1/helpVal2, mapped in config.json)
# drive whether this row can promote to its own CurrentScan/Devices
# entry - see docs/PLUGINS_IMPORT_BEHAVIOR.md. A container without
# its own LAN-visible MAC (bridge/overlay/etc.) always gets a blank
# scanMac, which blocks device creation for the whole group
# regardless of scanCreatesDevice - it can never be its own device.
# One with a real MAC only creates/confirms a device when the user
# opted in via DOCKERDISC_CREATE_DEV.
can_create_device = bool(container_mac) and create_dev

plugin_objects.add_object(
primaryId=host_mac,
secondaryId=handleEmpty(container_name),
Expand All @@ -326,16 +351,23 @@ def process_host(host_entry, deadline, plugin_objects):
watched4=handleEmpty(container_mac),
extra=handleEmpty(container_ip),
foreignKey=host_mac,
helpVal1=container_mac,
helpVal2='1' if can_create_device else '0',
)
added += 1

return added


def main():
"""Entry point: reads the configured Docker hosts and DOCKERDISC_CREATE_DEV,
processes each host in turn against a shared per-run request-time
budget, and writes the combined result file."""

mylog('verbose', [f'[{pluginName}] In script'])

host_configs = get_setting_value('DOCKERDISC_hosts') or []
create_dev = bool(get_setting_value('DOCKERDISC_CREATE_DEV'))
run_timeout = get_setting_value('DOCKERDISC_RUN_TIMEOUT') or REQUEST_TIMEOUT_DEFAULT
# One shared deadline for the whole run (every host, every request) -
# config.json's "hosts" param has timeoutMultiplier set, so the outer
Expand All @@ -351,7 +383,7 @@ def main():
total_added = 0
for host_config in host_configs:
host_entry = decode_settings_base64(host_config)
total_added += process_host(host_entry, deadline, plugin_objects)
total_added += process_host(host_entry, deadline, plugin_objects, create_dev)

plugin_objects.write_result_file()

Expand Down
Loading
Loading