dot1x: add macOS and Windows 802.1X supplicant state table#113
dot1x: add macOS and Windows 802.1X supplicant state table#113robbiet480 wants to merge 37 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new eapol_status osquery table for macOS, backed by EAP8021X.framework (cgo) on Darwin, plus Bazel/WORKSPACE updates to support building with cgo.
Changes:
- Introduce
tables/eapolstatustable implementation (darwin cgo backend + non-darwin noop backend) and register it inmain.go. - Add unit tests covering row formatting, constraints handling, and helper functions.
- Update Bazel build config and WORKSPACE to enable cgo on macOS targets and add Apple toolchain dependencies.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tables/eapolstatus/eapolstatus.go | Core table schema, query logic, helpers, and row formatting |
| tables/eapolstatus/eapol_darwin.go | Darwin cgo backend calling EAP8021X via dlopen/dlsym |
| tables/eapolstatus/eapol_other.go | Non-darwin backend stub |
| tables/eapolstatus/eapolstatus_test.go | New unit tests for interface selection, row mapping, and helpers |
| tables/eapolstatus/BUILD.bazel | Bazel targets for library + tests with conditional cgo/linker opts |
| main.go | Registers eapol_status plugin on darwin |
| BUILD.bazel | Enables cgo + pure=off for macOS binaries and adds table dep |
| WORKSPACE | Adds apple_support/rules_cc toolchain setup for cgo builds |
| README.md | Documents the new eapol_status table |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
10dbb5f to
be7340d
Compare
be7340d to
0cb9e65
Compare
0cb9e65 to
791e608
Compare
791e608 to
8d164e8
Compare
608732a to
492c1b5
Compare
43bd354 to
557e164
Compare
557e164 to
6aebe6b
Compare
6aebe6b to
37e5824
Compare
…shared file Address Copilot review on the Windows backend: - GetStatus: bounds-check dataSize against sizeof(wlanConnectionAttributes) before dereferencing the WlanQueryInterface buffer, returning a per-interface error on a short buffer instead of risking an OOB read. - getWlanProfileXML / enumerateWlanInterfaceInfos: split the ret != 0 and nil-pointer cases so a "succeeded but returned no data" result no longer surfaces a misleading "errno 0" message. - Move the pure-Go WLAN profile XML parsing (eapMethod/EAPType/authMode/ TrustedRootCA extractors, isHexString, formatSHA1Hex, xml token helpers) and their tests out of the //go:build windows files into build-tag-free dot1x_wlanprofile.go / _test.go, so the parsing logic compiles, runs, and is coverage-counted on every platform (it now executes on the Linux CI / darwin test runs, not just Windows). Registered both files in BUILD.bazel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dows) - dot1x(darwin): dot1x_query now returns -2 when copy_state_fn reports success (ret==0) but yields a NULL status dictionary, and GetStatus maps that to a per-interface "returned no status" error. Previously this emitted a misleading "successful" row of sentinel values; now generateRows skips the interface. Documented the return codes. - dot1x(windows): wrap the underlying enumeration error with %w (now "%w: %w") so it stays introspectable in the chain while errors.Is(ErrBackendUnavailable) still holds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nual - Tests: replace t.Context() with context.Background() in the darwin and windows test files, matching the shared tests and avoiding a dependency on the newer (*testing.T).Context() helper. - Root BUILD.bazel: tag the darwin cgo go_binary targets "manual" so they're excluded from //... / :all wildcard expansion. `bazel build //...` / `bazel test //...` on a Linux/Windows host no longer fail trying to analyze them (they need a darwin C++ toolchain); explicit builds via the Makefile / bazel_to_builddir.sh still work. target_compatible_with can't gate these: the goos="darwin" transition makes the target platform macOS, so a macos constraint would always be satisfied and never skip on a Linux host. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Dot1XBackend doc comment now describes all three production backends (macOS cgo / EAP8021X, Windows wlanapi, other noop) instead of only darwin. - Move the duplicated constraintFor test helper out of the darwin and windows test files into dot1x_helpers_test.go. It carries a `darwin || windows` build tag (not untagged) because it's only used by those two backends' tests; an untagged helper would be flagged unused on Linux. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gle enum Address Copilot review: - Load wlanapi.dll via windows.NewLazySystemDLL (system directory) instead of syscall.NewLazyDLL (default search order), preventing DLL search-order hijacking when the process runs from a writable location. - Add a distinct tls_trusted_root_ca_sha1 column. Windows now stores the configured trusted-root-CA thumbprints there instead of overloading tls_server_certificate_sha1 (which macOS fills with the actual presented server-cert chain) — the two had different meanings across platforms. - Avoid the double WlanEnumInterfaces on unconstrained queries: windowsBackend caches both the info map and ordered names in its per-generation snapshot and exposes interfaceNames() via a new optional interfaceLister; interfacesToQuery sources the default list from the backend's snapshot when available, falling back to the package defaultInterfaces() otherwise. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gate - Capture the WLAN init failure reason in wlanInitErr (DLL load / missing proc / WlanOpenHandle) and surface it from unavailableBackend.GetStatus, instead of the misleading fixed "wlanapi.dll not available" message. errors.Is( ErrBackendUnavailable) still holds and the underlying cause is wrapped. - Makefile: compute UNAME_S := $(shell uname -s 2>/dev/null) once with a safe empty default and gate the darwin binary builds on it, rather than calling $(shell uname) inline (which fails where uname is unavailable). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "no constraint" / LIKE cases called the package defaultInterfaces(), which on Windows performs real WLAN enumeration — tying the unit test to host networking state. Add a stubLister backend (implements interfaceLister with a fixed name slice) and assert interfacesToQuery prefers backend-provided defaults, plus separate deterministic cases for empty (query nothing) and nil (en0-en9 fallback) defaults. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e pass - interfacesToQuery: consult the backend's interfaceLister FIRST and only call the package defaultInterfaces() when the backend doesn't implement it. Previously defaultInterfaces() was always called before the lister check, so on Windows it still enumerated twice — defeating the round's earlier goal. - WLAN profile XML: replace the four separate extractors (each of which built a new decoder and scanned the whole document) with a single-pass parseWLANProfile that collects EAP type, inner EAP type, authMode, and trusted-root-CA thumbprints in one token pass. GetStatus now parses once. Renamed the tests accordingly; same cases, verified identical results. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…le cleanup - readEapMethodType now consumes through the matching </EapMethod> (capturing the first <Type>) instead of returning mid-element, so a nested <EapMethod> can't be miscounted as the second method and the single-pass scan stays aligned. Verified the parser tests are unchanged. - formatSHA1Hex returns "" early for empty input and sizes Grow() to the actual input length instead of a fixed 59 bytes, avoiding allocations on the empty/short/invalid cases. - bazel_to_builddir.sh: install a `trap 'rm -f "$err"' RETURN` so the temp stderr file is removed on every return path (including errors added later / abrupt exit under set -e), replacing the scattered manual rm -f calls. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the function-level RETURN trap (which is global in bash, persists after the function returns, and doesn't fire on a set -e abort) with a subshell that owns its own EXIT trap. The trap fires on every exit path — normal, set -e, or signal — removes the temp file, and never touches the parent shell's global traps. Verified end-to-end: the script copies all five binaries with no leftover temp files and no global trap left installed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… comment - Compute host_os := uname -s (stderr suppressed, failure tolerated via `|| true`) once and gate the Darwin copy on it, so `set -euo pipefail` doesn't abort the Linux/Windows artifact copies in environments where uname is unavailable (matching the Makefile's UNAME_S approach). - Update the root BUILD.bazel comment that still referenced the old `ifeq ($(shell uname),Darwin)` gating to reflect the current `ifeq ($(UNAME_S),Darwin)` / `[ "$host_os" = "Darwin" ]` checks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@grahamgilbert this one is ready for review and merge! |
|
Hey @headmin, as Graham is unavailable to merge this for another 2 months or so, he suggested that you may be able to review and merge it. Let me know if that's possible and if you need anything from me to get it done. I'm available on the MacAdmins Slack if more convenient to talk there. Thank you! |
|
Has this been testing on an actual Windows device which is authenticated via 802.1x? I don't see any real world testing in the plan. |
Summary
Adds the
dot1xosquery table to expose per-interface 802.1X / EAPOL supplicant state on macOS and Windows, enabling fleet-wide queries like "which machines are/aren't authenticated to enterprise Wi-Fi" and diagnosis of EAP failures without remoting into individual devices.Motivation
osquery's built-in
wifi_statusshows SSID/security_type but cannot distinguish "authenticated" from "associated-but-EAP-held," and exposes no EAP method, auth state, or failure reason. This table fills that gap — real-world motivation: a multi-hour 802.1X triage that this table would have short-circuited.Data Sources
macOS
EAPOLControlCopyStateAndStatus()from/System/Library/PrivateFrameworks/EAP8021X.framework— a public-stable API from Apple's open-source eap8021x. Proven to compile/run as a normal user (no root). All status dictionary keys map to documentedkEAPOLControl*andkEAPClientProp*constants from EAPOLControlTypes.h and EAPClientProperties.h.Windows
Native Wifi API (
wlanapi.dll) viasyscall.NewLazyDLL— pure Go, no cgo. UsesWlanOpenHandle,WlanEnumInterfaces,WlanQueryInterface, andWlanGetProfileto query wireless adapter state and parse WLAN profile XML for EAP configuration. Gracefully degrades whenwlanapi.dllis unavailable (e.g. Windows Server without wireless).Columns (23)
interfacestatestate_namesupplicant_statesupplicant_state_nameeap_typeeap_type_nameinner_eap_typeinner_eap_type_nameclient_statusdomain_specific_errorauthenticator_mac_addressmodemode_nametls_session_was_resumedtls_server_certificate_chaintls_server_certificate_sha1tls_server_certificate_serialstls_trust_client_statustls_negotiated_protocol_versiontls_negotiated_cipherlast_status_timestampunique_identifierDesign
macOS
//go:build darwinand a no-op stub for non-darwin. EAP8021X itself is loaded viadlopen/dlsym, so only CoreFoundation is link-time.apple_support1.24.5 in WORKSPACE;cgo=Trueandpure=offon darwingo_binarytargets.crypto/x509with full RDN to RFC4514 LDAP rendering, SHA-1 fingerprinting, and serial number extraction.Windows
syscall.NewLazyDLL("wlanapi.dll")with lazy proc loading — gracefully returnsErrBackendUnavailableif the DLL or any proc is missing.WlanEnumInterfacesdiscovers real wireless adapter names (no hardcoded en0–en9).WlanGetProfilereturns UTF-16 XML; string-based extraction of<EapMethod><Type>,<authMode>, the nested inner EAP type, and<TrustedRootCA>.Shared
Dot1XBackendinterface:GetStatus(ifname string) (Dot1XStatus, error)with platform-specific implementations behind build tags.WHERE interface = '...'for exact-match; otherwise platform-specific enumeration with en0–en9 fallback.fakeBackendmock and table-driven tests on every platform.Testing
All tests pass with no regressions across
bazel test //...:ErrBackendUnavailable).extractEAPTypeFromXML,extractAuthModeFromXML,extractInnerEAPTypeFromXML,extractTrustedRootCAFromXML),mapWlanState, GUID/UTF-16 helpers, plus mock-backend and live smoke tests.