allan icon

dshield

allan | PRO | 04/02/22 03:28:18 PM UTC | 0 ⭐ | 993 👁️ | Never ⏰ | []
Python |

8.73 KB

|

None

|

0 👍

/

0 👎

#!/usr/local/bin/python3
 
# Tested under version 22.1
# Disable circular logs under System > Settings > Logging
# requires dshield.ini from https://github.com/jullrich/dshieldpfsense/blob/master/dshield.sample
#
# cron - create /etc/cron.d/dshield
# 53    2,5,8,11,14,17,20,23    *       *       *       root    /usr/local/bin/dshield
 
 
import sys
import configparser
import ipaddress
import pytz
import smtplib
from datetime import date, datetime
from email.message import EmailMessage
from os import path
 
 
config_file = '/usr/local/etc/dshield.ini'
timezone_name = 'US/Central'
dshield_email_addr = '[email protected]'
last_run_timestamp_file = '/var/run/dshield'
filter_log_dir = '/var/log/filter'
 
smtp_server_name = 'localhost'
smtp_server_port = 25
 
def parse_record(log_line: str) -> dict:
  log = {}
  log_fields = log_line.split(',')
  
  log['rulenr'] = int(log_fields[0])
  log['ridentifier'] = log_fields[3]
  log['interface'] = log_fields[4]
  log['reason'] = log_fields[5]
  log['action'] = log_fields[6]
  log['dir'] = log_fields[7]
  log['version'] = int(log_fields[8])
 
  if log['version'] == 4:
    log['tos'] = log_fields[9]
    log['ttl'] = int(log_fields[11])
    log['id'] = int(log_fields[12])
    log['offset'] = int(log_fields[13])
    log['ipflags'] = log_fields[14]
    log['proto'] = int(log_fields[15])
    log['protoname'] = log_fields[16].upper()
    log['length'] = int(log_fields[17])
    log['src'] = log_fields[18]
    log['dst'] = log_fields[19]
 
    if log['proto'] == 1:
      log['srcport'] = '???'
      log['dstport'] = '???'
 
    # proto  6 = TCP
    # proto 17 = UDP
    if log['proto'] in [6, 17]:
      log['srcport'] = int(log_fields[20])
      log['dstport'] = int(log_fields[21])
      log['datalen'] = int(log_fields[22])
 
      if log['proto'] == 6:
        log['tcpflags'] = log_fields[23]
        log['seq'] = log_fields[24]
        log['ack'] = log_fields[25]
        log['urp'] = int(log_fields[26])
        log['tcpoptions'] = log_fields[28]
    else:
      log['options'] = log_fields[20]
 
  elif log['version'] == 6:
    log['class'] = log_fields[9]
    log['flow'] = log_fields[10]
    log['hoplimit'] = log_fields[11]
    log['protoname'] = log_fields[12]
    log['proto'] = int(log_fields[13])
    log['payload-length'] = int(log_fields[14])
    log['src'] = ipaddress.ip_address(log_fields[15]).exploded
    log['dst'] = ipaddress.ip_address(log_fields[16]).exploded
 
    if log['proto'] == 58:
      # replace 'IPV6-ICMP' with standard name
      log['protoname'] = 'ICMP'
 
    if log['proto'] in [6, 17]:
      log['srcport'] = int(log_fields[17])
      log['dstport'] = int(log_fields[18])
      log['datalen'] = int(log_fields[19])
 
      if log['proto'] == 6:
        log['tcpflags'] = log_fields[20]
 
        # leaving seq as string as '2028535965:2028536024' is possible
        log['seq'] = log_fields[21]
        log['ack'] = log_fields[22]
        log['urp'] = int(log_fields[23])
        log['tcpoptions'] = log_fields[25]
 
  if debug:
    for field in log.keys():
      if type(log[field]) in [int]:
        print(f"{field}: {log[field]}")
      elif type(log[field]) == datetime:
        print(f"{field}: {log[field].strftime('%Y-%m-%dT%H:%M:%S%z')}")
      else:
        print(f"{field}: '{log[field]}'")
 
  return log
 
 
def check_record(record: dict) -> list:
  issues = []
  if record['interface'] not in interfaces:
    issues.append(f"{record['interface']} not in interface list")
  elif record['dir'] == 'out':
    issues.append(f"{record['interface']} traffic is outbound")
 
  ip_src = ipaddress.ip_address(record['src']) 
  if ip_src in authorized_source_ip:
    issues.append(f"{record['src']} in authorized_source_ip")
 
  if not ip_src.is_global:
    issues.append(f"src:{ip_src.compressed} not valid IP")
 
  ip_dst = ipaddress.ip_address(record['dst']) 
  if not ip_dst.is_global or ip_dst.is_multicast:
    issues.append(f"dst:{ip_dst.compressed} not valid IP")
 
  if record['protoname'] == 'IGMP':
    issues.append(f"skipping {record['protoname']}")
 
  if debug and len(issues) > 0:
    print(f"Issues found: {issues}")
 
  return issues
 
 
def localize_datetime(log_date: str) -> datetime:
  log_datetime = datetime.strptime(log_date, "%b %d %H:%M:%S")
  if today_month < log_datetime.month:
    log_datetime = log_datetime.replace(year=today_year - 1)
  else:
    log_datetime = log_datetime.replace(year=today_year)
 
  return local_tz.localize(log_datetime)
 
 
def add_record_to_msg(record:dict) -> None:
  global msg_body
 
  record_date = record.get('date').strftime('%Y-%m-%d %H:%M:%S %z')
 
  # add colon to TZ field as required for Dshield format
  email_date = f'{record_date[:-2]}:{record_date[-2:]}'
  src_port = record.get('srcport', '')
 
  dst_port = record.get('dstport', '')
  tcp_flags = record.get('tcpflags', '')
 
  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"
 
  if debug:
    print(f'msg+: {msg_record}', end='')
 
  msg_body += msg_record
 
 
def compose_email(body: str) -> EmailMessage:
  msg = EmailMessage()
 
  local_tz_offset = datetime.now(local_tz).strftime('%z')
  msg['Subject'] = f'FORMAT DSHIELD USERID {uid} TZ {local_tz_offset[:-2]}:{local_tz_offset[-2:]} OPNsense 0.01'
  msg['To'] = dshield_email_addr
  msg['From'] = from_address
 
  if cc_address != '':
    msg['CC'] = cc_address
 
  msg.set_content(body)
 
  if debug:
    print_header('=', 80, 'EMAIL TO SEND')
    print(msg.as_string())
 
  return msg
 
 
def print_header(char: str, length: int, header: str = None) -> None:
  print(char * length)
 
  if header is not None:
    spacer_length = int((length - len(header) - 4) / 2)
    print(char * spacer_length, end='  ')
    print(header, end='  ')
    print(char * (spacer_length + (length - len(header) - 4) % spacer_length))
    print(char * length)
 
 
def read_timestamp(file_name: str) -> int:
  timestamp = 0
 
  try:
    if path.exists(file_name) and path.isfile(file_name):
      with open(file_name, 'r') as f:
        timestamp = int(f.read())
  except ValueError:
    pass
 
  if debug:
    print(f'Last run: {timestamp}')
 
  return timestamp
 
 
def write_timestamp(file_name: str) -> int:
  timestamp = int(datetime.now().timestamp())
 
  with open(file_name, 'w') as f:
    f.write(str(timestamp))
 
  return timestamp
 
 
config = configparser.ConfigParser()
if not config.read(config_file):
  print(f"Cannot open '{config_file}' file for reading. Exiting.")
  sys.exit()
 
filter_file = datetime.now().strftime(f'{filter_log_dir}/filter_%Y%m%d.log')
if not path.exists(filter_file) or not path.isfile(filter_file):
  print(f"Cannot open '{filter_file}' for reading. Exiting.")
  sys.exit()
 
# get dshield.ini config parameters
debug = config['dshield'].getboolean('debug', False)
interfaces = config['dshield'].get('interfaces', '').strip('"\'').split(',')
authorized_source_ip = [ipaddress.ip_address(ip) for ip in config['dshield'].get('authorized_source_ip', '').strip('"\'').split(',')]
uid = config['dshield'].get('uid', '').strip('"\'')
from_address = config['dshield'].get('fromaddr', '').strip('"\'')
cc_address = config['dshield'].get('ccaddr', '').strip('"\'')
 
if debug:
  print_header('-', 80, 'PROGRAM PARAMETERS')
  print(f'Interfaces: {interfaces}')
  print(f'Authorized IPs: {authorized_source_ip}')
  print(f'UID: \'{uid}\'')
  print(f'fromaddr: \'{from_address}\'')
  print(f'ccaddr: \'{cc_address}\'')
 
local_tz = pytz.timezone(timezone_name)
last_run_timestamp = read_timestamp(last_run_timestamp_file)
 
lines = 0
msg_body = ''
 
with open(filter_file, 'r') as f:
  today_month = date.today().month
  today_year = date.today().year
 
  for filter_line in f:
    log_date = datetime.fromisoformat(filter_line.split()[1])
    if log_date.timestamp() < last_run_timestamp:
      continue
 
    if debug:
      print_header('-', 80)
      print(f"stdin: {filter_line}", end='')
 
    try:
      log = parse_record(filter_line.split()[8])
      log['date'] = log_date
 
      if check_record(log) == []:
        add_record_to_msg(log)
        lines += 1
 
    except (IndexError, ValueError, pytz.exceptions.AmbiguousTimeError):
      # IndexError happens when there are missing fields sent to parse_record()
      # ValueError happens when corrupt log causes int() in parse_record() to fail
      # pytz.exceptions.AmbiguousTimeError can happen when DST switches back to Standard Time
      pass
 
 
if lines > 0:
  if debug:
    print(msg_body)
 
  with smtplib.SMTP(smtp_server_name, smtp_server_port) as smtp:
    smtp.send_message(compose_email(msg_body))
    smtp.quit()
 
write_timestamp(last_run_timestamp_file)
 

Comments

  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎