You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by cd...@apache.org on 2026/08/21 18:39:17 UTC
(plc4x) branch chore/update-threat-model-to-only-include-supported-drivers created (now 07019ba7eb)
This is an automated email from the ASF dual-hosted git repository.
chrisdutz pushed a change to branch chore/update-threat-model-to-only-include-supported-drivers
in repository https://gitbox.apache.org/repos/asf/plc4x.git
at 07019ba7eb chore: Updated the THREAT-MODEL.md to exclude not-supported drivers.
This branch includes the following new commits:
new 07019ba7eb chore: Updated the THREAT-MODEL.md to exclude not-supported drivers.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(plc4x) 01/01: chore: Updated the THREAT-MODEL.md to exclude not-supported drivers.
Posted by cd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
chrisdutz pushed a commit to branch chore/update-threat-model-to-only-include-supported-drivers
in repository https://gitbox.apache.org/repos/asf/plc4x.git
commit 07019ba7ebb14b5d4c6b38019df038d8b07143e5
Author: Christofer Dutz <ch...@c-ware.de>
AuthorDate: Fri Aug 21 20:39:03 2026 +0200
chore: Updated the THREAT-MODEL.md to exclude not-supported drivers.
---
THREAT-MODEL.md | 198 ++++++++++++++++----
plc4j/drivers/all/pom.xml | 36 ++--
.../modules/users/pages/protocols/index.adoc | 206 ++++++++++++++++-----
3 files changed, 333 insertions(+), 107 deletions(-)
diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md
index 5146e94bdf..6bb3a349a4 100644
--- a/THREAT-MODEL.md
+++ b/THREAT-MODEL.md
@@ -10,6 +10,10 @@
- **Scope of this model**: the **main `apache/plc4x` repository only** —
driver layer, SPI, transports, code-generation runtime, and the four
language bindings shipped here (`plc4j`, `plc4go`, `plc4c`, `plc4py`).
+ Which **drivers** are covered is not listed here: this model delegates
+ that to the per-language driver matrix in
+ `website/.../protocols/index.adoc` — a green check means in scope,
+ anything else means out. See §3 item 6 for the rule.
The separately-released `plc4x-extras` repository (OPC-UA Server, PLC4X
Server, Calcite / Camel / Kafka-Connect / Karaf / NiFi integrations,
Connection-Cache, OPM, Scraper) is **out of scope** of this document
@@ -115,23 +119,24 @@ The main repo carves naturally into nine families with distinct threat
profiles. Anything marked **out** below reappears in §3 with the
reason.
-| Family | Representative entry point | Touches outside the process? | In this model? |
-|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|-------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|
-| **Java public API (`plc4j/api`)** | `PlcDriverManager.getConnectionFactory().getConnection(url, auth)` | no — pure dispatch | **yes** |
-| **Java SPI / runtime (`plc4j/spi`)** | `AbstractPlcConnection`, `ConversationContext`, `Plc4xProtocolBase` | no — orchestration | **yes** |
-| **Per-protocol drivers (`plc4j/drivers/{s7,modbus,opcua,ads,knxnetip,bacnet,c-bus,can,canopen,ctrlx,eip,firmata,iec-60870,logix,open-protocol,plc4x,profinet,profinet-ng,umas}`)** | one `PlcDriver` per protocol | **yes — network**, via the registered transport | **yes** |
-| **Transports (`plc4j/transports/{tcp,udp,serial,raw-socket,pcap-replay,pcap-shared,socketcan,can,virtualcan,test}`)** | Netty socket / serial / pcap | **yes — sockets, serial devices, libpcap** | **yes** |
-| **OPC UA security subsystem** (`plc4j/drivers/opcua/.../security/`, `.../context/SecureChannel`) | certificate verifier, security-policy negotiator, secure-channel encoder | **yes — TLS-like primitives on the wire** | **yes — distinguished from other drivers; see §8 P1** |
-| **Code-generation runtime (mspec read/write generators)** under `plc4j/.../readwrite/`, `plc4go/protocols/`, `plc4c/generated-sources/` | generated parsers / serializers | **yes — driven by remote-PLC bytes** | **yes** — same trust level as the driver that uses it |
-| **Go bindings (`plc4go/`)** | `plc4go.NewPlcDriverManager()` | as core | **yes** |
-| **C bindings (`plc4c/`)** | `plc4c_*` API | as core | **yes — but README declares "not ready for usage"** *(documented: `README.md`)* — see §3 |
-| **Python bindings (`plc4py/`)** | `plc4py` import | as core | **yes — but README declares "not ready for usage"** *(documented: `README.md`)* — see §3 |
-| **.NET bindings (`plc4net/`)** | n/a | n/a | **out** — README declares "not ready for usage - abandoned" *(documented: `README.md`)* |
-| **PLC4X proxy driver (`plc4j/drivers/plc4x`)** | `plc4x:tcp://host/?remote-connection-string=...` | network — outbound to a remote PLC4X server | **yes for the client side; the server side is in `plc4x-extras` and out of scope** *(documented: `website/.../protocols/plc4x.adoc`)* |
-| **Simulated / mock drivers (`plc4j/drivers/{simulated,mock}`)** | `simulated://...` | no | **yes for code-quality; not a security surface** |
-| **Utilities (`plc4j/utils/{pcap-replay,pcap-shared,plc-simulator,raw-sockets,test-generator,test-utils}`)** | dev tooling | varies | **out** — dev / test only *(inferred — §14 Q2)* |
-| **Code-generation tooling under `code-generation/`** | mspec compiler invoked at build | host filesystem | **out** — build-time, not in the deployed artifact *(inferred — §14 Q2)* |
-| **`tools/`, `images/`, `media/`, `website/`, `licenses/`, `src/`, `.idea/`, `.github/`, `.mvn/`, `Dockerfile`, `docker-compose.yaml`, `Jenkinsfile`** | tooling, docs, build orchestration | n/a | **out** *(inferred — §14 Q2)* |
+| Family | Representative entry point | Touches outside the process? | In this model? |
+|-------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|-------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|
+| **Java public API (`plc4j/api`)** | `PlcDriverManager.getConnectionFactory().getConnection(url, auth)` | no — pure dispatch | **yes** |
+| **Java SPI / runtime (`plc4j/spi`)** | `AbstractPlcConnection`, `ConversationContext`, `Plc4xProtocolBase` | no — orchestration | **yes** |
+| **Per-protocol drivers, green in the driver matrix** (`plc4j/drivers/*`, `plc4go/internal/*`) | one `PlcDriver` per protocol | **yes — network**, via the registered transport | **yes** — a green check in that language's column of `website/.../protocols/index.adoc` is the test; see §3 item 6 |
+| **Per-protocol drivers, not green in the driver matrix** | a `PlcDriver` that is unfinished or only partially implemented | opens a socket, but is not a supported surface | **out** — not declared usable, therefore not a supported product surface *(documented: `website/.../protocols/index.adoc`)* — §3 item 6 |
+| **Transports (`plc4j/transports/{tcp,udp,serial,raw-socket,pcap-replay,pcap-shared,socketcan,can,virtualcan,test}`)** | Netty socket / serial / pcap | **yes — sockets, serial devices, libpcap** | **yes** |
+| **OPC UA security subsystem** (`plc4j/drivers/opcua/.../security/`, `.../context/SecureChannel`) | certificate verifier, security-policy negotiator, secure-channel encoder | **yes — TLS-like primitives on the wire** | **yes — distinguished from other drivers; see §8 P1** |
+| **Code-generation runtime (mspec read/write generators)** under `plc4j/.../readwrite/`, `plc4go/protocols/`, `plc4c/generated-sources/` | generated parsers / serializers | **yes — driven by remote-PLC bytes** | **yes** — same trust level as the driver that uses it |
+| **Go bindings (`plc4go/`)** | `plc4go.NewPlcDriverManager()` | as core | **yes** |
+| **C bindings (`plc4c/`)** | `plc4c_*` API | as core | **yes — but README declares "not ready for usage"** *(documented: `README.md`)* — see §3 |
+| **Python bindings (`plc4py/`)** | `plc4py` import | as core | **yes — but README declares "not ready for usage"** *(documented: `README.md`)* — see §3 |
+| **.NET bindings (`plc4net/`)** | n/a | n/a | **out** — README declares "not ready for usage - abandoned" *(documented: `README.md`)* |
+| **PLC4X proxy driver (`plc4j/drivers/plc4x`)** | `plc4x:tcp://host/?remote-connection-string=...` | network — outbound to a remote PLC4X server | **yes for the client side; the server side is in `plc4x-extras` and out of scope** *(documented: `website/.../protocols/plc4x.adoc`)* |
+| **Simulated / mock drivers (`plc4j/drivers/{simulated,mock}`)** | `simulated://...` | no | **yes for code-quality; not a security surface** |
+| **Utilities (`plc4j/utils/{pcap-replay,pcap-shared,plc-simulator,raw-sockets,test-generator,test-utils}`)** | dev tooling | varies | **out** — dev / test only *(inferred — §14 Q2)* |
+| **Code-generation tooling under `code-generation/`** | mspec compiler invoked at build | host filesystem | **out** — build-time, not in the deployed artifact *(inferred — §14 Q2)* |
+| **`tools/`, `images/`, `media/`, `website/`, `licenses/`, `src/`, `.idea/`, `.github/`, `.mvn/`, `Dockerfile`, `docker-compose.yaml`, `Jenkinsfile`** | tooling, docs, build orchestration | n/a | **out** *(inferred — §14 Q2)* |
A finding is in-model only if it lands in a row marked "yes" — see §4
for the per-component reachability test.
@@ -196,6 +201,61 @@ any of these will be closed with the cited disposition.
`Dockerfile` / `docker-compose.yaml` / `Jenkinsfile` are build,
test, and documentation infrastructure *(documented: `README.md`;
inferred for the rest — §14 Q2)*. → `OUT-OF-MODEL: unsupported-component`.
+
+ The same carve-out covers **drivers that are not ready to be used**.
+ Rather than enumerate them here — a list that would go stale on every
+ driver change — this model **delegates the question to the
+ per-language driver matrix** in
+ `website/asciidoc/modules/users/pages/protocols/index.adoc`, which is
+ maintained alongside the code. The rule is:
+
+ > **A driver is in scope of this threat model for a given language if
+ > and only if its cell in that language's column of the
+ > *"Current language support for protocols"* table is a green check
+ > (`icon:check[role="green"]`, "Implemented and usable").**
+ >
+ > Every other marking — yellow `icon:exclamation` (partially
+ > implemented) or red `icon:times` (not usable: either no driver
+ > exists, or one exists but is unfinished) — puts that language's
+ > driver **out of scope**. A vulnerability report against
+ > a driver that is not green is declined with
+ > `OUT-OF-MODEL: unsupported-component`, citing the matrix cell.
+
+ The matrix is **per language**, and so is this rule: a protocol can
+ be in model in one language and out in another. C-Bus is the standing
+ example — the Go driver is green and in model, while the Java driver
+ is an unfinished skeleton and is not.
+
+ Consequences worth stating plainly:
+
+ - **The matrix is normative for scope.** Changing a cell changes what
+ this document covers, with no edit to this document required —
+ which is the point. §12 accordingly names a matrix change, not a
+ list change, as the revision trigger.
+ - **The second table on that page (*Features*) is not the scope
+ test.** It records which operations a driver implements, for
+ `plc4j` only. Scope is decided by the first table.
+ - **For `plc4j` the rule is enforced by the build.**
+ `plc4j-driver-all` bundles exactly the drivers green in the Java
+ column and nothing else *(documented: `plc4j/drivers/all/pom.xml`)*,
+ so a non-green driver reaches an application's classpath only if it
+ was depended on deliberately, by artifact. The other language
+ bindings have no equivalent meta-package, so there the matrix is
+ the only statement. Either way, presence on a classpath would not
+ make a driver an attack surface — being *drivable* does. Findings
+ in non-green drivers are code-quality issues, not vulnerabilities.
+ - **Green does not mean "implements every operation".** It means the
+ driver is declared ready: what it implements works, and it does not
+ advertise a capability it lacks. Several green drivers cover only
+ part of the operation set — AB-Ethernet reads but does not write,
+ Java CAN raw and Firmata write and subscribe but do not read (raw
+ CAN and Firmata have no meaningful read), IEC-60870-5-104
+ subscribes only. Those still parse untrusted wire bytes on a path
+ an application drives, which is exactly the §4 B2 boundary, so they
+ are in scope. What puts a driver out is being unfinished — no
+ usable operation, or metadata that advertises what the code cannot
+ do *(maintainer: chrisdutz, 2026-08-21 — see §14 Q32)*.
+
7. **The PLC device firmware, the network the device is on, and the
physical process the device controls.** PLC4X is a wire-protocol
library; bugs / exploits in the PLC itself (S7 vendor-specific
@@ -233,15 +293,15 @@ PLC4X has **two primary trust boundaries** plus one optional
cryptographic one for OPC UA. A finding is in-model only when it cleanly
maps to one of them.
-| # | Transition | Authentication [...]
-|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- [...]
-| B1 | **Embedding application → PLC4X API surface** | trusted by construction [...]
-| B2 | **PLC4X driver → remote PLC over the wire** (Modbus, S7 PUT/GET, BACnet/IP, IEC-60870-5-104, Profinet, CANopen, KNXnet/IP, C-Bus, DF1, AB-Ethernet, EtherNet/IP, UMAS, ADS/AMS without credentials) | **none — by protocol design** *(documented per protocol)* [...]
-| B2-OPCUA | **PLC4X OPC UA driver → OPC UA server (encrypted policy)** | server cert verified against trust store *(when `trust-store-file` is set)*; client cert optional; username/password or token over the encrypted channel [...]
-| B2-OPCUA-PERMISSIVE | **OPC UA driver in default (no `trust-store-file`) mode** | server certificates **are not validated** by default *(documented: `website/.../protocols/opcua.adoc` — "Unless explicitly disabled through configuration of `trust-store-file` all server certificates will be accepted without validation"; `plc4j/drivers/opcua/.../securit [...]
-| B2-ADS | **ADS driver → Beckhoff TwinCAT (AMS route setup with credentials)** | username/password to the TwinCAT system for AMS-route setup, when a `PlcUsernamePasswordAuthentication` is supplied *(documented: `plc4j/drivers/ads/.../AdsProtocolLogic.java` lines 130–145)* [...]
-| B3 | **PLC4X transport → host OS / NIC / serial port / libpcap** | OS-level [...]
-| B4 | **Code-generation runtime → operating system** at build time | n/a — only the developer runs this [...]
+| # | Transition | Authentication [...]
+|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- [...]
+| B1 | **Embedding application → PLC4X API surface** | trusted by construction [...]
+| B2 | **PLC4X driver → remote PLC over the wire** (every cleartext protocol whose driver is green in the §3 item 6 matrix — Modbus, S7 PUT/GET, IEC-60870-5-104, CANopen, KNXnet/IP, C-Bus *(Go)*, AB-Ethernet, BACnet/IP *(Go)*, CAN raw *(Java)*, Firmata, EtherNet/IP incl. Logix, SLMP, UMAS, ADS/AMS without credentials) | **none — by protocol design** *(documented per protocol)* [...]
+| B2-OPCUA | **PLC4X OPC UA driver → OPC UA server (encrypted policy)** | server cert verified against trust store *(when `trust-store-file` is set)*; client cert optional; username/password or token over the encrypted channel [...]
+| B2-OPCUA-PERMISSIVE | **OPC UA driver in default (no `trust-store-file`) mode** | server certificates **are not validated** by default *(documented: `website/.../protocols/opcua.adoc` — "Unless explicitly disabled through configuration of `trust-store-file` all server certificates will be accepted without validation"; `plc4j/drivers/opcua/. [...]
+| B2-ADS | **ADS driver → Beckhoff TwinCAT (AMS route setup with credentials)** | username/password to the TwinCAT system for AMS-route setup, when a `PlcUsernamePasswordAuthentication` is supplied *(documented: `plc4j/drivers/ads/.../AdsProtocolLogic.java` lines 130–145)* [...]
+| B3 | **PLC4X transport → host OS / NIC / serial port / libpcap** | OS-level [...]
+| B4 | **Code-generation runtime → operating system** at build time | n/a — only the developer runs this [...]
### Reachability preconditions per family
@@ -411,16 +471,16 @@ hypothesis (b)** and will need adjustment if the maintainer chooses
### Per-network-input trust table (wire bytes)
-| Surface | Parameter | Attacker-controllable? | Driver must enforce |
-|--------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Cleartext OT-protocol response (Modbus, S7, BACnet/IP, IEC-60870-104, Profinet, CANopen, KNXnet/IP, C-Bus, DF1, AB-Ethernet, ADS payload, EtherNet/IP, UMAS) | every byte of every response frame | **yes** — anyone on the OT network with reachability can spoof | **memory safety, bounded allocation, no infinite loop, no unbounded recursion** on malformed input — but **not** authenticity, integrity, or any payload-semantic guarantee *(inferred — §14 Q11)* |
-| OPC UA wire frames inside the secure channel | every byte | **yes** — but signed/encrypted by the negotiated policy when policy ≠ `NONE` | as above plus: correct verification of signature and MAC under the negotiated policy; correct decryption; correct chunk reassembly *(documented + inferred — §14 Q17)* |
-| OPC UA wire frames during discovery (`security-policy=NONE` phase) | every byte | **yes** | memory safety on malformed responses; **the discovery handshake itself is not authenticated** *(documented)* |
-| OPC UA server certificate (presented during handshake) | full DER bytes | **yes** | when `trust-store-file` is set: X.509 chain validation per JCE rules; when not set (default): **none** *(documented: §5a)* |
-| Serial-line frames | every byte | yes if the serial channel is attacker-reachable | memory safety on malformed framing *(inferred — §14 Q11)* |
-| libpcap capture / replay frames | every byte | yes if the capture file is attacker-controlled | the pcap-replay transport is a **dev/test tool**; if it's running in production, the integrator has put it there |
-| ETS `.knxproj` XML | as XML | yes if the file is attacker-supplied | XXE disabled *(documented)*; ZIP slip protection — *(inferred — §14 Q18)* |
-| BACnet EDE file | as text | yes if the file is attacker-supplied | parser robustness *(inferred — §14 Q18)* |
+| Surface | Parameter | Attacker-controllable? | Driver must enforce |
+|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Cleartext OT-protocol response, for any driver green in the §3 item 6 matrix (Modbus, S7, IEC-60870-104, CANopen, KNXnet/IP, C-Bus *(Go)*, AB-Ethernet, BACnet/IP *(Go)*, CAN raw, Firmata, ADS payload, EtherNet/IP incl. Logix, SLMP, UMAS) | every byte of every response frame | **yes** — anyone on the OT network with reachability can spoof | **memory safety, bounded allocation, no infinite loop, no unbounded recursion** on malformed input — but **not** authenticity, integri [...]
+| OPC UA wire frames inside the secure channel | every byte | **yes** — but signed/encrypted by the negotiated policy when policy ≠ `NONE` | as above plus: correct verification of signature and MAC under the negotiated policy; correct decryption; correct chunk reassembly *(documented + inferred — §14 Q17)* |
+| OPC UA wire frames during discovery (`security-policy=NONE` phase) | every byte | **yes** | memory safety on malformed responses; **the discovery handshake itself is not authenticated** *(documented)* |
+| OPC UA server certificate (presented during handshake) | full DER bytes | **yes** | when `trust-store-file` is set: X.509 chain validation per JCE rules; when not set (default): **none** *(documented: §5a)* |
+| Serial-line frames | every byte | yes if the serial channel is attacker-reachable | memory safety on malformed framing *(inferred — §14 Q11)* |
+| libpcap capture / replay frames | every byte | yes if the capture file is attacker-controlled | the pcap-replay transport is a **dev/test tool**; if it's running in production, the integrator has put it there |
+| ETS `.knxproj` XML | as XML | yes if the file is attacker-supplied | XXE disabled *(documented)*; ZIP slip protection — *(inferred — §14 Q18)* |
+| BACnet EDE file | as text | yes if the file is attacker-supplied | parser robustness *(inferred — §14 Q18)* |
### Size / shape / rate
@@ -824,7 +884,10 @@ every wire-format property of every driver.**
- **"Profinet IO frames are not authenticated."** Profinet's spec
splits "Profinet" into multiple application protocols; the variant
here does not include the optional Profinet-Security extensions. →
- §3 item 1, §9. `BY-DESIGN`.
+ §3 item 1, §9. `BY-DESIGN`. Both Java Profinet drivers — `profinet`
+ and `profinet-ng` — are also not green in the matrix and so out of
+ scope in their own right (§3 item 6), making a report here doubly
+ non-actionable.
- **"CANopen / SocketCAN frames are unauthenticated."** CAN is a
fieldbus; the integrity property is "this frame originated on this
physical bus", not cryptographic. → §3 item 1, §9. `BY-DESIGN`.
@@ -834,7 +897,9 @@ every wire-format property of every driver.**
§3 item 1, §9. `BY-DESIGN`.
- **"C-Bus, DF1, AB-Ethernet, EtherNet/IP (classic CIP), UMAS, ADS/AMS
payload traffic, Open-Protocol — all unauthenticated /
- cleartext."** Per spec. → §3 item 1, §9. `BY-DESIGN`.
+ cleartext."** Per spec. → §3 item 1, §9. `BY-DESIGN`. The Java C-Bus
+ and Open-Protocol drivers are additionally not green in the matrix and
+ so out of scope per §3 item 6; the Go C-Bus driver is the in-model one.
- **"Firmata is a serial protocol with no authentication."**
Firmata is a hobbyist Arduino protocol; the security model is
"the serial cable is your perimeter". → §3 item 1, §9. `BY-DESIGN`.
@@ -875,6 +940,20 @@ every wire-format property of every driver.**
- **"Cryptographic finding in `plc4c/` / `plc4py/` / `plc4net/`."**
README flags these as not-ready. → §3 item 6. `OUT-OF-MODEL:
unsupported-component` pending the README line being removed.
+- **"Unimplemented / stubbed / dead code in a driver —
+ `discoverWithHandler` returns `null`, `onRegisterConsumer` throws, a
+ subscription future is never completed, a connection implements no
+ request builder."** Check the driver matrix first. For a driver that
+ is not green, the stubs *are* the current state, not a bug in a
+ shipped feature. → §3 item 6. `OUT-OF-MODEL: unsupported-component`.
+- **"Any finding — NPE, hang, resource leak, parser crash, weak
+ default — in a driver that is not green in the matrix."** Out of
+ scope by the §3 item 6 rule, whatever its technical merit. At the time
+ of writing that covers ctrlX, Open-Protocol, Java C-Bus, Java
+ BACnet/IP and both Java Profinet drivers, but **consult the
+ matrix rather than this sentence** — it is the authority and this list
+ is only illustrative. → §3 item 6.
+ `OUT-OF-MODEL: unsupported-component`.
- **"`code-generation/` mspec compiler has X."** Build-time only,
not in the runtime artifact. → §3 item 6. `OUT-OF-MODEL:
unsupported-component`.
@@ -915,6 +994,16 @@ Revise this document when any of the following lands:
`security-policy`, `trust-store-file`, or `message-security`.
- `plc4c/`, `plc4py/`, or `plc4net/` has its "not ready for usage"
README flag removed — those move from §3 item 6 to in-model.
+- **A cell in the driver matrix
+ (`website/.../protocols/index.adoc`, "Current language support for
+ protocols") changes to or from a green check.** This is the scope
+ trigger for drivers, and it takes effect *without* an edit to this
+ document — §3 item 6 delegates to the matrix precisely so the list
+ lives in one place. What does need review when a driver turns green:
+ its artifact should be added to `plc4j/drivers/all/pom.xml` (removed
+ when it turns non-green), its protocol should join the §4 B2 and §8
+ untrusted-input example lists, and any §11a entry naming it should be
+ struck.
- The boundary with `plc4x-extras` shifts (e.g. an integration is
brought back into the main repo, or vice versa).
- A vulnerability report that cannot be cleanly routed to one of the
@@ -930,7 +1019,7 @@ A report against PLC4X receives exactly one of the following.
| `VALID-HARDENING` | No §8 property violated, but a §11 misuse pattern can be made harder to fall into by code change (e.g. issue a runtime warning when OPC UA runs `security-policy=NONE`). Fixed at maintainer discretion, typically no CVE. | §11 |
| `OUT-OF-MODEL: trusted-input` | Requires attacker control of a §6 parameter the model marks trusted — the connection URL, the auth object, the value being written, tag-address strings (per §3 item 5 when the integrator has not opted them into untrusted-string handling). | §6 |
| `OUT-OF-MODEL: adversary-not-in-scope` | Requires a §7 actor the model excludes — embedding application is hostile, side-channel observer, quantum adversary, OT-network perimeter is the integrator's problem. | §7 |
-| `OUT-OF-MODEL: unsupported-component` | Lands in `plc4c/`, `plc4py/`, `plc4net/`, `tools/`, `code-generation/`, `plc4j/utils/`, `plc4j/drivers/{simulated,mock}`, `plc4x-extras` content, vendored upstream code, or repo infrastructure. | §3 item 2, §3 item 6 |
+| `OUT-OF-MODEL: unsupported-component` | Lands in `plc4c/`, `plc4py/`, `plc4net/`, `tools/`, `code-generation/`, `plc4j/utils/`, `plc4j/drivers/{simulated,mock}`, any driver not marked with a green check in the §3 item 6 driver matrix, `plc4x-extras` content, vendored upstream code, or repo infrastructure. | §3 item 2, §3 item 6 |
| `OUT-OF-MODEL: non-default-build` | Only manifests under a §5a knob the maintainer has ruled is dev/test. **Note:** per the maintainer (chrisdutz, §14 Q14/Q15/Q16) the OPC UA insecure defaults are moving to secure-by-default — the permissive certificate verifier in particular is a gap to fix (`VALID`), not a non-default-build exclusion. | §5a |
| `BY-DESIGN: protocol-disclaimed` | Concerns a property the **protocol** (not the library) does not provide — every "Modbus / S7 / BACnet / etc. is unauthenticated" report. | §9, §3 item 1, §11a |
| `BY-DESIGN: property-disclaimed` | Concerns a §9 property the library explicitly does not provide (built-in TLS tunneling, end-user authn, DoS protection at the API). | §9 |
@@ -1163,6 +1252,35 @@ recurring false-positives strengthen the suppression list. *(meta —
**Q31.** What kind of change to PLC4X should trigger a revision
(proposed list in §12 — confirm or correct)? *(meta, §12)*
+**Q32.** Driver scope. **Answered (chrisdutz, 2026-08-21):** rather
+than carry a list of unfinished drivers in this document, scope is
+delegated to the per-language driver matrix in
+`website/.../protocols/index.adoc` — a driver is in scope for a
+language iff that language's cell is a green check, and a report
+against any driver that is not green is declined. Two consequences the
+maintainer accepted explicitly: both Java Profinet drivers — the older
+`plc4j/drivers/profinet` and the newer `plc4j/drivers/profinet-ng`,
+both kept in the tree — are listed as unsupported and therefore out of
+scope; and partial coverage on its own does not make a driver
+unsupported, so the Java AB-Ethernet, CAN raw and Firmata drivers are
+green and in scope while the Java C-Bus driver, which advertises
+`canRead()` and implements nothing, is not. The Go column was audited
+on the same test and every Go driver was found to declare its
+capabilities honestly through `PlcConnectionMetadata`; Go AB-Ethernet,
+BACnet/IP and Firmata are green with the rest.
+
+The **Java BACnet/IP** driver is the case that shows why the matrix,
+not a code reading, is the authority. Its code passes the mechanical
+test — it advertises only `canSubscribe` and implements exactly that,
+the same shape as the green IEC-60870-5-104 driver — but the
+maintainer's assessment is that it is an experiment that does not hold
+up at scale *(maintainer: chrisdutz, 2026-08-21)*. It stays non-green
+and therefore out of scope. Readiness is a maintainer judgement that
+the matrix records; a driver can be internally consistent and still not
+be something to build on. The Go BACnet/IP driver is unaffected and
+stays in scope.
+*(maps to §1, §2, §3 item 6, §4 B2, §8, §11a, §12, §13)*
+
---
## Appendix: Existing security-policy artefact → §x back-map
@@ -1197,6 +1315,8 @@ security-policy artefacts are:
| `website/.../protocols/canopen.adoc` ("CANopen … address areas") | CANopen scope | §3 item 1, §11a |
| `website/.../protocols/ads.adoc` ("device-independent and fieldbus independent interface for communication between Beckhoff automation devices") | ADS scope | §3 item 1, §11a |
| `website/.../protocols/index.adoc` | per-language driver-coverage matrix | §2 component table |
+| `website/.../protocols/index.adoc` ("Current language support for protocols" table + its "Scope of support" note: green check = supported and in the threat model; anything else = unsupported, security reports declined) | **normative** driver-scope rule | §1, §2, §3 item 6, §4 B2, §8, §11a, §12, §13, §14 Q32 |
+| `plc4j/drivers/all/pom.xml` (bundles exactly the Java-green drivers: ab-eth, ads, can, canopen, eip, firmata, iec-60870, knxnetip, modbus, opcua, plc4x, s7, simulated, slmp, umas) | the matrix rule, enforced by the build | §3 item 6 |
| `website/asciidoc/.../security.adoc` ("For more information about reporting vulnerabilities, see the Apache Security Team page") | reporting channel | §1 reporting cross-reference |
| `website/asciidoc/.../developers/maturity.adoc` (QU20, QU30) | maturity posture | §1 reporting cross-reference |
| `plc4j/drivers/opcua/.../security/PermissiveCertificateVerifier.java` (no-op `checkCertificateTrusted`) | implements the default | §4 B2-OPCUA-PERMISSIVE, §5a |
diff --git a/plc4j/drivers/all/pom.xml b/plc4j/drivers/all/pom.xml
index 15f74a4e73..08ada01939 100644
--- a/plc4j/drivers/all/pom.xml
+++ b/plc4j/drivers/all/pom.xml
@@ -75,7 +75,17 @@
</build>
<dependencies>
- <!-- Include all non-mock drivers to produce a meta-package -->
+ <!--
+ Meta-package of the drivers that are ready to be used.
+
+ A driver belongs here only if it is marked with a green check in the Java column of the
+ driver matrix in website/asciidoc/modules/users/pages/protocols/index.adoc ("Current
+ language support for protocols"). That table is the authoritative statement of what is
+ supported, and it is what the security threat model (THREAT-MODEL.md, section 3 item 6)
+ keys off. Drivers that are unfinished or only partially implemented are deliberately left
+ out and have to be depended on explicitly: bacnet, c-bus, ctrlx, open-protocol, profinet,
+ profinet-ng.
+ -->
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-driver-ab-eth</artifactId>
@@ -88,18 +98,6 @@
<version>1.0.0-SNAPSHOT</version>
<scope>runtime</scope>
</dependency>
- <dependency>
- <groupId>org.apache.plc4x</groupId>
- <artifactId>plc4j-driver-bacnet</artifactId>
- <version>1.0.0-SNAPSHOT</version>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>org.apache.plc4x</groupId>
- <artifactId>plc4j-driver-c-bus</artifactId>
- <version>1.0.0-SNAPSHOT</version>
- <scope>runtime</scope>
- </dependency>
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-driver-can</artifactId>
@@ -136,12 +134,6 @@
<version>1.0.0-SNAPSHOT</version>
<scope>runtime</scope>
</dependency>
- <dependency>
- <groupId>org.apache.plc4x</groupId>
- <artifactId>plc4j-driver-open-protocol</artifactId>
- <version>1.0.0-SNAPSHOT</version>
- <scope>runtime</scope>
- </dependency>
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-driver-modbus</artifactId>
@@ -160,12 +152,6 @@
<version>1.0.0-SNAPSHOT</version>
<scope>runtime</scope>
</dependency>
- <dependency>
- <groupId>org.apache.plc4x</groupId>
- <artifactId>plc4j-driver-profinet-ng</artifactId>
- <version>1.0.0-SNAPSHOT</version>
- <scope>runtime</scope>
- </dependency>
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-driver-s7</artifactId>
diff --git a/website/asciidoc/modules/users/pages/protocols/index.adoc b/website/asciidoc/modules/users/pages/protocols/index.adoc
index 464f9a8fe2..454f2b12e5 100644
--- a/website/asciidoc/modules/users/pages/protocols/index.adoc
+++ b/website/asciidoc/modules/users/pages/protocols/index.adoc
@@ -22,14 +22,17 @@ The tables below list the drivers that actually ship with each language implemen
A protocol only appears as supported where a driver exists and is registered with that
implementation's driver manager - a protocol specification on its own is not enough.
+The first table is also the project's *definition of which drivers are supported*: see
+<<scope-of-support>> below before relying on a driver or reporting a security issue against one.
+
|===
|Protocol | C | C# | Go | Java | Python
|AB-Ethernet
|icon:times[role="red"]
|icon:times[role="red"]
-|icon:exclamation[role="yellow"]
-|icon:exclamation[role="yellow"]
+|icon:check[role="green"]
+|icon:check[role="green"]
|icon:times[role="red"]
|ADS /AMS
@@ -42,15 +45,15 @@ implementation's driver manager - a protocol specification on its own is not eno
|BACnet/IP
|icon:times[role="red"]
|icon:times[role="red"]
-|icon:exclamation[role="yellow"]
-|icon:exclamation[role="yellow"]
+|icon:check[role="green"]
+|icon:times[role="red"]
|icon:times[role="red"]
|CAN (raw)
|icon:times[role="red"]
|icon:times[role="red"]
|icon:times[role="red"]
-|icon:exclamation[role="yellow"]
+|icon:check[role="green"]
|icon:times[role="red"]
|CANopen
@@ -64,14 +67,14 @@ implementation's driver manager - a protocol specification on its own is not eno
|icon:times[role="red"]
|icon:times[role="red"]
|icon:check[role="green"]
-|icon:exclamation[role="yellow"]
+|icon:times[role="red"]
|icon:times[role="red"]
|ctrlX
|icon:times[role="red"]
|icon:times[role="red"]
|icon:times[role="red"]
-|icon:exclamation[role="yellow"]
+|icon:times[role="red"]
|icon:times[role="red"]
|DF1
@@ -98,8 +101,8 @@ implementation's driver manager - a protocol specification on its own is not eno
|Firmata
|icon:times[role="red"]
|icon:times[role="red"]
-|icon:exclamation[role="yellow"]
-|icon:exclamation[role="yellow"]
+|icon:check[role="green"]
+|icon:check[role="green"]
|icon:times[role="red"]
|IEC-60870
@@ -134,7 +137,7 @@ implementation's driver manager - a protocol specification on its own is not eno
|icon:times[role="red"]
|icon:times[role="red"]
|icon:times[role="red"]
-|icon:exclamation[role="yellow"]
+|icon:times[role="red"]
|icon:times[role="red"]
|PLC4X (Proxy-Protocol)
@@ -144,11 +147,18 @@ implementation's driver manager - a protocol specification on its own is not eno
|icon:check[role="green"]
|icon:times[role="red"]
-|Profinet
+|Profinet (`profinet`)
+|icon:times[role="red"]
+|icon:times[role="red"]
+|icon:times[role="red"]
+|icon:times[role="red"]
+|icon:times[role="red"]
+
+|Profinet (`profinet-ng`)
+|icon:times[role="red"]
|icon:times[role="red"]
|icon:times[role="red"]
|icon:times[role="red"]
-|icon:exclamation[role="yellow"]
|icon:times[role="red"]
|S7
@@ -184,40 +194,90 @@ implementation's driver manager - a protocol specification on its own is not eno
|===
|Legend:
-- icon:check[role="green"] Implemented and usable
-- icon:exclamation[role="yellow"] Partially implemented - see the notes below
-- icon:times[role="red"] Not implemented
+- icon:check[role="green"] Supported - ready to be used. Not every supported driver implements
+every operation: some protocols have no meaningful read, or the driver covers a deliberate subset.
+What it does implement works. See <<features,Features>> for the per-operation breakdown and
+<<notes-on-driver-coverage,Notes on driver coverage>> for the scope of each.
+- icon:exclamation[role="yellow"] Partially implemented and not supported - see the notes below
+- icon:times[role="red"] Not usable. Either no driver exists for that language, or one exists but is
+unfinished and nothing can be built on it - see <<unfinished-drivers,Unfinished drivers>> for which
+is which. Either way it is not supported.
|===
-Notes on the partially implemented drivers:
-
-* *AB-Ethernet* reads and offers polled subscriptions in both Java and Go; neither writes.
-* *BACnet/IP* passively receives broadcast traffic and exposes it as subscriptions. It does not read or write properties.
-* *CAN (raw)* (Java) writes and subscribes; there is no read support.
-* *CBus* (Java) is a skeleton that connects but implements no operations. The Go driver is the complete one.
-* *ctrlX* (Java) answers pings; that is the only operation it completes. Discovery is offered by
-the driver, but `CtrlXPlcDiscoverer.discoverWithHandler` is an unimplemented TODO that returns
-`null` instead of a future, so a discovery request fails rather than finding anything. Its read,
-write and subscribe builders are not implemented either, and although the connection carries
-browse code, browsing does not work yet.
-* *Firmata* writes and subscribes in both Java and Go; neither reads. A board is driven and
-monitored, never polled.
+[#scope-of-support]
+== Scope of support
+
+The table above is the authoritative statement of what this project supports, per language:
+
+* A driver marked icon:check[role="green"] in a language's column is *ready to be used*. It is a
+supported part of the release and is in scope of the project's security threat model
+(`THREAT-MODEL.md`).
+* Every other marking - icon:exclamation[role="yellow"] or icon:times[role="red"] - means the driver
+is *not declared ready*. It is not supported, and it is
+*out of scope of the threat model*: a vulnerability report against a driver that is not marked with
+a green check will be declined, citing this table. Findings in such a driver are treated as
+code-quality issues rather than as vulnerabilities.
+
+This is decided *per language*: the same protocol can be supported in one language and not in
+another. C-Bus is the standing example - the Go driver is green and supported, the Java driver is an
+unfinished skeleton and is not.
+
+This rule deliberately does not depend on the second table on this page (<<features,Features>>),
+which records which operations a driver implements in plc4j and is descriptive only.
+
+For plc4j the rule is also enforced by the build: `plc4j-driver-all` bundles exactly the drivers
+that are green in the Java column, and nothing else. A driver that is not green has to be depended
+on explicitly, by its own artifact - it will not arrive on your classpath through the meta package.
+
+The way to move a driver into scope is to finish it and change its cell to a green check - a change
+to this table changes the threat model's coverage, with no edit to `THREAT-MODEL.md` needed.
+
+[#notes-on-driver-coverage]
+== Notes on driver coverage
+
+What the drivers marked icon:exclamation[role="yellow"] above do and do not do, plus the scope of
+the green ones that cover only part of the operation set. A green driver that appears here is
+supported - the entry describes what it is for, not a defect:
+
+* *AB-Ethernet* reads and offers polled subscriptions, and both drivers are supported. In Java
+`onRead` is a complete implementation (sequential single-flight reads with a transaction counter,
+request timeout and per-tag response codes) and subscriptions come from
+`PollingSubscriptionConnectionBase`; Go mirrors it exactly, with `DefaultPollingSubscriber` as the
+counterpart. Neither writes, and neither advertises the capability, so a write request is refused
+rather than silently dropped - DF1 does define write commands, so this is an unimplemented feature
+rather than a protocol limit. The Go reader decodes the `Integer`, `Word`, `Dword` and `Singlebit`
+file types and answers `UNSUPPORTED` for anything else, which is one better than plc4j, where the
+same case returns OK with a null value.
+* *BACnet/IP* has two drivers that are not the same driver, and only the Go one is supported. *Go*
+does `ReadProperty` (with segmentation and complex-ack decoding), `WriteProperty`, and real COV
+subscriptions via `SubscribeCOV` with a lifetime. *Java* is an experiment and is not supported: it
+is passive, receiving broadcast traffic and exposing it as subscriptions, and it does not read or
+write properties. Its code is honest about what it declares - it advertises only `canSubscribe` and
+implements that - but it does not hold up at scale, so it is not something to build on.
+* *CAN (raw)* (Java) writes and subscribes, and is supported. Writes for one node id are packed into
+a single frame and serialized per tag with individual response codes; subscriptions register a
+consumer per node and every inbound frame is dispatched as a `PlcSubscriptionEvent`. There is no
+read, and there cannot meaningfully be one: a raw CAN bus is broadcast, with no request/response
+exchange to address.
+* *Firmata* writes and subscribes, and both drivers are supported. Writes are built into Firmata
+messages and shipped (the protocol has no write acknowledgement, so the response is OK once the
+bytes are out); a subscription sets the pin modes and enables reporting, and pin updates are
+dispatched as subscription events. Neither driver reads - a board is driven and monitored, never
+polled, which is how Firmata works.
* *IEC 60870-5-104* subscribes only, in both Java and Go, because the protocol is push driven: the
RTU sends unsolicited ASDUs once the session is started, so there is nothing to read or write. The Go
driver keeps the IEC quality descriptors alongside each value, so a reading the RTU marked invalid,
blocked, substituted or not-topical does not arrive looking like a good one.
-* *KNXnet/IP* (C#) is the only driver in plc4net and has not kept pace with the other implementations.
-* *Open-Protocol* (Java) is a skeleton that connects but implements no operations.
-* *Profinet* (Java) discovers devices and browses the submodules of a connected device; neither of
-the two Java drivers has read or write support. Only the newer `profinet-ng` driver is part of
-`plc4j-driver-all`; the older `profinet` driver is still in the code base but is not bundled. In
-`profinet-ng` a subscription request completes the PROFINET connect handshake, but the subscription
-response future is deliberately left incomplete and `onRegisterConsumer` throws
-`UnsupportedOperationException`, so no cyclic data ever reaches an application. The unbundled
-`profinet` driver does implement that path - it completes the subscription response, registers
-consumers per subscription handle and dispatches a `PlcSubscriptionEvent` for every incoming cyclic
-frame - so the subscribe entry below, which describes what ships, does not apply to it.
+* *KNXnet/IP* (C#) is the only driver in plc4net and has not kept pace with the other
+implementations. plc4net is flagged "not ready for usage - abandoned" in the README, so the whole
+implementation is unsupported, not just this driver.
+* *OPC-UA* and *CBus* (Go) both implement `Subscribe` fully but leave `Unsubscribe` returning
+"Not Implemented" - an active subscription ends when the connection closes. Everything else in both
+drivers works, which is why they are supported.
+* *CBus* (Go) additionally carries `TODO: implement me` gaps for several CAL command types in its
+tag handler and for some data types in its value handler. The mainstream lighting/status paths -
+read, write, browse and subscribe - are implemented.
* *SLMP* addresses the word devices `D`, `W` and `R` only, in both Java and Go. That is the scope
the driver was written to rather than a port gap - the bit devices and the wider MELSEC command set
are not implemented in either language.
@@ -231,6 +291,58 @@ message model for it, but no language implementation has a driver: there is noth
DF1 connection. DF1 command messages also appear inside the AB-Ethernet model, which is where that
protocol is actually used today.
+[#unfinished-drivers]
+== Unfinished drivers
+
+The drivers below are in the code base but are *not usable*: each one connects, and then completes
+nothing an application could be built on - at most a connectivity ping, a discovery scan or a
+browse, never a read, a write or a delivered subscription. They are marked icon:times[role="red"] in
+the table above, the same as a protocol with no driver at all - from an application's point of view
+the two are equivalent, and this section is what tells them apart. All of them are Java-only - no other language ships a driver for these protocols.
+
+* *CBus* (Java) is a shell. It connects and starts a receive loop, but `getTagHandler()` returns
+`null` - no tag syntax is defined, so nothing can be addressed - and the incoming-message handler
+only writes a trace log, so received frames are discarded. No read, write, subscribe or browse
+builder exists anywhere in its 376 lines, yet `CBusDriver.canRead()` returns `true`, so the driver
+advertises a capability it does not have. *C-Bus is effectively a Go-only protocol*: the Go driver
+is a complete ~25,000-line implementation with read, write, browse and subscribe, and is supported.
+* *ctrlX* (Java) answers pings; that is the only operation it completes. Discovery is offered by
+the driver, but `CtrlXPlcDiscoverer.discoverWithHandler` is an unimplemented TODO that returns
+`null` instead of a future, so a discovery request fails rather than finding anything. Its read,
+write and subscribe builders are not implemented either, and although the connection carries
+browse code, browsing does not work yet.
+* *Open-Protocol* (Java) is a skeleton that connects but implements no operations.
+* *Profinet* - there are *two* Java drivers, `profinet` and `profinet-ng`, and *neither is
+supported*. Both discover devices and browse the submodules of a connected device, and neither has
+read or write support.
++
+--
+** `profinet-ng` is the newer one. A subscription request completes the PROFINET connect handshake,
+but the subscription response future is deliberately left incomplete and `onRegisterConsumer` throws
+`UnsupportedOperationException`, so no cyclic data ever reaches an application.
+** `profinet` is the older one. It does implement the cyclic path that `profinet-ng` does not - it
+completes the subscription response, registers consumers per subscription handle and dispatches a
+`PlcSubscriptionEvent` for every incoming frame - but it is superseded and still has no read or
+write, and it is not supported either.
+--
+
+None of these are bundled in `plc4j-driver-all` - it carries only the drivers that are green in the
+Java column - so an application picks one up only by depending on its artifact deliberately.
+
+NOTE: The Go column has now been audited to the same depth as the Java one. Every Go driver
+declares its capabilities through `PlcConnectionMetadata`, and in each case the declaration matches
+what the code implements - `simulated` is the clearest illustration: it hands out a subscription
+request builder but deliberately reports `ProvidesSubscribing: false`, because the `Subscriber`
+behind it is a stub, and the code says so in a comment. No Go driver was found advertising a
+capability it does not have, which is the failure that puts the Java C-Bus driver out of support.
+The remaining yellow cell is C# KNXnet/IP, and that whole implementation (plc4net) is abandoned.
+
+These drivers are unsupported and out of scope of the threat model under the general rule in
+<<scope-of-support>> - not because of a carve-out specific to them, but simply because they are not
+marked with a green check. The same applies to the icon:exclamation[role="yellow"] drivers listed
+further up: partial implementation is not readiness either.
+
+[#features]
== Features
The table below lists which operations each driver actually implements. It describes *plc4j*,
@@ -281,7 +393,7 @@ metadata inconsistently, so this table follows the code rather than the metadata
|icon:check[role="green"]
|icon:times[role="red"]
-|CBus
+|CBus (unsupported)
|icon:times[role="red"]
|icon:times[role="red"]
|icon:times[role="red"]
@@ -289,7 +401,7 @@ metadata inconsistently, so this table follows the code rather than the metadata
|icon:times[role="red"]
|icon:times[role="red"]
-|ctrlX
+|ctrlX (unsupported)
|icon:times[role="red"]
|icon:times[role="red"]
|icon:times[role="red"]
@@ -361,7 +473,7 @@ metadata inconsistently, so this table follows the code rather than the metadata
|icon:check[role="green"]
|icon:times[role="red"]
-|Open-Protocol (Torque-Tools)
+|Open-Protocol (Torque-Tools, unsupported)
|icon:times[role="red"]
|icon:times[role="red"]
|icon:times[role="red"]
@@ -377,7 +489,7 @@ metadata inconsistently, so this table follows the code rather than the metadata
|icon:times[role="red"]
|icon:times[role="red"]
-|Profinet (`profinet-ng`)
+|Profinet (`profinet-ng`, unsupported)
|icon:check[role="green"]
|icon:check[role="green"]
|icon:times[role="red"]
@@ -385,6 +497,14 @@ metadata inconsistently, so this table follows the code rather than the metadata
|icon:times[role="red"]
|icon:times[role="red"]
+|Profinet (`profinet`, unsupported, not bundled)
+|icon:check[role="green"]
+|icon:check[role="green"]
+|icon:times[role="red"]
+|icon:times[role="red"]
+|icon:check[role="green"]
+|icon:times[role="red"]
+
|S7
|icon:check[role="green"]
|icon:check[role="green"]