Scan your network and alert of new unknown clients

I’ve been looking for a way to scan my home network and alert me via email when a new device, by MAC address, is seen on my network. At one point I tried completing this with a shell script on my server using a flatfile database of known MAC addresses but either 1.) my shell-script-fu wasn’t strong enough to get the job done or 2.) I needed something more powerful, like python. Enter python.
Working at NASA I am surrounded by some incredibly intelligent people. So I decided to ask one of my co-workers to assist me with writing a python script. This is what we came up with. You can find his original script here: https://github.com/AdamFSU/Scripts
Here is what I worked out.
I have a CentOS 7 server running a postfix smtp relay. Here is the configuration for postfix I took from a RHCE 7 class I took on Linux Academy:

  1. Install necessary packages

yum install postfix cyrus-sasl cyrus-sasl-plain –y

  1. Edit the config: vim /etc/postfix/main.cf

relayhost = [smtp.mailserver.com]

example: relayhost = [smtp.gmail.com]:587

smtp_use_tls = yes
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_tls_CAfile = /etc/ssl/certs/ca-bundle.crt
smtp_sasl_security_options = noanonymous
smtp_sasl_tls_security_options = noanonymous

inet_protocols = ipv4
inet_interfaces = loopback-only

mynetworks = 127.0.0.0/8
myorigin = $myhostname
mydestination =
local_transport = error: local delivery disabled

create a sasl password file specifically for Gmail:

cd /etc/postfix

vim sasl_passwd

[smtp.gmail.com]:587 username@gmail.com:password

Finish configuration of sasl password:

postmap sasl_passwd
chown root:postfix sasl_passwd
chmod 640 sasl_passwd
postmap sasl_passwd

  1. Enable and start necessary services

systemctl enable postfix && systemctl start postfix

Now, here are the additional requirements for the script:

Prerequisites:

  1. Required Python packages: python-nmap – To install run: python3 -m pip install python-nmap
  2. The $PYTHONPATH environment variable must be set: example: export PYTHONPATH=/usr/lib/python3/dist-packages/ For CentOS added to root’s .bashrc: export PYTHONPATH=/usr/local/lib/python3.6/site-packages/
  3. This script also uses the mail utility for sending email alerts – For CentOS: install mailx
    yum install mailx -y
  4. Flatfile called master_mac.txt with listing of known MAC addresses on the LAN
  5. The nmap utility
    yum install nmap -y

Here is the python script:

#!/usr/bin/python3

# *** NOTE: THIS SCRIPT MUST BE RUN AS ROOT ***

# python module imports
import os
import shutil
import nmap
import sys
import subprocess
from datetime import datetime

# variable containing the filepath of the nmap scan results file
mac_list = os.environ['HOME'] + "/maclist.txt"

# variable containing the filepath of the approved mac addresses on the LAN file
masterfile = os.environ['HOME'] + "/master_mac.txt"

# If a file from previous nmap scans exists, create a backup of the file
if os.path.exists(mac_list):
    shutil.copyfile(mac_list, os.environ['HOME'] + '/maclist_' + datetime.now().strftime("%Y_%m_%H:%M") + '.log.bk')

# Open a new file for the new nmap scan results
f = open(mac_list, "w+")

# Print to console this warning
print("Don't forget to update your network in the nmap scan of this script")

# This block of code will scan the network and extract the IP and MAC address
# Create port scanner object, nm
nm = nmap.PortScanner()
# Perform: nmap -oX - -n -sn 192.168.0.1/24
# *** NOTE: -sP has changed to -sn for newer versions of nmap! ***
# Change this to your network IP range
nm.scan(hosts='192.168.0.1/24', arguments='-n -sn')
# Retrieve results for all hosts found
nm.all_hosts()
# For every host result, write their mac address and ip to the $mac_list file
for host in nm.all_hosts():
    # If 'mac' expression is found in results write results to file
    if 'mac' in nm[host]['addresses']:
        f.write(str(nm[host]['addresses']) + "\n")
    # If 'mac' expression isn't found in results print warning
    elif nm[host]['status']['reason'] != "localhost-response":
        print("MAC addresses not found in results, make sure you run this script as root!")

# Close file for editing
f.close()
# Open file for reading
f = open(mac_list, "r")
# Read each line of file and store it in a list
mac_addresses = f.read().splitlines()
# Close file for reading
f.close()
# Open masterfile for reading
if os.path.isfile(masterfile):
    f2 = open(masterfile, "r")
else:
    print("Could not find master_mac file! Verify file path is correct!")
    sys.exit()

# Read each line of file and store it in a list
master_mac_addresses = f2.read().splitlines()
# Close file for reading
f2.close()

# Create empty list of new devices found on network
new_devices = []

# For every list entry in the mac_addresses list
for i in mac_addresses:
    # Convert the list index from a string to a dictionary so we can parse out mac address for comparison
    dic = eval(i)
    # Compare mac address portion of dictionary to mac addresses in the master_mac_addresses file
    # If the scanned mac address is not in the master_mac_addresses file
    if dic['mac'] not in master_mac_addresses:
        # Add scanned mac address to new devices list
        new_devices.append(dic)

# If the new_devices list isn't empty
if len(new_devices) != 0:
    # output a warning to the console
    warning = "\nWARNING!! NEW DEVICE(S) ON THE LAN!! - UNKNOWN MAC ADDRESS(ES): " + str(new_devices) + "\n"
    print(warning)

    # Create email notification of the warning
    try:
        # subject of email
        subject = "WARNING, new device on LAN!"
        # content of email
        content = "New unknown device(s) on the LAN: " + str(new_devices)
        # shell process of sending email with mutt
        m1 = subprocess.Popen('echo "{content}" | mail -s "{subject}" youremail@gmail.com'.format(
            content=content, subject=subject), shell=True)
        # output whether email was successful in sending or not
        print(m1.communicate())

    # if sending of the email fails, this will output why
    except OSError as e:
        print("Error sending email: {0}".format(e))
    except subprocess.SubprocessError as se:
        print("Error sending email: {0}".format(se))

Next is to make sure your Gmail is setup correctly by making sure to enable less secure app access in Google account security. Then setup a crontab under root that looks something like this:

#scan the network every 10 minutes
*/10 * * * * /root/bin/nmapscan_wo_log.py > /dev/null 2>&1