#!/usr/bin/python ############################################################################## ## ## Name: sra_checks.py ## ## Description: ## ## This script performs checks on Secure Remote Access logs including: ## ## - Incoming connections from outside the USA ## - High frequency of failed logons ## ## Exceptions and raw data reports are produced. ## ## Usage: ./sra_checks.py ## ## Author: Eric Ooi ## ############################################################################## ## Modules import time import GeoIP import gzip import re from datetime import datetime as dt from argparse import ArgumentParser from argparse import RawTextHelpFormatter ## Script Information NAME = 'Secure Remote Access Checks' VERSION = 'Version 1.0 (01/24/2013)' AUTHOR = 'Eric Ooi' COPYRIGHT = '(C) Copyright 2013 Eric Ooi' # Script Activity Log LOG_TIME = dt.strftime(dt.now(), "-%m-%d-%Y--%H-%M-%S") LOG_FILE = "sra_checks" + LOG_TIME + ".log" # script log activity ## Report Filenames REPORT_RAW = "sra_raw.csv" REPORT_OUTSIDE_AREA = "sra_outside_area.csv" REPORT_FAILED_LOGINS = "sra_failed_logins.csv" ## Generate path to data def generateDataPath(user_date=""): """Generate path to data""" data_root = '/backlog/sra_audits' if user_date: user_date = user_date.split('/') user_month = int(user_date[0]) if user_month < 10: user_month = '0' + str(user_month) user_day = int(user_date[1]) if user_day < 10: user_day = '0' + str(user_day) user_year = user_date[2] data_path = "%s/%s/%s/%s/" % (data_root, user_year, user_month, user_day) else: yesterday = time.time() - (60 * 60 * 24) year, month, day = time.localtime(yesterday)[:3] if month < 10: month = '0' + str(month) if day < 10: day = '0' + str(day) data_path = '%s/%s/%s/%s/' % (data_root, year, month, day) return data_path # parse through command line arguments parser = ArgumentParser(description=NAME+'\n'+VERSION+'\n'+AUTHOR+'\n'+COPYRIGHT, formatter_class=RawTextHelpFormatter) parser.add_argument('-d', action='store', dest='DATE', help='Run checks for a specific date') args = parser.parse_args() # set user date if requested if args.DATE: DATA_PATH = generateDataPath(args.DATE) # Script Activity Log LOG_FILE = DATA_PATH + LOG_FILE LOG_FH = open(LOG_FILE, 'wb') print "\nDATE SPECIFIED ON COMMAND LINE" LOG_FH.write("\n\nDATE SPECIFIED ON COMMAND LINE") print "Report Path:", DATA_PATH LOG_FH.write("\nReport Path: " + DATA_PATH) else: DATA_PATH = generateDataPath() # Script Activity Log LOG_FILE = DATA_PATH + LOG_FILE LOG_FH = open(LOG_FILE, 'wb') print "\nNO DATE SPECIFIED - RUN REPORT FOR YESTERDAY" LOG_FH.write("\n\nNO DATE SPECIFIED - RUN REPORT FOR YESTERDAY") print "Report Path:", DATA_PATH LOG_FH.write("\nReport Path: " + DATA_PATH) REPORT_RAW_FILE = DATA_PATH + REPORT_RAW REPORT_OUTSIDE_AREA_FILE = DATA_PATH + REPORT_OUTSIDE_AREA REPORT_FAILED_LOGINS_FILE = DATA_PATH + REPORT_FAILED_LOGINS ## Parse out records from SRA logs def parseRecords(): """Parse out records from SRA logs""" # field regex patterns pattern_local_time = re.compile("time=\"(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)") pattern_utc_time = re.compile("vp_time=\"(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d UTC)") pattern_src = re.compile("src=(\d+\.\d+\.\d+\.\d+)") pattern_dst = re.compile("dst=(\d+\.\d+\.\d+\.\d+)") pattern_user = re.compile("user=\"(.*?)\"") pattern_usr = re.compile("usr=\"(.*?)\"") pattern_msg = re.compile("msg=\"(.*?)\"") pattern_agent = re.compile("agent=\"(.*?)\"") # read sra.log.gz from syslog-ng data_FH = gzip.open(DATA_PATH + 'sra.log.gz') sra_raw = [] gi = GeoIP.open("/usr/share/GeoIP/GeoIPCity.dat",GeoIP.GEOIP_STANDARD) # parse out fields from SRA logs for record in data_FH: # search for regex patterns in SRA record local_time = pattern_local_time.search(record) utc_time = pattern_utc_time.search(record) src = pattern_src.search(record) dst = pattern_dst.search(record) user = pattern_user.search(record) usr = pattern_usr.search(record) msg = pattern_msg.search(record) agent = pattern_agent.search(record) # records with RFC 1918 source IP adddresses are private, these are the addresses assigned by SRA if src.group(1).startswith("192.168.") or src.group(1).startswith("10.") or src.group(1).startswith("172.16"): location = {} location['city'] = "Private" location['region'] = "Private" location['region_name'] = "Private" location['time_zone'] = "Private" location['country_code3'] = "Private" location['country_name'] = "Private" # records without RFC 1918 source IP addresses are external and should be analyzed for location data else: location = gi.record_by_addr(src.group(1)) sra_raw.append([local_time.group(1), utc_time.group(1), src.group(1), str(location['city']), str(location['region']), \ str(location['region_name']), str(location['time_zone']), str(location['country_code3']), str(location['country_name']), \ user.group(1), msg.group(1), agent.group(1)]) return sra_raw ## Analyze records for connections made outside the area def findOutsideArea(sra_raw): """Analyze records for connections made outside the area""" # define area area = ["California", "Oregon"] outside_area = [] # save all records where "Region" is not in the area for record in sra_raw: if record[3] == "Private": continue else: if record[5] not in area: outside_area.append(record) else: continue return outside_area ## Analyze login activity for high number of failed login attempts def findFailedLogins(sra_raw): """Analyze logon activity for high number of failed logon attempts""" FAILED_LOGINS_THRESHOLD = 10 all_fails = {} user_login_fails = {} high_fails_users = [] high_fails_records = [] # extract records ONLY with a login status of "User login failed" # group by sra_user # these are the high level "fail" messages for record in sra_raw: message = record[10].lower() sra_user = record[9] if message == "user login failed": if sra_user not in user_login_fails: user_login_fails[sra_user] = [] user_login_fails[sra_user].append(record) else: continue # extract ALL records with a login status containing "fail" # both "User login failed" and "Failure in kerberos_kinit_password" messages # group these records by sra_user # these are BOTH high level and detailed "fail" messages for record in sra_raw: message = record[10].lower() sra_user = record[9] if "fail" in message: if sra_user not in all_fails: all_fails[sra_user] = [] all_fails[sra_user].append(record) else: continue # count number of records with ONLY "User login failed" and save users with high counts for sra_user, fails in user_login_fails.iteritems(): if len(fails) >= FAILED_LOGINS_THRESHOLD: high_fails_users.append(sra_user) else: continue # for users with high number of fails, save ALL their records for sra_user, fails in all_fails.iteritems(): if sra_user in high_fails_users: high_fails_records = high_fails_records + fails else: continue return high_fails_records ## Print and format report data def reportPrinter(data_list, REPORT_FILE): """Print and format report data""" # write nothing if there's no data if not data_list: return REPORT_FH = open(REPORT_FILE, 'wb') # print headers at the top headers = ("Local Time", "UTC Time", "Source IP", "City", "Region", "Region Name", "Time Zone", "Country Code", "Country", "User", "Message", "Agent") REPORT_FH.write('"' + '","'.join(headers) + '"\n') # write out results as double-quoted and comma-delimited for record in data_list: record = tuple(record) REPORT_FH.write('"' + '","'.join(record) + '"\n') REPORT_FH.close() return ## Execute Secure Remote Access Checks def main(): """Execute Secure Remote Access Checks""" # Start script timer start = dt.now() LOG_FH.write("\n\nTime Start: " + str(start)) # run SRA checks print "\nRun SRA Checks" LOG_FH.write("\n\nRun SRA Checks") sra_raw = parseRecords() outside_area = findOutsideArea(sra_raw) high_fails = findFailedLogins(sra_raw) # write out SRA raw log with location data and analyses print "\nWriting out SRA raw log with location data and analyses" LOG_FH.write("\n\nWriting out SRA raw log with location data and analyses") reportPrinter(sra_raw, REPORT_RAW_FILE) reportPrinter(outside_area, REPORT_OUTSIDE_AREA_FILE) reportPrinter(high_fails, REPORT_FAILED_LOGINS_FILE) # End script timer end = dt.now() elapsed = end - start print "\nTimer: ", elapsed LOG_FH.write("\n\nTime End: " + str(end)) LOG_FH.write("\nTime Elapsed: " + str(elapsed)) # Close log file LOG_FH.close() return ## Run script if __name__ == '__main__': try: main() except (IOError, KeyError, AttributeError, ValueError, NameError, TypeError, LookupError), message: error = '%s -- Got Exception: %s\n' % (dt.now(), message) print error LOG_FH.write("\n\n" + error) except KeyboardInterrupt: print 'Ctrl-C Caught... Exiting' sys.exit(0)