#!/usr/bin/env python

"""
macdb

macdb is a command line utility to annotate stdin with information about any mac addresses found on stdin

it uses two files - an oui.txt, which can be obtained from http://standards-oui.ieee.org/oui.txt or an alternate
database from https://code.wireshark.org/review/gitweb?p=wireshark.git;a=blob_plain;f=manuf

the second file, a known.txt.  This textfile is something you make, and consists of lines of MAC addresses that
you know about (whitespace) and then a text string that you want associated with that address.

These files are very similar, but the oui file will be 6 hex digits and the known file will be full macs

Eli Fulkerson, April 25 2017
http://www.elifulkerson.com

"""

import sys
import optparse

import os.path

default_ouifile = "oui.txt"
default_knownfile = "known.txt"

color_reset = "\x1b[0m"
color_green = "\x1b[1;32;40m"
color_green_dim = "\x1b[2;32;40m"
color_blue = "\x1b[1;34;40m"
color_blue_dim = "\x1b[2;34;40m"


#
# So we take a string, and a length, and a bool and we...
# ... take the mac, strip out some non-data characters that are often in mac addresses,
# chop it down to the desired length and determine whether its a valid base 16 number.
#
# either return the mac string itself (so we can use it to compare against
#
def chew_potential_mac(mac,length):
   
    mac = mac.replace(':','')
    mac = mac.replace('-','')
    mac = mac.replace('.','')

    # first whatever
    mac = mac[0:length].lower()

    if len(mac) < length:
        return False

    try:
        tmp = int(mac,16)
    except:
        return False

    return mac

#
# Generic input processings with optparse...
#
def process_command_line(argv):
    """
    Return a 2-tuple: (settings object, args list).
    `argv` is a list of arguments, or `None` for ``sys.argv[1:]``.
    """
    if argv is None:
        argv = sys.argv[1:]

    # initialize the parser object:
    parser = optparse.OptionParser(
        formatter=optparse.TitledHelpFormatter(width=78),
        add_help_option=None)

    # define options here:
    parser.add_option(      # customized description; put --help last
        '-h', '--help', action='help',
        help='Show this help message and exit.')

    parser.add_option('-o', '--ouifile', action='store', dest='ouifile', help='Use specified oui textfile (default %s)' % (default_ouifile))
    parser.add_option('-k', '--knownfile', action='store', dest='knownfile', help='Use specified list of known macs (default %s)' % (default_knownfile))

    parser.add_option('-x', '--noouifile', action='store_true', dest='no_ouifile', default=False, help="Don't lookup anything in the oui textfile")
    parser.add_option('-n', '--noknownfile', action='store_true', dest='no_knownfile', default=False, help="Don't lookup anything in the known textfile")

    settings, args = parser.parse_args(argv)

    if args:
        parser.error('program takes no command-line arguments; '
                     '"%s" ignored.' % (args,))
        
    return settings, args

#
# Generic main...
#
def main(argv=None):
    settings, args = process_command_line(argv)

    # these flags are for "did we successfully read in oui/known files" checks, separate from the "do we want to use oui/known files" options.
    using_ouifile = True
    using_knownfile = True

    #
    # first, we read in our ouifile
    #

    ouidict = {}
    if settings.ouifile != None:
        ouifile = settings.ouifile
    else:
        ouifile = default_ouifile

    try:
        if os.path.exists(ouifile):
            file = open(ouifile, 'r')
        else:
            file = open(os.path.join(sys.path[0],ouifile), 'r')

        for l in file.readlines():
            l = l.strip()
        
            # we don't care about lines that can't be a mac
            if len(l) > 6:
                w = l.split()
            
                tmp = chew_potential_mac(w[0],6)
                if tmp == False:
                    continue
                else:
                    if tmp in ouidict:
                        pass
                    else:
                        ouidict[tmp] = " ".join(w[1:])

    except IOError:
        # we're going to pass, because we don't care unless both fail
        using_ouifile = False
        pass

    #
    # second, we read in our known mac file
    #
    
    knowndict = {}

    if settings.knownfile != None:
        knownfile = settings.knownfile
    else:
        knownfile = default_knownfile

    try:
        if os.path.exists(knownfile):
            file = open(knownfile, 'r')
        else:
            file = open(os.path.join(sys.path[0],knownfile), 'r')

        for l in file.readlines():
            l = l.strip()
        
            # we don't care about lines that can't be a mac
            if len(l) > 12:
                w = l.split()
            
                tmp = chew_potential_mac(w[0],12)
                if tmp == False:
                    continue
                else:
                    if tmp in knowndict:
                        pass
                    else:
                        knowndict[tmp] = " ".join(w[1:])

    except IOError:
        # we're going to pass, because we don't care unless both failed
        using_knownfile = False
        pass

    # ok this is slightly goofy - theoretically you might want to only use a knownfile or only an ouifile, but
    # without one *or* the other whats the point?  Better to output nothing than to just cat it out silently
    # in this case I think.
    if using_ouifile == False and using_knownfile == False:
        print "Error:  Neither the ouifile (%s) or the knownfile(%s) is available, aborting" % (ouifile, knownfile)
        return 1

    # Eh, let them do this though, maybe they just want to yank it out of the pipeline w/out removing the command
    # ... for some reason.  That is, this is the "files exist but we don't want to use em" option
    if settings.no_ouifile == True and settings.no_knownfile == True:
        #print "Error:  If you don't want to do any look up, just don't use me"
        pass
                        
    # now, to stdin!
    
    for line in sys.stdin:
        line_matched = False
        outbuf = ""

        for word in line.split():

            tmp = chew_potential_mac(word,12)

            if tmp != False:

                if tmp in knowndict and settings.no_knownfile == False:
                    outbuf += color_green + "Known: " + color_green_dim + knowndict[tmp] + color_reset + " "
                    line_matched = True
                
                tmp = tmp[0:6]
                if tmp in ouidict and settings.no_ouifile == False:                
                    outbuf += color_blue + "OUI: " + color_blue_dim + ouidict[tmp] + color_reset + " "
                    line_matched = True
                     
        if line_matched == False:
            print line.strip()
        else:
            print line.strip() + " " + outbuf
                
    return 0

if __name__ == '__main__':
    status = main()
    sys.exit(status)

