Newer
Older
abck / abck
#!/usr/local/bin/python
#
# abck - Part of the ABMGMT package from TundraWare Inc.
# Copyright (c) 2001, TundraWare Inc., All Rights Reserved.
# See the accompanying file called, 1-ABMGMT-License.txt
# for Licensing Terms
#
# Build a report of all unauthorized access attempts
#
# Usage: abck [-d date offset] -e [Except String] -s [Match String]

##########

VERSION = "$Id: abck,v 1.93 2001/07/19 00:03:37 tundra Exp $"



####################
# Imports
####################

import commands
import getopt
import re
import sys
import time

####################
# Booleans
####################

FALSE = 0 == 1
TRUE  = not FALSE

DONE  = FALSE

####################
# Constants And Literals
####################

ANS  = ";; ANSWER SECTION:"
AUTH = ";; AUTHORITY SECTION:"
DLEN = 24*60*60
DIG  = "dig -t ptr -x "
LOG = "/var/log/messages"
MOS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
       "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
OUT = "./ABUSERS"
PROMPT = "\nLog Record:\n%s\n\nWho Gets Message For: <%s>? %s[%s] "
WHO = "whois "

USAGE = "abck " + VERSION.split()[2] + " " + \
        "Copyright (c) 2001, TundraWare Inc.  All Rights Reserved.\n" + \
        " usage:\n" + \
        "   abck [-d # days to look back]\n" + \
        "        [-e except string]\n" + \
        "        [-m match string]\n" + \
        "        [-s Show, but do not process matching records]\n"


####################
# Data Structures
####################

# Dictionary of keywords indicating attack, and position of host address/name
# in their log records

AttackKeys = {
              "refused" : 8,
              "unauthorized" : 7
             }

# Cache dictionary of all attacking hosts discovered this run of the program

NameCache = {}

####################
# Regular Expression Handlers
####################

# Regular Expression which describes a legit IP quad address

IPQuad   = r"(\d{1,3}\.){3}\d{1,3}$"

####################
# Function Definitions
####################

# Return the ending substring of a host name with 'depth' number of dots
# in it

def HostDepth(host, depth=2):

    # Break the address down into components
    components = host.split(".")

    # And return the recombined pieces we want
    return '.'.join(components[-depth:])

####################

# Check a name, see if it's an IP quad, and if so, return reverse.
# If not, return the original name.
#
# This is better than a socket.gethostbyaddr() call because
# this will return the authority information for an address
# if no explicit reverse resolution is found.

def CheckIPReverse(hostquad):
    
    # If it's an IP address in quad form, reverse resolve it
    if re.match(IPQuad, hostquad):

        DIGResults = commands.getoutput(DIG + hostquad).splitlines()

        ansname = ""
        authname = hostquad
        # Results must either have an Answer or Authority record
        if ( DIGResults.count(ANS) + DIGResults.count(AUTH)):

            i = 0
            while i < len(DIGResults):

                if DIGResults[i].startswith(ANS):
                    ansname = DIGResults[i+1].split()[4]

                if DIGResults[i].startswith(AUTH):
                    authname = DIGResults[i+1].split()[-2]

                i += 1

        if ansname:
            hostname = ansname
        else:
            hostname = authname

        # Get rid of trailing dot, if any
        if hostname[-1] == '.':
            hostname = hostname[:-1]

    else:
        hostname = hostquad
        
    return hostname


####################

# Paw through a log record, doing any reverse resolution needed,
# confirm with user, and return name of the host to notify about
# the instrusion attempt.  A null return means the user want to
# skip this record.


def ProcessLogRecord(logrecord, NOMATCH, SHOWONLY):

    # Check for each known attack keyword

    sendto = ""
    for attackkey in AttackKeys.keys():

        logfield = logrecord.split()
        if logrecord.count(attackkey):

            # Even if it is a legitimate attack record,
            # we do not process it if it contains text
            # the user does not want matched.

            if NOMATCH and logrecord.count(NOMATCH):
                break

            if SHOWONLY:
                print logrecord
                break
        
            # Different attack records put the hostquad in different places
            hostquad =  logfield[AttackKeys[attackkey]]
            if hostquad[-1] == ',':
                hostquad = hostquad[:-1]         # Strip trailing dots

            # Go do a reverse resolution if we need to
            hostname = CheckIPReverse(hostquad)

            # Check for the case of getting a PTR record back
            hostname = ReversePTR(hostname)

            # Check if we've seen this abuser before
            if NameCache.has_key(hostname):
                sendto = NameCache[hostname]

            # New one
            else:
                depth = 2
                DONE=FALSE
                while not DONE:

                    # Set depth of default response
                    default = HostDepth(hostname, depth)

                    # Ask the user about it
                    st = raw_input(PROMPT % (logrecord, hostname[-40:],
                                             " " * (40 - len(hostname)),
                                   default))

                    # Parse the response
                    if st == "s":              # Skip this record
                        sendto = ""
                        DONE = TRUE

                    elif st.lower() == "l":    # More depth in recipient name
                        if depth < len(hostname.split('.')):
                             depth += 1

                    elif st.lower() == "r":    # Less depth in recipient name
                        if depth > 2:
                            depth -= 1

                    elif st.lower() == "w":    # Run a whois on 'em
                        print commands.getoutput(WHO + hostquad)

                    else:
                        if st:                 # User keyed in their own recipient
                            hostname = st
                        else:                  # User accepted the default
                            sendto = default
                            DONE = TRUE
                        
                NameCache[hostname] = sendto  # Cache it
            
    return sendto

####################

# Check the passed hostname and see if it looks like a PTR record.
# If so, strip out the address portion, reverse it, and trying
# doing another reverse lookup.  If not, just return the original hostname.

def ReversePTR(hostname):

    tmp = hostname.split("in-addr.arpa")

    if len(tmp) > 1:                       # Looks like a PTR record

        tmp = tmp[0].split('.')            # Get addr components
        tmp.reverse()                      # and reverse their order
        # Take the 1st four quads (some PTR records have more)
        # and see if we can dig out a reverse.
        hostname = CheckIPReverse('.'.join(tmp[1:5]))

    return hostname


#------------------------- Program Entry And Mail Loop -----------------------#



# Program entry and command line processing

try:
    opts, args = getopt.getopt(sys.argv[1:], '-d:e:m:s')
except getopt.GetoptError:
    print USAGE
    sys.exit(2)
    
OLDEST   = 0
MATCH    = ""
NOMATCH  = ""
SHOWONLY = FALSE

for opt, val in opts:
    if opt == "-d":
        OLDEST = time.time() - (int(val) * DLEN)
    if opt == "-e":
        NOMATCH = val
    if opt == "-m":
        MATCH = val
    if opt == "-s":
        SHOWONLY = TRUE


# Loop through the log, processing matching records

logfile    = open(LOG)
abuserfile = open(OUT, "w")

# Read in the whole log logfileile
for logrecord in logfile.read().splitlines():

    # Check to see whether this record should even be
    # processed.

    DOIT = TRUE
    # Did user limit how far back to look?
    if OLDEST:
        # Parse the record's time into into a list
        logfields = logrecord.split()
        logtime = logfields[2].split(":")
        EventTime = [None, logfields[0], logfields[1],
                     logtime[0], logtime[1], logtime[2]]

        # Figure out what year - not in the log explicitly
        # We do this by comparing the Month in the log entry
        # against today's month.  We get away with this so long
        # as the log never is allowed to get so big that it has
        # entries over a year old in it. (Which should be the case
        # for any reasonably administered system.
        
        lt = time.localtime()
        logyear = int(lt[0])
        if MOS.index(EventTime[1]) > int(lt[1]): # Log shows a later month
            logyear -= 1                         # 'Must be last year
        EventTime[0] = str(logyear)
        
        # Don't process if older than the oldest allowed
        if time.mktime(time.strptime("%s %s %s %s %s %s" % tuple(EventTime),
                                "%Y %b %d %H %M %S")) < OLDEST:
            DOIT = FALSE

    # Did user specify a selection matching string?
    if not logrecord.count(MATCH):
        DOIT = FALSE

    # If we passed all those tests, it's time to process this record.
    if DOIT:
        sendto = ProcessLogRecord(logrecord, NOMATCH, SHOWONLY)
        if sendto:
            abuserfile.write("abnot \"" + logrecord + "\" " +
                             sendto + "\n")

logfile.close()
abuserfile.close()