Advertisement
allan

dshield-pre22.1

Feb 1st, 2022
1,162
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.70 KB | None | 0 0
  1. #!/usr/local/bin/python3
  2.  
  3. # Tested under version 21.7.8
  4. # Disable circular logs under System > Settings > Logging
  5. # requires dshield.ini from https://github.com/jullrich/dshieldpfsense/blob/master/dshield.sample
  6. #
  7. # cron - create /etc/cron.d/dshield
  8. # 53    2,5,8,11,14,17,20,23    *   *   *   root    /usr/local/bin/dshield
  9.  
  10.  
  11. import sys
  12. import configparser
  13. import ipaddress
  14. import pytz
  15. import smtplib
  16. from datetime import date, datetime
  17. from email.message import EmailMessage
  18. from os import path
  19.  
  20.  
  21. config_file = '/usr/local/etc/dshield.ini'
  22. timezone_name = 'US/Central'
  23. dshield_email_addr = 'reports@dshield.org'
  24. last_run_timestamp_file = '/var/run/dshield'
  25. filter_log_dir = '/var/log/filter'
  26.  
  27. smtp_server_name = 'localhost'
  28. smtp_server_port = 25
  29.  
  30. def parse_record(log_line: str) -> dict:
  31.   log = {}
  32.   log_fields = log_line.split(',')
  33.  
  34.   log['rulenr'] = int(log_fields[0])
  35.   log['ridentifier'] = log_fields[3]
  36.   log['interface'] = log_fields[4]
  37.   log['reason'] = log_fields[5]
  38.   log['action'] = log_fields[6]
  39.   log['dir'] = log_fields[7]
  40.   log['version'] = int(log_fields[8])
  41.  
  42.   if log['version'] == 4:
  43.     log['tos'] = log_fields[9]
  44.     log['ttl'] = int(log_fields[11])
  45.     log['id'] = int(log_fields[12])
  46.     log['offset'] = int(log_fields[13])
  47.     log['ipflags'] = log_fields[14]
  48.     log['proto'] = int(log_fields[15])
  49.     log['protoname'] = log_fields[16].upper()
  50.     log['length'] = int(log_fields[17])
  51.     log['src'] = log_fields[18]
  52.     log['dst'] = log_fields[19]
  53.  
  54.     if log['proto'] == 1:
  55.       log['srcport'] = '???'
  56.       log['dstport'] = '???'
  57.  
  58.     # proto  6 = TCP
  59.     # proto 17 = UDP
  60.     if log['proto'] in [6, 17]:
  61.       log['srcport'] = int(log_fields[20])
  62.       log['dstport'] = int(log_fields[21])
  63.       log['datalen'] = int(log_fields[22])
  64.  
  65.       if log['proto'] == 6:
  66.         log['tcpflags'] = log_fields[23]
  67.         log['seq'] = log_fields[24]
  68.         log['ack'] = log_fields[25]
  69.         log['urp'] = int(log_fields[26])
  70.         log['tcpoptions'] = log_fields[28]
  71.     else:
  72.       log['options'] = log_fields[20]
  73.  
  74.   elif log['version'] == 6:
  75.     log['class'] = log_fields[9]
  76.     log['flow'] = log_fields[10]
  77.     log['hoplimit'] = log_fields[11]
  78.     log['protoname'] = log_fields[12].upper()
  79.     log['proto'] = int(log_fields[13])
  80.     log['payload-length'] = int(log_fields[14])
  81.     log['src'] = ipaddress.ip_address(log_fields[15]).exploded
  82.     log['dst'] = ipaddress.ip_address(log_fields[16]).exploded
  83.  
  84.     if log['proto'] == 58:
  85.       # replace 'IPV6-ICMP' with standard name
  86.       log['protoname'] = 'ICMP'
  87.  
  88.     if log['proto'] in [6, 17]:
  89.       log['srcport'] = int(log_fields[17])
  90.       log['dstport'] = int(log_fields[18])
  91.       log['datalen'] = int(log_fields[19])
  92.  
  93.       if log['proto'] == 6:
  94.         log['tcpflags'] = log_fields[20]
  95.  
  96.         # leaving seq as string as '2028535965:2028536024' is possible
  97.         log['seq'] = log_fields[21]
  98.         log['ack'] = log_fields[22]
  99.         log['urp'] = int(log_fields[23])
  100.         log['tcpoptions'] = log_fields[25]
  101.  
  102.   if debug:
  103.     for field in log.keys():
  104.       if type(log[field]) in [int]:
  105.         print(f"{field}: {log[field]}")
  106.       elif type(log[field]) == datetime:
  107.         print(f"{field}: {log[field].strftime('%Y-%m-%dT%H:%M:%S%z')}")
  108.       else:
  109.         print(f"{field}: '{log[field]}'")
  110.  
  111.   return log
  112.  
  113.  
  114. def check_record(record: dict) -> list:
  115.   issues = []
  116.   if record['interface'] not in interfaces:
  117.     issues.append(f"{record['interface']} not in interface list")
  118.   elif record['dir'] == 'out':
  119.     issues.append(f"{record['interface']} traffic is outbound")
  120.  
  121.   ip_src = ipaddress.ip_address(record['src'])
  122.   if ip_src in authorized_source_ip:
  123.     issues.append(f"{record['src']} in authorized_source_ip")
  124.  
  125.   if not ip_src.is_global:
  126.     issues.append(f"src:{ip_src.compressed} not valid IP")
  127.  
  128.   ip_dst = ipaddress.ip_address(record['dst'])
  129.   if not ip_dst.is_global or ip_dst.is_multicast:
  130.     issues.append(f"dst:{ip_dst.compressed} not valid IP")
  131.  
  132.   if record['protoname'] == 'IGMP':
  133.     issues.append(f"skipping {record['protoname']}")
  134.  
  135.   if debug and len(issues) > 0:
  136.     print(f"Issues found: {issues}")
  137.  
  138.   return issues
  139.  
  140.  
  141. def localize_datetime(log_date: str) -> datetime:
  142.   log_datetime = datetime.strptime(log_date, "%b %d %H:%M:%S")
  143.   if today_month < log_datetime.month:
  144.     log_datetime = log_datetime.replace(year=today_year - 1)
  145.   else:
  146.     log_datetime = log_datetime.replace(year=today_year)
  147.  
  148.   return local_tz.localize(log_datetime)
  149.  
  150.  
  151. def add_record_to_msg(record:dict) -> None:
  152.   global msg_body
  153.  
  154.   record_date = record.get('date').strftime('%Y-%m-%d %H:%M:%S %z')
  155.  
  156.   # add colon to TZ field as required for Dshield format
  157.   email_date = f'{record_date[:-2]}:{record_date[-2:]}'
  158.   src_port = record.get('srcport', '')
  159.  
  160.   dst_port = record.get('dstport', '')
  161.   tcp_flags = record.get('tcpflags', '')
  162.  
  163.   msg_record = f"{email_date}\t{uid}\t1\t{record['src']}\t{src_port}\t{record['dst']}\t{dst_port}\t{record['protoname']}\t{tcp_flags}\n"
  164.  
  165.   if debug:
  166.     print(f'msg+: {msg_record}', end='')
  167.  
  168.   msg_body += msg_record
  169.  
  170.  
  171. def compose_email(body: str) -> EmailMessage:
  172.   msg = EmailMessage()
  173.  
  174.   local_tz_offset = datetime.now(local_tz).strftime('%z')
  175.   msg['Subject'] = f'FORMAT DSHIELD USERID {uid} TZ {local_tz_offset[:-2]}:{local_tz_offset[-2:]} OPNsense 0.01'
  176.   msg['To'] = dshield_email_addr
  177.   msg['From'] = from_address
  178.  
  179.   if cc_address != '':
  180.     msg['CC'] = cc_address
  181.  
  182.   msg.set_content(body)
  183.  
  184.   if debug:
  185.     print_header('=', 80, 'EMAIL TO SEND')
  186.     print(msg.as_string())
  187.  
  188.   return msg
  189.  
  190.  
  191. def print_header(char: str, length: int, header: str = None) -> None:
  192.   print(char * length)
  193.  
  194.   if header is not None:
  195.     spacer_length = int((length - len(header) - 4) / 2)
  196.     print(char * spacer_length, end='  ')
  197.     print(header, end='  ')
  198.     print(char * (spacer_length + (length - len(header) - 4) % spacer_length))
  199.     print(char * length)
  200.  
  201.  
  202. def read_timestamp(file_name: str) -> int:
  203.   timestamp = 0
  204.  
  205.   try:
  206.     if path.exists(file_name) and path.isfile(file_name):
  207.       with open(file_name, 'r') as f:
  208.         timestamp = int(f.read())
  209.   except ValueError:
  210.     pass
  211.  
  212.   if debug:
  213.     print(f'Last run: {timestamp}')
  214.  
  215.   return timestamp
  216.  
  217.  
  218. def write_timestamp(file_name: str) -> int:
  219.   timestamp = int(datetime.now().timestamp())
  220.  
  221.   with open(file_name, 'w') as f:
  222.     f.write(str(timestamp))
  223.  
  224.   return timestamp
  225.  
  226.  
  227. config = configparser.ConfigParser()
  228. if not config.read(config_file):
  229.   print(f"Cannot open '{config_file}' file for reading. Exiting.")
  230.   sys.exit()
  231.  
  232. filter_file = datetime.now().strftime(f'{filter_log_dir}/filter_%Y%m%d.log')
  233. if not path.exists(filter_file) or not path.isfile(filter_file):
  234.   print(f"Cannot open '{filter_file}' for reading. Exiting.")
  235.   sys.exit()
  236.  
  237. # get dshield.ini config parameters
  238. debug = config['dshield'].getboolean('debug', False)
  239. interfaces = config['dshield'].get('interfaces', '').strip('"\'').split(',')
  240. authorized_source_ip = [ipaddress.ip_address(ip) for ip in config['dshield'].get('authorized_source_ip', '').strip('"\'').split(',')]
  241. uid = config['dshield'].get('uid', '').strip('"\'')
  242. from_address = config['dshield'].get('fromaddr', '').strip('"\'')
  243. cc_address = config['dshield'].get('ccaddr', '').strip('"\'')
  244.  
  245. if debug:
  246.   print_header('-', 80, 'PROGRAM PARAMETERS')
  247.   print(f'Interfaces: {interfaces}')
  248.   print(f'Authorized IPs: {authorized_source_ip}')
  249.   print(f'UID: \'{uid}\'')
  250.   print(f'fromaddr: \'{from_address}\'')
  251.   print(f'ccaddr: \'{cc_address}\'')
  252.  
  253. local_tz = pytz.timezone(timezone_name)
  254. last_run_timestamp = read_timestamp(last_run_timestamp_file)
  255.  
  256. lines = 0
  257. msg_body = ''
  258.  
  259. with open(filter_file, 'r') as f:
  260.   today_month = date.today().month
  261.   today_year = date.today().year
  262.  
  263.   for filter_line in f:
  264.     log_date = localize_datetime(filter_line[:15])
  265.     if log_date.timestamp() < last_run_timestamp:
  266.       continue
  267.  
  268.     if debug:
  269.       print_header('-', 80)
  270.       print(f"stdin: {filter_line}", end='')
  271.  
  272.     try:
  273.       log = parse_record(filter_line.split()[5])
  274.       log['date'] = log_date
  275.  
  276.       if check_record(log) == []:
  277.         add_record_to_msg(log)
  278.         lines += 1
  279.  
  280.     except (IndexError, ValueError, pytz.exceptions.AmbiguousTimeError):
  281.       # IndexError happens when there are missing fields sent to parse_record()
  282.       # ValueError happens when corrupt log causes int() in parse_record() to fail
  283.       # pytz.exceptions.AmbiguousTimeError can happen when DST switches back to Standard Time
  284.       pass
  285.  
  286.  
  287. if lines > 0:
  288.   if debug:
  289.     print(msg_body)
  290.  
  291.   with smtplib.SMTP(smtp_server_name, smtp_server_port) as smtp:
  292.     smtp.send_message(compose_email(msg_body))
  293.     smtp.quit()
  294.  
  295. write_timestamp(last_run_timestamp_file)
  296.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement