Skip to content

Instantly share code, notes, and snippets.

@jlmalone
Created June 17, 2025 22:56
Show Gist options
  • Save jlmalone/78c10e98e9036752dc87f6b208bb69e9 to your computer and use it in GitHub Desktop.
Save jlmalone/78c10e98e9036752dc87f6b208bb69e9 to your computer and use it in GitHub Desktop.
Find Devices on Network with Special emphasis of Macs and Mac Minis on a local network
#!/bin/bash
#
# find_macs.sh - Local Network Mac & Mac Mini Scanner
#
# Author: Joseph Malone / jlmalone
# Date: 2025-06-18
# Version: 1.0
#
# Description:
# This script scans the local network to identify all active devices.
# It uses nmap, Bonjour (mDNS), and ARP lookups to gather information.
# Its primary goal is to make an educated guess on which devices are
# Apple Macs or Mac Minis based on their MAC address, hostname,
# and advertised services.
#
# Usage:
# Run with administrator privileges:
# sudo ./find_macs.sh
#
# For detailed help and explanation:
# ./find_macs.sh --help
#
# Dependencies:
# - macOS (relies on 'route', 'ifconfig', and 'dns-sd')
# - nmap (must be installed, e.g., 'brew install nmap')
#
################################################################################
# --- Help Function ---
# Provides detailed information to the user on how the script works.
show_help() {
cat << EOF
Mac & Mac Mini Network Scanner (v1.1)
This script scans your local network to find active devices and makes an
educated guess to identify Apple Mac and Mac Mini computers.
USAGE:
sudo ./find_macs.sh
./find_macs.sh -h | --help
DESCRIPTION:
The script performs several steps to identify devices:
1. Finds your computer's local subnet (e.g., 192.168.1.0/24).
2. Uses 'nmap' to perform a fast "ping scan" to find all active IP addresses.
3. Uses 'dns-sd' (Bonjour) to listen for Macs advertising SSH services.
4. For each active IP, it uses 'arp' and 'host' to find its MAC address
and hostname.
5. It then analyzes this information to flag potential Macs.
HOW IT IDENTIFIES MACS:
A device is considered a likely Mac if:
- Its MAC address starts with a prefix registered to Apple, Inc.
- Its hostname contains "Mac" (e.g., "joes-macbook-pro.local").
- It is discovered via Bonjour advertising an SSH service.
A device is a strong candidate for a Mac Mini if its hostname also
contains "Mini".
REQUIREMENTS:
- macOS: The script is designed for macOS and uses specific commands.
- nmap: A powerful network scanner. Install with Homebrew:
brew install nmap
- sudo: Administrator privileges are required for the 'arp' command to
reliably retrieve MAC addresses for all devices on the network.
EOF
}
# --- Argument Parsing ---
# If the user asks for help, show it and exit immediately.
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
show_help
exit 0
fi
# Use "strict mode" to make the script more robust.
set -euo pipefail
# --- Configuration & Setup ---
C_GREEN=$(tput setaf 2)
C_YELLOW=$(tput setaf 3)
C_BOLD=$(tput bold)
C_RESET=$(tput sgr0)
# --- Pre-flight Checks ---
# 1. Check for root privileges.
if [[ $EUID -ne 0 ]]; then
echo "This script uses the 'arp' command for reliable MAC address lookups."
echo "Please run with sudo: ${C_BOLD}sudo $0${C_RESET}"
exit 1
fi
# 2. Check if nmap is installed.
if ! command -v nmap &> /dev/null; then
echo "Error: nmap is not installed. Install it with 'brew install nmap'."
exit 1
fi
# --- Network Discovery ---
echo "Determining active network..."
# Get the active network interface and subnet.
interface=$(route -n get default | grep 'interface:' | awk '{print $2}')
if [ -z "$interface" ]; then
echo "Error: No active network interface found."
exit 1
fi
subnet=$(ifconfig "$interface" | grep 'inet ' | awk '{print $2}' | cut -d. -f1-3).0/24
if [ -z "$subnet" ]; then
echo "Error: Could not determine subnet."
exit 1
fi
echo "Scanning network subnet: $subnet"
# --- Host Scanning ---
# Create temporary files that we will clean up automatically on exit.
HOSTS_FILE=$(mktemp)
BONJOUR_FILE=$(mktemp)
trap 'rm -f "$HOSTS_FILE" "$BONJOUR_FILE"' EXIT
# 1. Perform a quick ping scan to find active hosts.
echo "Finding active hosts with nmap..."
nmap -sn "$subnet" -oG - | grep "Up" | awk '{print $2}' > "$HOSTS_FILE"
# 2. Use dns-sd to find macOS devices advertising SSH (via Bonjour/mDNS).
echo "Scanning for macOS devices with SSH via Bonjour (6 seconds)..."
dns-sd -B _ssh._tcp local > "$BONJOUR_FILE" 2>/dev/null &
dns_sd_pid=$!
sleep 6
kill "$dns_sd_pid" 2>/dev/null
wait "$dns_sd_pid" 2>/dev/null # Suppress "Terminated" message
# --- Analysis and Reporting ---
echo
echo "${C_BOLD}Active Devices Found:${C_RESET}"
echo "---------------------"
if [ ! -s "$HOSTS_FILE" ]; then
echo "No active hosts found on the network."
exit 0
fi
# Iterate through discovered hosts.
while IFS= read -r ip; do
# Get MAC address and hostname from the ARP table.
arp_info=$(arp -n "$ip" | grep -v 'incomplete')
mac=$(echo "$arp_info" | awk '{print $4}' | grep -E '([0-9a-f]{1,2}:){5}[0-9a-f]{1,2}')
hostname_from_arp=$(echo "$arp_info" | awk '{print $2}' | grep -v '?')
hostname=${hostname_from_arp:-"unknown"}
# Check Bonjour for a more definitive macOS hostname.
bonjour_hostname=$(grep " $ip\.$" "$BONJOUR_FILE" | awk '{print $7}' | head -n 1)
if [[ -n "$bonjour_hostname" ]]; then
hostname="$bonjour_hostname.local"
fi
echo "${C_BOLD}IP: $ip${C_RESET}"
if [[ -n "$mac" ]]; then
echo "MAC: $mac"
fi
if [[ "$hostname" != "unknown" ]]; then
echo "Hostname: $hostname"
fi
# --- Mac Identification Logic ---
is_mac=0
is_mac_mini=0
# 1. Check MAC address against known Apple Organizationally Unique Identifiers (OUIs).
# Source: https://gitlab.com/wireshark/wireshark/-/raw/master/manuf
if [[ -n "$mac" ]]; then
mac_prefix=$(echo "$mac" | cut -c 1-8 | tr '[:lower:]' '[:upper:]')
case "$mac_prefix" in
"00:03:93"|"00:05:02"|"00:0A:27"|"00:0A:95"|"00:0D:93"|"00:10:FA"|"00:11:24"|"00:14:51"|"00:16:CB"|"00:17:F2"|"00:19:E3"|"00:1B:63"|"00:1C:B3"|"00:1D:4F"|"00:1E:52"|"00:1E:C2"|"00:1F:5B"|"00:1F:F3"|"00:21:E9"|"00:22:41"|"00:23:12"|"00:23:32"|"00:23:6C"|"00:23:DF"|"00:24:36"|"00:25:00"|"00:25:4B"|"00:25:BC"|"00:26:08"|"00:26:4F"|"00:26:B0"|"00:26:BB"|"00:3E:E1"|"00:A0:40"|"00:C6:10"|"00:F4:B9"|"04:0C:CE"|"04:1E:64"|"04:26:05"|"04:48:9A"|"04:54:53"|"04:DB:56"|"04:E5:36"|"08:00:07"|"08:74:02"|"0C:1A:10"|"0C:30:21"|"0C:4D:E9"|"0C:74:C2"|"10:1C:0C"|"10:40:F3"|"10:9A:DD"|"14:10:9F"|"14:5A:05"|"14:8F:C6"|"14:99:E2"|"18:20:32"|"18:34:51"|"18:81:0E"|"18:9E:FC"|"18:AF:61"|"18:AF:8F"|"1C:1A:C0"|"1C:36:BB"|"1C:5C:F2"|"1C:91:4F"|"20:78:F0"|"20:C9:D0"|"24:1E:EB"|"24:A0:74"|"24:A2:E1"|"28:0B:5C"|"28:37:37"|"28:5A:EB"|"28:6A:BA"|"28:CF:DA"|"28:E0:2C"|"28:E7:CF"|"28:F0:76"|"2C:29:97"|"2C:61:F6"|"2C:BE:08"|"2C:F0:A2"|"30:10:E4"|"30:35:AD"|"30:63:6B"|"30:90:AB"|"30:F7:C5"|"34:12:98"|"34:36:3B"|"34:51:C9"|"34:A8:4E"|"34:C0:59"|"34:E2:FD"|"38:0F:4A"|"38:4F:F0"|"38:60:77"|"38:71:DE"|"38:86:A5"|"38:B5:4D"|"3C:07:54"|"3C:2F:3A"|"3C:AB:8E"|"3C:D0:F8"|"3C:E0:72"|"40:3C:FC"|"40:98:AD"|"40:A6:D9"|"40:D3:2D"|"44:00:10"|"44:2A:60"|"44:48:9A"|"44:D8:84"|"44:F4:59"|"48:3B:38"|"48:43:7C"|"48:60:BC"|"48:74:6E"|"48:A1:C5"|"48:D7:05"|"4C:32:75"|"4C:57:CA"|"4C:7C:5F"|"50:32:37"|"50:EA:D6"|"54:26:96"|"54:4E:90"|"54:72:4F"|"54:9F:13"|"54:AE:27"|"54:E4:3A"|"58:1F:AA"|"58:40:4E"|"58:55:CA"|"58:B0:35"|"5C:09:47"|"5C:59:48"|"5C:8D:4E"|"5C:96:9D"|"5C:97:F3"|"5C:A8:6A"|"5C:F5:DA"|"5C:F9:38"|"60:03:08"|"60:19:71"|"60:30:D4"|"60:67:20"|"60:92:17"|"60:C5:47"|"60:D9:C7"|"60:F8:1D"|"60:FA:CD"|"64:31:50"|"64:5A:04"|"64:7B:D3"|"64:A3:CB"|"64:A5:C3"|"64:B9:E8"|"68:3E:34"|"68:5B:35"|"68:64:4B"|"68:96:7B"|"68:A8:6D"|"68:D9:3C"|"68:DB:CA"|"6C:29:92"|"6C:40:08"|"6C:70:9F"|"6C:94:F8"|"6C:C9:22"|"70:11:24"|"70:3E:AC"|"70:56:81"|"70:70:0D"|"70:A2:B3"|"70:CD:60"|"70:DE:E2"|"70:F0:87"|"74:81:14"|"74:E1:A2"|"78:31:C1"|"78:3A:84"|"78:6C:1C"|"78:7E:61"|"78:88:6A"|"78:A3:E4"|"78:CA:39"|"7C:04:D0"|"7C:11:BE"|"7C:6D:62"|"7C:C3:A1"|"7C:C5:37"|"7C:D1:C3"|"7C:F0:5F"|"80:00:6E"|"80:49:71"|"80:92:9F"|"80:E6:50"|"80:EA:96"|"84:29:99"|"84:38:35"|"84:78:8B"|"84:85:0A"|"84:8E:0C"|"84:B1:97"|"84:FC:FE"|"88:1F:A1"|"88:63:DF"|"88:66:5A"|"88:C6:63"|"8C:00:6D"|"8C:29:37"|"8C:58:77"|"8C:7B:9D"|"8C:85:90"|"8C:87:32"|"8C:FA:BA"|"90:72:40"|"90:84:0D"|"90:B2:1F"|"90:DD:5D"|"94:E9:6A"|"98:00:C6"|"98:01:A7"|"98:03:D8"|"98:46:0A"|"98:5A:EB"|"98:9E:63"|"98:B8:E3"|"98:D6:BB"|"98:E0:D9"|"98:F0:AB"|"9C:04:EB"|"9C:20:7B"|"9C:35:EB"|"9C:F3:87"|"A0:3B:E3"|"A0:4E:A7"|"A0:99:9B"|"A0:ED:CD"|"A4:5E:60"|"A4:B1:97"|"A4:E9:75"|"A8:20:66"|"A8:5C:2C"|"A8:60:B6"|"A8:66:7F"|"A8:86:DD"|"A8:88:08"|"A8:96:8A"|"A8:BB:C5"|"A8:FA:D8"|"AC:18:26"|"AC:3C:0B"|"AC:61:EA"|"AC:87:A3"|"AC:BC:32"|"AC:E3:42"|"AC:FD:EC"|"B0:34:95"|"B0:65:BD"|"B0:9F:BA"|"B0:C0:90"|"B4:43:0D"|"B4:F0:AB"|"B8:09:8A"|"B8:16:19"|"B8:44:D9"|"B8:53:AC"|"B8:63:4D"|"B8:78:2E"|"B8:8D:12"|"B8:C7:5D"|"B8:E8:56"|"B8:F6:B1"|"BC:3B:AF"|"BC:4C:C4"|"BC:52:B7"|"BC:67:A6"|"BC:9F:EF"|"C0:1A:DA"|"C0:63:94"|"C0:84:7A"|"C0:B6:58"|"C0:CE:CD"|"C4:2C:03"|"C4:61:8B"|"C8:1F:77"|"C8:2A:14"|"C8:33:4B"|"C8:6F:1D"|"C8:85:50"|"C8:B5:B7"|"C8:D0:83"|"CC:08:E0"|"CC:20:E8"|"CC:29:F5"|"CC:44:63"|"CC:78:5F"|"CC:C7:60"|"D0:03:4B"|"D0:23:DB"|"D0:4F:7E"|"D0:81:7A"|"D0:A6:37"|"D0:D2:B0"|"D0:E1:82"|"D4:61:2E"|"D4:61:9D"|"D4:A3:3D"|"D8:00:4D"|"D8:1D:72"|"D8:30:62"|"D8:96:95"|"D8:A7:56"|"D8:D1:CB"|"DC:08:0F"|"DC:2B:2A"|"DC:86:D8"|"DC:A9:04"|"E0:33:8E"|"E0:5F:45"|"E0:75:7D"|"E0:AC:CB"|"E0:B9:BA"|"E0:C9:7A"|"E0:F5:C6"|"E4:2B:34"|"E4:30:22"|"E4:50:EB"|"E4:8B:7F"|"E4:98:D6"|"E4:B2:FB"|"E4:C6:3D"|"E8:04:0B"|"E8:06:88"|"E8:80:2E"|"EC:35:86"|"EC:85:2F"|"F0:18:98"|"F0:24:75"|"F0:78:16"|"F0:98:9D"|"F0:99:BF"|"F0:B4:79"|"F0:C1:F1"|"F0:D1:A9"|"F0:DC:E2"|"F4:1B:A1"|"F4:37:B7"|"F4:F1:5A"|"F4:F9:51"|"F8:27:93"|"F8:38:80"|"F8:4D:89"|"F8:D0:BD"|"F8:E0:79"|"FC:1B:1F"|"FC:25:3F"|"FC:D8:48")
is_mac=1
;;
esac
fi
# 2. Check hostname for common Mac patterns.
if [[ "$hostname" =~ [Mm]ac ]]; then
is_mac=1
if [[ "$hostname" =~ [Mm]ini ]]; then
is_mac_mini=1
fi
fi
# --- Final Verdict ---
if [[ $is_mac -eq 1 ]]; then
echo "${C_GREEN}Verdict: Likely a Mac device${C_RESET}"
if [[ $is_mac_mini -eq 1 ]]; then
echo "${C_YELLOW}Note: Strong candidate for Mac Mini (hostname contains 'Mini')${C_RESET}"
fi
fi
echo "---------------------"
done < "$HOSTS_FILE"
echo "Scan complete."