Skip to content

Instantly share code, notes, and snippets.

@pelwell
Created September 30, 2026 09:22
Show Gist options
  • Select an option

  • Save pelwell/7712f2f734748e1aa36eb4b1919f72b5 to your computer and use it in GitHub Desktop.

Select an option

Save pelwell/7712f2f734748e1aa36eb4b1919f72b5 to your computer and use it in GitHub Desktop.
#!/bin/bash
# wifi-sec-info - summarise Wi-Fi security capabilities and the current connection
#
# Pulls together iw, wpa_cli, nmcli and iwlist output to show:
# - the authentication mechanisms/features each local Wi-Fi interface supports
# - the AP each interface is connected to, and what that AP advertises
# - the authentication mechanism actually used for the current connection
#
# Usage: wifi-sec-info [interface...] (default: all wireless interfaces)
#
# Root is not required if the user is in the netdev group (for wpa_cli).
PATH=$PATH:/usr/sbin:/sbin
have() { command -v "$1" >/dev/null 2>&1; }
hdr() { printf '\n=== %s ===\n' "$*"; }
sub() { printf '\n -- %s --\n' "$*"; }
kv() { [ -n "$2" ] && printf ' %-24s %s\n' "$1:" "$2"; }
yn() { if eval "$2"; then kv "$1" yes; else kv "$1" no; fi; }
has() { grep -qE -- "$1" <<<"$2"; }
# Join words onto one line (xargs would choke on quotes, e.g. EAP method AKA')
words() { tr -s ' \t\n' ' ' | sed 's/^ //; s/ $//'; }
T=$'\t'
if ! have iw; then
echo "iw not found - install the 'iw' package" >&2
exit 1
fi
if [ $# -gt 0 ]; then
IFACES="$*"
else
IFACES=$(for d in /sys/class/net/*; do
[ -e "$d/phy80211" ] && basename "$d"
done)
fi
if [ -z "$IFACES" ]; then
echo "No wireless interfaces found" >&2
exit 1
fi
# Print the value of field $2 from IE section $1 (RSN or WPA) of an iw BSS dump
ie_field() {
awk -v s="$1" -v f="$2" '
$0 ~ "^\t" s ":" { p = 1 }
p && /^\t[^\t]/ && $0 !~ "^\t" s ":" { p = 0 }
p {
l = $0
sub(/^[ \t]*([A-Z]+:)?[ \t]*\* /, "", l)
if (index(l, f ": ") == 1) { print substr(l, length(f) + 3); exit }
}'
}
# Print the items of an iw phy list block whose header matches $1
phy_list() {
awk -v h="$1" '
$0 ~ "^\t" h { p = 1; next }
p && /^\t[^\t]/ { exit }
p && /\* / { sub(/^[ \t]*\* */, ""); print }'
}
# Translate an RSN AKM suite name to a friendly description
akm_desc() {
case "$1" in
PSK) echo "WPA2-Personal (PSK)" ;;
PSK/SHA-256) echo "WPA2-Personal (PSK-SHA256)" ;;
FT/PSK) echo "WPA2-Personal with Fast Transition" ;;
SAE) echo "WPA3-Personal (SAE)" ;;
FT/SAE) echo "WPA3-Personal with Fast Transition" ;;
SAE-EXT-KEY|00-0f-ac:24) echo "WPA3-Personal (SAE-EXT-KEY)" ;;
FT/SAE-EXT-KEY|00-0f-ac:25) echo "WPA3-Personal (FT SAE-EXT-KEY)" ;;
"IEEE 802.1X") echo "WPA2-Enterprise (802.1X)" ;;
"IEEE 802.1X/SHA-256") echo "WPA2/3-Enterprise (802.1X-SHA256)" ;;
"FT/IEEE 802.1X") echo "Enterprise with Fast Transition" ;;
"IEEE 802.1X/SUITE-B") echo "WPA3-Enterprise Suite-B" ;;
"IEEE 802.1X/SUITE-B-192") echo "WPA3-Enterprise 192-bit" ;;
"FT/IEEE 802.1X/SHA-384") echo "Enterprise 192-bit with Fast Transition" ;;
OWE) echo "Enhanced Open (OWE)" ;;
FILS*) echo "FILS ($1)" ;;
*) echo "$1" ;;
esac
}
# Translate a wpa_supplicant key_mgmt status value to a friendly description
keymgmt_desc() {
case "$1" in
WPA-PSK) echo "WPA1-Personal (PSK) - legacy" ;;
WPA2-PSK) echo "WPA2-Personal (PSK)" ;;
WPA2-PSK-SHA256) echo "WPA2-Personal (PSK-SHA256)" ;;
FT-PSK) echo "WPA2-Personal with Fast Transition" ;;
SAE) echo "WPA3-Personal (SAE)" ;;
FT-SAE) echo "WPA3-Personal with Fast Transition" ;;
SAE-EXT-KEY) echo "WPA3-Personal (SAE-EXT-KEY)" ;;
*SUITE-B-192*) echo "WPA3-Enterprise 192-bit" ;;
*SUITE-B*) echo "WPA3-Enterprise Suite-B" ;;
WPA/IEEE\ 802.1X/EAP) echo "WPA1-Enterprise (802.1X) - legacy" ;;
*802.1X*|*EAP*) echo "WPA2/3-Enterprise (802.1X)" ;;
OWE) echo "Enhanced Open (OWE)" ;;
NONE) echo "Open / WEP (no key management)" ;;
*) echo "$1" ;;
esac
}
sae_group_desc() {
case "$1" in
19) echo "19 (NIST P-256)" ;;
20) echo "20 (NIST P-384)" ;;
21) echo "21 (NIST P-521)" ;;
*) echo "$1" ;;
esac
}
# Decode the first octet of an RSNX element (hex string)
rsnx_desc() {
local v=$((16#$1)) out=""
((v & 0x10)) && out+="Protected-TWT "
((v & 0x20)) && out+="SAE-H2E "
((v & 0x40)) && out+="SAE-PK "
((v & 0x80)) && out+="Protected-WUR "
echo "${out% }" | sed "s/^$/none/"
}
declare -A PHY_DONE
for IF in $IFACES; do
if [ ! -e "/sys/class/net/$IF/phy80211" ]; then
echo "$IF: not a wireless interface" >&2
continue
fi
PHY=$(basename "$(readlink -f "/sys/class/net/$IF/phy80211")")
DRV=$(basename "$(readlink -f "/sys/class/net/$IF/device/driver")" 2>/dev/null)
PHYINFO=$(iw phy "$PHY" info 2>/dev/null)
IFINFO=$(iw dev "$IF" info 2>/dev/null)
hdr "Interface $IF ($PHY, driver ${DRV:-unknown})"
kv "MAC address" "$(cat "/sys/class/net/$IF/address")"
kv "Mode" "$(awk '$1 == "type" { $1 = ""; print substr($0, 2) }' <<<"$IFINFO")"
# ---- Local capabilities (driver/hardware and supplicant) ----
CIPHERS=$(phy_list "Supported Ciphers" <<<"$PHYINFO" | awk '{ print $1 }' | words)
EXTFEAT=$(phy_list "Supported extended features" <<<"$PHYINFO" |
sed -n 's/^\[ \(.*\) \].*/\1/p' | words)
AUTHFEAT=$(tr ' ' '\n' <<<"$EXTFEAT" |
grep -E 'SAE|4WAY|BEACON_PROT|OWE|FILS|MFP|CONTROL_PORT|AKM|SECURE|PROT' | words)
DRV_SAE=
if grep -q "Device supports SAE with AUTHENTICATE" <<<"$PHYINFO"; then
DRV_SAE="host SAE (AUTHENTICATE cmd)"
fi
has 'SAE_OFFLOAD( |$)' "$EXTFEAT" && DRV_SAE="${DRV_SAE:+$DRV_SAE, }SAE offload"
if [ -z "${PHY_DONE[$PHY]}" ]; then
PHY_DONE[$PHY]=1
sub "Local hardware/driver capabilities ($PHY)"
kv "Ciphers" "$CIPHERS"
kv "SAE support" "${DRV_SAE:-none advertised}"
kv "Auth-related features" "$AUTHFEAT"
AKMS=$(phy_list "Supported AKM suites" <<<"$PHYINFO" | words)
kv "Driver AKM suites" "$AKMS"
kv "Interface modes" "$(phy_list "Supported interface modes" <<<"$PHYINFO" | words)"
awk '
function flush() {
if (band == "") return
s = "802.11" (bn == "2.4 GHz" ? "b/g" : "a")
if (ht) s = s "/n"
if (vht) s = s "/ac"
if (he) s = s "/ax"
if (eht) s = s "/be"
gen = eht ? "Wi-Fi 7" : he ? (bn == "6 GHz" ? "Wi-Fi 6E" : "Wi-Fi 6") : \
vht ? "Wi-Fi 5" : ht ? "Wi-Fi 4" : "legacy"
printf " %-24s %s (%s)\n", "Band " bn ":", s, gen
}
/^\tBand [0-9]+:/ { flush(); band = $2; bn = ""; ht = vht = he = eht = 0; next }
/^\t[^\t]/ { flush(); band = "" }
band == "" { next }
/^\t\tCapabilities: 0x/ { ht = 1 }
/VHT Capabilities/ { vht = 1 }
/HE Iftypes/ { he = 1 }
/EHT Iftypes/ { eht = 1 }
bn == "" && /\* [0-9.]+ MHz/ {
f = $2 + 0
bn = f < 3000 ? "2.4 GHz" : f < 5925 ? "5 GHz" : f < 7200 ? "6 GHz" : "60 GHz"
}
END { flush() }' <<<"$PHYINFO"
fi
WPA_OK=
if have wpa_cli && wpa_cli -i "$IF" ping 2>/dev/null | grep -q PONG; then
WPA_OK=1
wcap() { wpa_cli -i "$IF" get_capability "$1" 2>/dev/null | words; }
KM_CAP=$(wcap key_mgmt)
# Some drivers (e.g. brcmfmac) only advertise SAE via the nl80211
# feature flag, which wpa_supplicant doesn't reflect here
if [ -n "$DRV_SAE" ] && ! has '(^| )SAE( |$)' "$KM_CAP"; then
KM_CAP="$KM_CAP (+SAE via driver)"
fi
sub "Supplicant capabilities ($(wpa_supplicant -v 2>/dev/null | awk 'NR == 1 { print $2 }'))"
kv "Key management" "$KM_CAP"
kv "Protocols" "$(wcap proto)"
kv "Pairwise ciphers" "$(wcap pairwise)"
kv "Group ciphers" "$(wcap group)"
kv "Mgmt group ciphers" "$(wcap group_mgmt)"
kv "Auth algorithms" "$(wcap auth_alg)"
kv "EAP methods" "$(wcap eap)"
fi
if have iwlist; then
WEXT=$(iwlist "$IF" auth 2>/dev/null | awk 'NR > 1 && NF { print $1 }' | words)
[ -n "$WEXT" ] && { sub "Legacy WEXT view (iwlist auth)"; kv "Auth capabilities" "$WEXT"; }
fi
sub "Security modes this device can use"
yn "WPA2-Personal" 'has "CCMP" "$CIPHERS"'
yn "WPA3-Personal (SAE)" '[ -n "$DRV_SAE" ] || has "(^| )SAE( |$)" "$KM_CAP"'
if [ -n "$WPA_OK" ]; then
yn "WPA2/3-Enterprise" 'has "WPA-EAP" "$KM_CAP"'
yn "WPA3-Ent 192-bit" 'has "GCMP-256" "$CIPHERS" && has "SUITE-B-192" "$KM_CAP"'
yn "Enhanced Open (OWE)" 'has "OWE" "$KM_CAP" && has "CMAC|GMAC" "$CIPHERS"'
fi
yn "PMF / 802.11w" 'has "CMAC|GMAC" "$CIPHERS"'
yn "Beacon protection" 'has "BEACON_PROTECTION" "$EXTFEAT"'
yn "Legacy TKIP/WEP" 'has "TKIP|WEP" "$CIPHERS"'
# ---- Associated AP ----
LINK=$(iw dev "$IF" link 2>/dev/null)
BSSID=$(awk '/^Connected to/ { print $3 }' <<<"$LINK")
if [ -z "$BSSID" ]; then
sub "Not connected"
continue
fi
sub "Connected AP"
kv "SSID" "$(sed -n 's/^\tSSID: //p' <<<"$LINK")"
kv "BSSID" "$BSSID"
kv "Channel" "$(sed -n 's/^\tchannel \(.*\)/\1/p' <<<"$IFINFO")"
kv "Signal" "$(sed -n 's/^\tsignal: //p' <<<"$LINK")"
kv "RX bitrate" "$(sed -n 's/^\trx bitrate: //p' <<<"$LINK")"
kv "TX bitrate" "$(sed -n 's/^\ttx bitrate: //p' <<<"$LINK")"
BSS=$(iw dev "$IF" scan dump -u 2>/dev/null |
awk -v b="$BSSID" 'tolower($0) ~ "^bss " b { p = 1; next } /^BSS / { p = 0 } p')
if [ -z "$BSS" ] && [ "$(id -u)" = 0 ]; then
# Scan results may have expired - try a fresh scan
BSS=$(timeout 20 iw dev "$IF" scan -u 2>/dev/null |
awk -v b="$BSSID" 'tolower($0) ~ "^bss " b { p = 1; next } /^BSS / { p = 0 } p')
fi
if [ -z "$BSS" ]; then
kv "Advertised security" "unknown - no scan result for this BSS (re-run as root to rescan)"
else
sub "AP advertised security and features"
RSN_AKM=$(ie_field RSN "Authentication suites" <<<"$BSS")
RSN_CAP=$(ie_field RSN "Capabilities" <<<"$BSS")
WPA_AKM=$(ie_field WPA "Authentication suites" <<<"$BSS")
if [ -n "$RSN_AKM" ]; then
kv "RSN (WPA2/3) AKMs" "$RSN_AKM"
for a in $(sed 's/IEEE 802.1X/IEEE_802.1X/g' <<<"$RSN_AKM"); do
kv " offers" "$(akm_desc "${a//_/ }")"
done
kv "RSN pairwise ciphers" "$(ie_field RSN "Pairwise ciphers" <<<"$BSS")"
kv "RSN group cipher" "$(ie_field RSN "Group cipher" <<<"$BSS")"
kv "RSN mgmt group cipher" "$(ie_field RSN "Group mgmt cipher suite" <<<"$BSS")"
if has "MFP-required" "$RSN_CAP"; then
kv "PMF (802.11w)" "required"
elif has "MFP-capable" "$RSN_CAP"; then
kv "PMF (802.11w)" "optional"
else
kv "PMF (802.11w)" "not supported"
fi
fi
if [ -n "$WPA_AKM" ]; then
kv "WPA1 AKMs (legacy)" "$WPA_AKM"
kv "WPA1 ciphers" "$(ie_field WPA "Pairwise ciphers" <<<"$BSS")"
fi
if [ -z "$RSN_AKM" ] && [ -z "$WPA_AKM" ]; then
if grep -q "^${T}capability:.*Privacy" <<<"$BSS"; then
kv "Security" "WEP (legacy)"
else
kv "Security" "Open (no encryption)"
fi
fi
RSNX=$(sed -n 's/^\tUnknown IE (244): \([0-9a-f]\{2\}\).*/\1/p' <<<"$BSS")
[ -n "$RSNX" ] && kv "RSNX (WPA3 extras)" "$(rsnx_desc "$RSNX")"
grep -qE "^${T}(Supported|Extended supported) rates:.* 61\.5\*" <<<"$BSS" &&
kv "SAE H2E only" "yes (BSS membership selector 123)"
grep -q "^${T}WPS:" <<<"$BSS" && kv "WPS" "advertised"
STD=""
grep -q "^${T}HT capabilities:" <<<"$BSS" && STD+="n "
grep -q "^${T}VHT capabilities:" <<<"$BSS" && STD+="ac "
grep -qE "^${T}HE capabilities:|Extension ID \(35\)" <<<"$BSS" && STD+="ax "
grep -qE "^${T}EHT capabilities:|Extension ID \(108\)" <<<"$BSS" && STD+="be "
kv "802.11 standards" "${STD:-legacy a/b/g only}"
EXT=""
grep -q "BSS Transition" <<<"$BSS" && EXT+="802.11v(BSS-transition) "
grep -q "RM enabled capabilities" <<<"$BSS" && EXT+="802.11k(radio-measurement) "
has "FT/" "$RSN_AKM" && EXT+="802.11r(fast-transition) "
grep -q "TWT Responder" <<<"$BSS" && EXT+="TWT "
kv "Roaming/other" "$EXT"
fi
# ---- What was actually negotiated ----
sub "Current connection - negotiated security"
if [ -n "$WPA_OK" ]; then
ST=$(wpa_cli -i "$IF" status 2>/dev/null)
get() { sed -n "s/^$1=//p" <<<"$ST"; }
KM=$(get key_mgmt)
kv "Authentication used" "$(keymgmt_desc "$KM")"
kv "key_mgmt (supplicant)" "$KM"
kv "Pairwise cipher" "$(get pairwise_cipher)"
kv "Group cipher" "$(get group_cipher)"
MGMT=$(get mgmt_group_cipher)
if [ -n "$MGMT" ]; then
kv "PMF (802.11w)" "in use ($MGMT)"
else
kv "PMF (802.11w)" "not in use"
fi
SG=$(get sae_group)
if [ -n "$SG" ]; then
kv "SAE group" "$(sae_group_desc "$SG")"
[ "$(get sae_h2e)" = 1 ] && H2E="hash-to-element (H2E)" || H2E="hunting-and-pecking"
kv "SAE PWE derivation" "$H2E"
[ "$(get sae_pk)" = 1 ] && kv "SAE-PK" "in use"
fi
kv "EAP method" "$(get selectedMethod)"
kv "EAP phase 2" "$(sed -n 's/^.*Phase2 method=//p' <<<"$ST")"
kv "EAP TLS version" "$(get eap_tls_version)"
kv "EAP state" "$(get 'EAP state')"
kv "State" "$(get wpa_state)"
fi
if have nmcli; then
CON=$(nmcli -g GENERAL.CONNECTION dev show "$IF" 2>/dev/null)
if [ -n "$CON" ]; then
kv "NM connection" "$CON"
kv "NM configured key-mgmt" \
"$(nmcli -g 802-11-wireless-security.key-mgmt con show "$CON" 2>/dev/null)"
case "$(nmcli -g 802-11-wireless-security.pmf con show "$CON" 2>/dev/null)" in
0) PMF="default (global setting)" ;;
1) PMF="disabled" ;;
2) PMF="optional" ;;
3) PMF="required" ;;
*) PMF="" ;;
esac
kv "NM configured PMF" "$PMF"
fi
fi
[ -z "$WPA_OK" ] && ! have nmcli &&
kv "Note" "wpa_supplicant control interface not reachable (try sudo)"
done
echo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment